admin_url('admin-post.php?action=abcode_dbe_download'), 'downloadNonce' => wp_create_nonce('abcode_dbe_download_nonce'), 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('abcode_dbe_nonce'), 'i18n' => [ 'standardSql' => __('Standard SQL file', 'abcode-database-exporter'), 'compressedSmaller' => __('Compressed, smaller file', 'abcode-database-exporter'), 'justNow' => __('just now', 'abcode-database-exporter'), /* translators: %d: number of minutes */ 'minuteAgo' => __('%d minute ago', 'abcode-database-exporter'), /* translators: %d: number of minutes */ 'minutesAgo' => __('%d minutes ago', 'abcode-database-exporter'), /* translators: %d: number of hours */ 'hourAgo' => __('%d hour ago', 'abcode-database-exporter'), /* translators: %d: number of hours */ 'hoursAgo' => __('%d hours ago', 'abcode-database-exporter'), /* translators: %d: number of days */ 'dayAgo' => __('%d day ago', 'abcode-database-exporter'), /* translators: %d: number of days */ 'daysAgo' => __('%d days ago', 'abcode-database-exporter'), /* translators: %s: error message */ 'error' => __('Error: %s', 'abcode-database-exporter'), 'retryExport' => __('Retry Export', 'abcode-database-exporter'), 'selectTables' => __('Select tables to export', 'abcode-database-exporter'), /* translators: %s: table count label (e.g. "5 tables") */ 'exportBtn' => __('Export (%s)', 'abcode-database-exporter'), 'restartExport' => __('Restart Export', 'abcode-database-exporter'), /* translators: %s: file format (e.g. .sql, .sql.gz) */ 'downloadFormat' => __('Download .%s', 'abcode-database-exporter'), /* translators: %d: number of tables */ 'tablesSingular' => __('%d table', 'abcode-database-exporter'), /* translators: %d: number of tables */ 'tablesPlural' => __('%d tables', 'abcode-database-exporter'), 'exporting' => __('Exporting...', 'abcode-database-exporter'), 'startingExport' => __('Starting export...', 'abcode-database-exporter'), 'finalisingExport' => __('Finalising export...', 'abcode-database-exporter'), 'done' => __('Done!', 'abcode-database-exporter'), /* translators: %s: relative time (e.g. "5 minutes ago") */ 'lastExported' => __('Last exported: %s', 'abcode-database-exporter'), 'exportAgain' => __('Export Again', 'abcode-database-exporter'), 'networkErrorFinalise' => __('Network error during finalisation', 'abcode-database-exporter'), /* translators: %1$d: current table number, %2$d: total tables, %3$s: table name */ 'exportingTable' => __('Exporting table %1$d of %2$d: %3$s', 'abcode-database-exporter'), /* translators: %s: table name */ 'failedExportTable' => __('Failed to export table: %s', 'abcode-database-exporter'), /* translators: %s: table name */ 'networkErrorTable' => __('Network error exporting table: %s', 'abcode-database-exporter'), 'networkErrorStart' => __('Network error starting export', 'abcode-database-exporter'), 'failedStartExport' => __('Failed to start export', 'abcode-database-exporter'), 'failed' => __('Failed', 'abcode-database-exporter'), 'schemaCopied' => __('CREATE TABLE copied to clipboard', 'abcode-database-exporter'), 'copySchema' => __('Copy CREATE TABLE to clipboard', 'abcode-database-exporter'), 'downloadCsv' => __('Download as CSV', 'abcode-database-exporter'), 'exportSql' => __('Export as SQL', 'abcode-database-exporter'), ], ] ); wp_add_inline_script('abcode-dbe', abcode_dbe_inline_js()); }); /** * Returns the plugin's admin CSS as a string for wp_add_inline_style(). * * @return string CSS content. */ function abcode_dbe_inline_css() { ob_start(); ?> .abcode-dbe-banner { background: #0f0d25; margin: -1px -20px 0; display: flex; align-items: center; gap: 18px; height: 60px; overflow: hidden; padding: 15px 32px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px; color: #fff; position: relative; border-bottom: 3px solid rgba(255, 255, 255, 0.1); } .abcode-dbe-banner-badge { position: absolute; top: 0; left: 50%; transform: translateX(-50%); z-index: 10; background: linear-gradient(0deg, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0.05) 100%); color: rgb(253, 251, 255); font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; padding: 5px 20px; border-radius: 0 0 8px 8px; border-top: none; border-right: 0.5px solid rgba(255,255,255,0.12); border-bottom: 0.5px solid rgba(255,255,255,0.12); border-left: 0.5px solid rgba(255,255,255,0.12); line-height: 1; height: 15px; display: inline-flex; align-items: center; justify-content: center; box-shadow: rgba(0,0,0,0.18) 0 0 2px; } .abcode-dbe-banner-logo { width: 46px; height: 46px; flex-shrink: 0; } .abcode-dbe-banner-logo svg { width: 46px; height: 46px; } .abcode-dbe-banner-title-wrap { display: flex; flex-direction: column; gap: 2px; } .abcode-dbe-banner-title-row { display: flex; align-items: center; gap: 10px; } .abcode-dbe-banner-title-row h1 { margin: 0; font-size: 23px; font-weight: 600; color: #fff; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; line-height: normal; position: relative; } .abcode-dbe-banner-separator { color: rgba(255,255,255,0.3); font-size: 18px; font-weight: 300; } .abcode-dbe-banner-section { background: linear-gradient(135deg, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0.05) 100%); color: rgba(255,255,255,0.7); opacity: 0.9; font-size: 12px; font-weight: 400; text-transform: uppercase; letter-spacing: 1px; line-height: normal; padding: 6px 12px; border-radius: 20px; border: 1px solid rgba(255,255,255,0.1); align-self: center; position: relative; top: 1px; } .abcode-dbe-banner-subtitle { font-size: 11px; color: rgba(255,255,255,0.4); } .abcode-dbe-banner-subtitle a { color: rgba(255,255,255,0.4); text-decoration: none; } .abcode-dbe-banner-subtitle a:hover { color: rgba(255,255,255,0.7); } .abcode-dbe-rate { display: flex; align-items: center; justify-content: center; gap: 6px; margin-top: 16px; color: #646970; text-decoration: none; font-size: 13px; transition: color 0.15s; } .abcode-dbe-rate:hover { color: #0f0d25; } .abcode-dbe-rate svg { fill: #f5a623; } .abcode-dbe-wrap { max-width: 680px; margin: 30px auto 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .abcode-dbe-card { background: #fff; border: none; border-radius: 10px; box-shadow: 0 1px 4px rgba(15,13,37,0.08), 0 4px 16px rgba(15,13,37,0.04); padding: 32px; } .abcode-dbe-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 28px; } .abcode-dbe-stat { background: #f4f0fa; border: 1px solid #e2dff0; border-radius: 8px; padding: 16px; text-align: center; } .abcode-dbe-stat .label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: #646970; margin-bottom: 4px; } .abcode-dbe-stat .value { font-size: 22px; font-weight: 600; color: #0f0d25; } .abcode-dbe-stat .value.loading { color: #c3c4c7; } .abcode-dbe-table-list { border: 1px solid #e2dff0; border-radius: 8px; margin-bottom: 28px; overflow: hidden; } .abcode-dbe-table-scroll { max-height: 220px; overflow-y: auto; } .abcode-dbe-table-list.locked .tbl-check, .abcode-dbe-table-list.locked #abcode-dbe-check-all { pointer-events: none; } .abcode-dbe-table-list.locked .abcode-dbe-table-dl, .abcode-dbe-table-list.locked .abcode-dbe-table-csv, .abcode-dbe-table-list.locked .abcode-dbe-table-schema { pointer-events: none; opacity: 0.3; } .abcode-dbe-table-row { display: grid; grid-template-columns: 28px 1fr 60px 170px; align-items: center; padding: 12px 16px; font-size: 13px; gap: 10px; border-bottom: 1px solid #f0eef5; } .abcode-dbe-table-head { display: grid; grid-template-columns: 28px 1fr 60px 170px; align-items: center; padding: 10px 16px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: rgba(255,255,255,0.7); gap: 10px; background: #0f0d25; border-bottom: 1px solid rgba(255,255,255,0.08); position: sticky; top: 0; z-index: 1; } .abcode-dbe-table-head input { width: 16px; height: 16px; margin: 0; cursor: pointer; accent-color: #1eb300; } .abcode-dbe-head-rows { text-align: center; } .abcode-dbe-head-action { text-align: right; } .abcode-dbe-table-row:last-child { border-bottom: none; } .abcode-dbe-table-row:nth-child(even) { background: #faf9fc; } .abcode-dbe-table-row .tbl-check { width: 16px; height: 16px; margin: 0; cursor: pointer; flex-shrink: 0; accent-color: #1eb300; } .abcode-dbe-table-row .name { font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; font-size: 12px; color: #0f0d25; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .abcode-dbe-table-row .rows { color: #646970; font-size: 12px; white-space: nowrap; text-align: center; } .abcode-dbe-table-row .action-col { display: flex; align-items: center; justify-content: flex-end; gap: 4px; } .abcode-dbe-table-row .status-icon { display: none; width: 16px; height: 16px; flex-shrink: 0; } .abcode-dbe-table-row.exporting .abcode-dbe-table-dl, .abcode-dbe-table-row.exported .abcode-dbe-table-dl { display: none !important; } .abcode-dbe-table-row.exporting .action-col::before { content: ''; display: block; width: 16px; height: 16px; border: 2px solid #e2dff0; border-top-color: #0f0d25; border-radius: 50%; animation: abcode-dbe-spin 0.6s linear infinite; flex-shrink: 0; } .abcode-dbe-table-row.exported .status-icon.check { display: inline-flex !important; align-items: center; justify-content: center; } @keyframes abcode-dbe-spin { to { transform: rotate(360deg); } } .abcode-dbe-table-row .status-icon.check svg { fill: #1eb300; } .abcode-dbe-table-dl { background: none; border: 1px solid #c3c4c7; border-radius: 4px; padding: 3px 8px; font-size: 11px; color: #0f0d25; cursor: pointer; white-space: nowrap; transition: all 0.15s; } .abcode-dbe-table-dl:hover { background: #f4f0fa; border-color: #0f0d25; } .abcode-dbe-table-dl:disabled { opacity: 0.4; cursor: not-allowed; } .abcode-dbe-btn { display: inline-flex; align-items: center; gap: 10px; background: #0f0d25; color: #fff; border: none; border-radius: 8px; padding: 14px 28px; font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.2s; width: 100%; justify-content: center; } .abcode-dbe-btn:hover { background: #1a1740; } .abcode-dbe-btn:disabled { background: #a7aaad; cursor: not-allowed; } .abcode-dbe-btn svg { width: 20px; height: 20px; fill: currentColor; } .abcode-dbe-progress-wrap { display: none; margin-top: 16px; } .abcode-dbe-progress-wrap.active { display: block; } .abcode-dbe-progress-bar-outer { width: 100%; height: 18px; background: #f4f0fa; border-radius: 9px; overflow: hidden; margin-bottom: 10px; } .abcode-dbe-progress-bar-inner { height: 100%; width: 0%; background: linear-gradient(90deg, #1eb300, #5cd645); border-radius: 9px; transition: width 0.4s ease; } .abcode-dbe-progress-status { display: flex; justify-content: space-between; font-size: 13px; color: #646970; margin-bottom: 20px; } .abcode-dbe-success { display: none; align-items: center; gap: 10px; padding: 12px 16px; background: rgba(30, 179, 0, 0.08); border: 1px solid rgba(30, 179, 0, 0.3); border-radius: 8px; margin-top: 16px; color: #145a00; font-size: 14px; } .abcode-dbe-success.active { display: flex; } .abcode-dbe-success svg { width: 22px; height: 22px; fill: #1eb300; flex-shrink: 0; } .abcode-dbe-download-area { display: none; flex-direction: column; gap: 16px; margin-top: 16px; } .abcode-dbe-download-area.active { display: flex; } .abcode-dbe-dl-btn { display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; background: #1eb300; color: #fff; border: none; padding: 14px 18px; border-radius: 8px; font-weight: 600; font-size: 15px; cursor: pointer; transition: background 0.15s; } .abcode-dbe-dl-btn:hover { background: #19a000; } .abcode-dbe-dl-btn svg { width: 18px; height: 18px; fill: #fff; } .abcode-dbe-search { display: none; padding: 10px 16px; border-bottom: 1px solid #e2dff0; background: #faf9fc; } .abcode-dbe-search.active { display: block; } .abcode-dbe-search input { width: 100%; padding: 7px 10px 7px 32px; border: 1px solid #e2dff0; border-radius: 6px; font-size: 13px; outline: none; background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23a7aaad'%3E%3Cpath d='M15.5 14h-.79l-.28-.27A6.47 6.47 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3C/svg%3E") 8px center / 16px no-repeat; box-sizing: border-box; transition: border-color 0.15s; } .abcode-dbe-search input:focus { border-color: #0f0d25; box-shadow: 0 0 0 1px rgba(15,13,37,0.2); } .abcode-dbe-table-row .abcode-dbe-table-csv, .abcode-dbe-table-row .abcode-dbe-table-schema { background: none; border: 1px solid #c3c4c7; border-radius: 4px; padding: 3px 8px; font-size: 11px; color: #646970; cursor: pointer; white-space: nowrap; transition: all 0.15s; } .abcode-dbe-table-row .abcode-dbe-table-csv:hover, .abcode-dbe-table-row .abcode-dbe-table-schema:hover { background: #f4f0fa; border-color: #0f0d25; } .abcode-dbe-table-row .abcode-dbe-table-csv:disabled, .abcode-dbe-table-row .abcode-dbe-table-schema:disabled { opacity: 0.4; cursor: not-allowed; } .abcode-dbe-table-row.exporting .abcode-dbe-table-csv, .abcode-dbe-table-row.exported .abcode-dbe-table-csv, .abcode-dbe-table-row.exporting .abcode-dbe-table-schema, .abcode-dbe-table-row.exported .abcode-dbe-table-schema { display: none !important; } .abcode-dbe-last-export { display: none; align-items: center; gap: 8px; padding: 10px 14px; background: #f4f0fa; border: 1px solid #e2dff0; border-radius: 8px; margin-bottom: 20px; font-size: 13px; color: #0f0d25; } .abcode-dbe-last-export.active { display: flex; } .abcode-dbe-last-export svg { width: 16px; height: 16px; fill: #0f0d25; flex-shrink: 0; } .abcode-dbe-format-select { display: flex; align-items: center; gap: 12px; } .abcode-dbe-format-label { font-size: 13px; font-weight: 500; color: #0f0d25; } .abcode-dbe-format-pills { display: flex; background: #f4f0fa; border-radius: 8px; padding: 3px; gap: 2px; } .abcode-dbe-format-pill { padding: 6px 16px; border-radius: 6px; border: none; background: transparent; font-size: 13px; font-weight: 500; color: #646970; cursor: pointer; transition: all 0.15s; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; letter-spacing: -0.3px; } .abcode-dbe-format-pill:hover { color: #0f0d25; } .abcode-dbe-format-pill.active { background: #0f0d25; color: #fff; box-shadow: 0 1px 3px rgba(15,13,37,0.2); } .abcode-dbe-format-pill.disabled, .abcode-dbe-format-pill:disabled { opacity: 0.35; cursor: not-allowed; } .abcode-dbe-format-hint { font-size: 12px; color: #646970; margin-left: auto; } .abcode-dbe-copied { position: fixed; bottom: 24px; right: 24px; background: #0f0d25; color: #fff; padding: 10px 18px; border-radius: 6px; font-size: 13px; opacity: 0; transform: translateY(8px); transition: opacity 0.2s, transform 0.2s; z-index: 99999; pointer-events: none; } .abcode-dbe-copied.show { opacity: 1; transform: translateY(0); } (function() { var downloadUrl = abcodeDbeConfig.downloadUrl; var downloadNonce = abcodeDbeConfig.downloadNonce; var ajaxUrl = abcodeDbeConfig.ajaxUrl; var nonce = abcodeDbeConfig.nonce; var i18n = abcodeDbeConfig.i18n; // Table names come straight from the database and land in innerHTML, // so they are escaped before being interpolated into markup. function esc(str) { return String(str).replace(/[&<>"']/g, function(c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); } function sprintf(fmt) { var args = Array.prototype.slice.call(arguments, 1), i = 0; return fmt.replace(/%(\d\$)?[sd]/g, function(m, pos) { var idx = pos ? parseInt(pos) - 1 : i++; return args[idx] !== undefined ? args[idx] : m; }); } function buildDownloadUrl(exportId, gzip) { var url = downloadUrl + '&_wpnonce=' + encodeURIComponent(downloadNonce) + '&export_id=' + encodeURIComponent(exportId); if (gzip) url += '&gzip=1'; return url; } function triggerDownload(url) { var iframe = document.getElementById('abcode-dbe-dl-iframe'); if (!iframe) { iframe = document.createElement('iframe'); iframe.id = 'abcode-dbe-dl-iframe'; iframe.style.display = 'none'; document.body.appendChild(iframe); } iframe.src = url; } var selectedFormat = 'sql'; var formatHints = { 'sql': i18n.standardSql, 'sql.gz': i18n.compressedSmaller }; var currentExportId = null; var btn = document.getElementById('abcode-dbe-btn'); var btnText = document.getElementById('abcode-dbe-btn-text'); var progressWrap = document.getElementById('abcode-dbe-progress'); var bar = document.getElementById('abcode-dbe-bar'); var statusText = document.getElementById('abcode-dbe-status-text'); var statusPct = document.getElementById('abcode-dbe-status-pct'); var successMsg = document.getElementById('abcode-dbe-success'); var downloadArea = document.getElementById('abcode-dbe-download-area'); var tableData = []; function formatBytes(bytes) { if (bytes === 0) return '0 B'; var k = 1024; var sizes = ['B', 'KB', 'MB', 'GB']; var i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } function formatNumber(n) { return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); } function timeAgo(timestamp) { var seconds = Math.floor(Date.now() / 1000) - timestamp; if (seconds < 60) return i18n.justNow; var minutes = Math.floor(seconds / 60); if (minutes < 60) return sprintf(minutes !== 1 ? i18n.minutesAgo : i18n.minuteAgo, minutes); var hours = Math.floor(minutes / 60); if (hours < 24) return sprintf(hours !== 1 ? i18n.hoursAgo : i18n.hourAgo, hours); var days = Math.floor(hours / 24); return sprintf(days !== 1 ? i18n.daysAgo : i18n.dayAgo, days); } function showToast(msg) { var toast = document.getElementById('abcode-dbe-copied'); toast.textContent = msg; toast.classList.add('show'); setTimeout(function() { toast.classList.remove('show'); }, 2000); } function postAjax(action, data) { var formData = new FormData(); formData.append('action', action); formData.append('_nonce', nonce); for (var key in data) { formData.append(key, data[key]); } return fetch(ajaxUrl, { method: 'POST', credentials: 'same-origin', body: formData, }).then(function(r) { return r.json(); }); } function showError(msg) { statusText.textContent = sprintf(i18n.error, msg); bar.style.background = '#d63638'; bar.style.width = '100%'; statusPct.textContent = ''; btn.disabled = false; btnText.textContent = i18n.retryExport; } function findTableIndex(name) { for (var i = 0; i < tableData.length; i++) { if (tableData[i].name === name) return i; } return -1; } // Load DB info on page load fetch(ajaxUrl + '?action=abcode_dbe_info&_nonce=' + encodeURIComponent(nonce), { credentials: 'same-origin' }) .then(function(r) { return r.json(); }) .then(function(resp) { if (!resp.success) return; var d = resp.data; document.getElementById('abcode-dbe-db-name').textContent = d.db_name; document.getElementById('abcode-dbe-db-name').classList.remove('loading'); document.getElementById('abcode-dbe-table-count').textContent = d.tables.length; document.getElementById('abcode-dbe-table-count').classList.remove('loading'); document.getElementById('abcode-dbe-db-size').textContent = formatBytes(d.db_size); document.getElementById('abcode-dbe-db-size').classList.remove('loading'); tableData = d.tables; var rowsContainer = document.getElementById('abcode-dbe-table-rows'); rowsContainer.innerHTML = ''; // Show column header var tableHead = document.getElementById('abcode-dbe-table-head'); tableHead.style.display = ''; var checkAll = document.getElementById('abcode-dbe-check-all'); d.tables.forEach(function(t, idx) { var row = document.createElement('div'); row.className = 'abcode-dbe-table-row'; row.id = 'abcode-dbe-trow-' + idx; row.setAttribute('data-table-name', t.name.toLowerCase()); var safeName = esc(t.name); row.innerHTML = '' + '' + safeName + '' + '' + esc(formatNumber(t.rows)) + '' + '' + '' + '' + '' + '' + ''; rowsContainer.appendChild(row); }); // Select all / none toggle checkAll.addEventListener('change', function() { var checked = this.checked; document.querySelectorAll('.tbl-check').forEach(function(cb) { cb.checked = checked; }); updateBtnState(); }); // Update select-all checkbox and button when individual checkboxes change rowsContainer.addEventListener('change', function(e) { if (e.target.classList.contains('tbl-check')) { var allBoxes = document.querySelectorAll('.tbl-check'); var checkedBoxes = document.querySelectorAll('.tbl-check:checked'); checkAll.checked = checkedBoxes.length === allBoxes.length; checkAll.indeterminate = checkedBoxes.length > 0 && checkedBoxes.length < allBoxes.length; updateBtnState(); } }); function formatBtnLabel(count) { var tablesLabel = sprintf(count !== 1 ? i18n.tablesPlural : i18n.tablesSingular, count); return sprintf(i18n.exportBtn, tablesLabel); } function updateBtnState() { var checkedCount = document.querySelectorAll('.tbl-check:checked').length; if (checkedCount === 0) { btn.disabled = true; btnText.textContent = i18n.selectTables; } else { btn.disabled = false; btnText.textContent = formatBtnLabel(checkedCount); } } function updateBtnText() { // No longer needed: format doesn't affect main button } // Individual table export buttons rowsContainer.querySelectorAll('.abcode-dbe-table-dl').forEach(function(dlBtn) { dlBtn.addEventListener('click', function() { var tableName = this.getAttribute('data-table'); var thisBtn = this; var rowIdx = findTableIndex(tableName); thisBtn.disabled = true; thisBtn.textContent = '...'; if (rowIdx !== -1) setRowState(rowIdx, 'exporting'); postAjax('abcode_dbe_single_table', { table: tableName }).then(function(resp) { if (rowIdx !== -1) setRowState(rowIdx, null); if (!resp.success) { thisBtn.textContent = i18n.failed; setTimeout(function() { thisBtn.textContent = 'SQL'; thisBtn.disabled = false; }, 2000); return; } thisBtn.textContent = 'SQL'; thisBtn.disabled = false; var dlUrl = buildDownloadUrl(resp.data.export_id, selectedFormat === 'sql.gz'); triggerDownload(dlUrl); }).catch(function() { if (rowIdx !== -1) setRowState(rowIdx, null); thisBtn.textContent = i18n.failed; setTimeout(function() { thisBtn.textContent = 'SQL'; thisBtn.disabled = false; }, 2000); }); }); }); // Show search bar document.getElementById('abcode-dbe-search').classList.add('active'); // Format pill selector var formatPills = document.querySelectorAll('.abcode-dbe-format-pill'); var formatHint = document.getElementById('abcode-dbe-format-hint'); var dlBtn = document.getElementById('abcode-dbe-dl-link'); formatPills.forEach(function(pill) { pill.addEventListener('click', function() { formatPills.forEach(function(p) { p.classList.remove('active'); }); this.classList.add('active'); selectedFormat = this.getAttribute('data-format'); formatHint.textContent = formatHints[selectedFormat]; dlBtn.textContent = sprintf(i18n.downloadFormat, selectedFormat); }); }); // Download button in success area dlBtn.addEventListener('click', function() { if (!currentExportId) return; var url = buildDownloadUrl(currentExportId, selectedFormat === 'sql.gz'); triggerDownload(url); }); // Search/filter tables var searchInput = document.getElementById('abcode-dbe-search-input'); searchInput.addEventListener('input', function() { var q = this.value.toLowerCase(); var rows = rowsContainer.querySelectorAll('.abcode-dbe-table-row'); rows.forEach(function(row) { var name = row.getAttribute('data-table-name') || ''; row.style.display = (!q || name.indexOf(q) !== -1) ? '' : 'none'; }); }); // Schema copy buttons rowsContainer.querySelectorAll('.abcode-dbe-table-schema').forEach(function(schemaBtn) { schemaBtn.addEventListener('click', function() { var tableName = this.getAttribute('data-table'); var thisBtn = this; thisBtn.disabled = true; thisBtn.textContent = '...'; postAjax('abcode_dbe_schema', { table: tableName }).then(function(resp) { thisBtn.textContent = 'Schema'; thisBtn.disabled = false; if (!resp.success) return; navigator.clipboard.writeText(resp.data.sql).then(function() { showToast(i18n.schemaCopied); }); }).catch(function() { thisBtn.textContent = 'Schema'; thisBtn.disabled = false; }); }); }); // CSV export buttons rowsContainer.querySelectorAll('.abcode-dbe-table-csv').forEach(function(csvBtn) { csvBtn.addEventListener('click', function() { var tableName = this.getAttribute('data-table'); var thisBtn = this; thisBtn.disabled = true; thisBtn.textContent = '...'; postAjax('abcode_dbe_csv', { table: tableName }).then(function(resp) { thisBtn.textContent = 'CSV'; thisBtn.disabled = false; if (!resp.success) return; var dlUrl = buildDownloadUrl(resp.data.export_id); triggerDownload(dlUrl); }).catch(function() { thisBtn.textContent = 'CSV'; thisBtn.disabled = false; }); }); }); // Show last export timestamp if (d.last_export > 0) { var lastEl = document.getElementById('abcode-dbe-last-export'); var textEl = document.getElementById('abcode-dbe-last-export-text'); textEl.textContent = sprintf(i18n.lastExported, timeAgo(d.last_export)); lastEl.classList.add('active'); } btn.disabled = false; btnText.textContent = formatBtnLabel(d.tables.length); }); function setRowState(idx, state) { var row = document.getElementById('abcode-dbe-trow-' + idx); if (!row) return; row.classList.remove('exporting', 'exported'); if (state) row.classList.add(state); } function resetAllRows() { for (var i = 0; i < tableData.length; i++) { setRowState(i, null); } } function setAllTableBtnsDisabled(disabled) { document.querySelectorAll('.abcode-dbe-table-dl, .abcode-dbe-table-csv, .abcode-dbe-table-schema').forEach(function(b) { b.disabled = disabled; }); } function getSelectedTables() { var selected = []; document.querySelectorAll('.tbl-check:checked').forEach(function(cb) { var idx = parseInt(cb.getAttribute('data-idx'), 10); selected.push({ idx: idx, name: tableData[idx].name }); }); return selected; } btn.addEventListener('click', function() { // If export is complete, restart by reloading if (currentExportId) { window.location.reload(); return; } var selected = getSelectedTables(); if (selected.length === 0) return; btn.disabled = true; btnText.textContent = i18n.exporting; document.getElementById('abcode-dbe-table-list').classList.add('locked'); progressWrap.classList.add('active'); successMsg.classList.remove('active'); downloadArea.classList.remove('active'); bar.style.background = ''; bar.style.width = '0%'; statusPct.textContent = '0%'; statusText.textContent = i18n.startingExport; resetAllRows(); setAllTableBtnsDisabled(true); var total = selected.length; // Step 1: Start export postAjax('abcode_dbe_start', {}).then(function(resp) { if (!resp.success) { showError(resp.data || i18n.failedStartExport); setAllTableBtnsDisabled(false); return; } var exportId = resp.data.export_id; var current = 0; // Step 2: Export selected tables one by one function exportNextTable() { if (current >= total) { // Step 3: Finish export statusText.textContent = i18n.finalisingExport; bar.style.width = '98%'; statusPct.textContent = '98%'; postAjax('abcode_dbe_finish', { export_id: exportId }).then(function(resp) { if (!resp.success) { showError(resp.data || i18n.finalisingExport); setAllTableBtnsDisabled(false); return; } bar.style.width = '100%'; statusPct.textContent = '100%'; statusText.textContent = i18n.done; // Update last export display var lastEl = document.getElementById('abcode-dbe-last-export'); document.getElementById('abcode-dbe-last-export-text').textContent = sprintf(i18n.lastExported, i18n.justNow); lastEl.classList.add('active'); // Store export ID for download currentExportId = exportId; // Show success bar and download area successMsg.classList.add('active'); downloadArea.classList.add('active'); progressWrap.classList.remove('active'); // Lock table list document.getElementById('abcode-dbe-table-list').classList.add('locked'); // Button becomes "Restart Export", which reloads the page btn.disabled = false; btnText.textContent = i18n.restartExport; }).catch(function() { showError(i18n.networkErrorFinalise); setAllTableBtnsDisabled(false); }); return; } var tbl = selected[current]; setRowState(tbl.idx, 'exporting'); statusText.textContent = sprintf(i18n.exportingTable, current + 1, total, tbl.name); var pct = Math.round(((current + 1) / total) * 95); bar.style.width = pct + '%'; statusPct.textContent = pct + '%'; postAjax('abcode_dbe_table', { export_id: exportId, table: tbl.name, }).then(function(resp) { if (!resp.success) { showError(resp.data || sprintf(i18n.failedExportTable, tbl.name)); setAllTableBtnsDisabled(false); return; } setRowState(tbl.idx, 'exported'); current++; exportNextTable(); }).catch(function() { showError(sprintf(i18n.networkErrorTable, tbl.name)); setAllTableBtnsDisabled(false); }); } exportNextTable(); }).catch(function() { showError(i18n.networkErrorStart); setAllTableBtnsDisabled(false); }); }); // Keyboard shortcut: Ctrl+Enter or Cmd+Enter to start export document.addEventListener('keydown', function(e) { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && !btn.disabled) { e.preventDefault(); btn.click(); } }); })(); ' . esc_html__('Export Database', 'abcode-database-exporter') . ''); return $links; }); // ─── AJAX: get table list + row counts for progress tracking ────────────────── add_action('wp_ajax_abcode_dbe_info', function () { abcode_dbe_verify_request(); global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- SHOW TABLES is uncacheable metadata. $tables = $wpdb->get_col("SHOW TABLES"); $info = []; $total_rows = 0; // Use information_schema for fast row counts + DB size in a single query. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Live DB metadata, cannot be cached. $table_stats = $wpdb->get_results( $wpdb->prepare( "SELECT table_name, table_rows, data_length + index_length AS size FROM information_schema.tables WHERE table_schema = %s", DB_NAME ), OBJECT_K ); $db_size = 0; foreach ($tables as $table) { $rows = isset($table_stats[$table]) ? (int) $table_stats[$table]->table_rows : 0; $info[] = ['name' => $table, 'rows' => $rows]; $total_rows += $rows; if (isset($table_stats[$table])) { $db_size += (int) $table_stats[$table]->size; } } $last_export = get_option('abcode_dbe_last_export', 0); wp_send_json_success([ 'db_name' => DB_NAME, 'tables' => $info, 'total_rows' => $total_rows, 'db_size' => (int) $db_size, 'last_export' => (int) $last_export, ]); }); // ─── Helpers ───────────────────────────────────────────────────────────────── /** * Returns the plugin's temp directory, creating and hardening it if needed. * * @return string Absolute path to the temp directory (no trailing slash). */ function abcode_dbe_get_temp_dir() { $dir = wp_upload_dir(); $tmp = trailingslashit($dir['basedir']) . 'abcode-dbe-tmp'; if (!is_dir($tmp)) { wp_mkdir_p($tmp); // Block direct HTTP access on both Apache 2.2 and 2.4. $htaccess = "\n" . "Require all denied\n" . "\n" . "\n" . "Order allow,deny\n" . "Deny from all\n" . "\n"; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Creating security files in temp directory. file_put_contents($tmp . '/.htaccess', $htaccess); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents file_put_contents($tmp . '/index.php', " $filepath, 'user' => get_current_user_id()], HOUR_IN_SECONDS ); } /** * Resolves an export ID to its file path. * * Rejects sessions belonging to another user and any path that has escaped the * plugin's temp directory, so a poisoned transient cannot be used to read * arbitrary files from disk. * * @param string $export_id Export identifier from the request. * @return string|false Absolute file path, or false if invalid. */ function abcode_dbe_get_export_path($export_id) { $session = get_transient('abcode_dbe_' . $export_id); if (!is_array($session) || empty($session['file'])) { return false; } $owner = isset($session['user']) ? (int) $session['user'] : 0; if (!$owner || $owner !== get_current_user_id()) { return false; } // wp_normalize_path() keeps the comparison working on Windows, where // realpath() returns backslashes but wp_upload_dir() does not. $real = realpath($session['file']); $base = realpath(abcode_dbe_get_temp_dir()); if (!$real || !$base) { return false; } $real = wp_normalize_path($real); $base = trailingslashit(wp_normalize_path($base)); if (strpos($real, $base) !== 0) { return false; } return $real; } /** * Returns a table name only if it exists in the current database. * * SHOW TABLES is the source of truth, because table names cannot be passed through * $wpdb->prepare() as identifiers, so an allowlist check is the safe route. * * @param string $table Table name from the request. * @return string|false The validated table name, or false. */ function abcode_dbe_validate_table($table) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- SHOW TABLES is uncacheable metadata. $tables = $wpdb->get_col('SHOW TABLES'); return in_array($table, (array) $tables, true) ? $table : false; } function abcode_dbe_cleanup_old_files() { $dir = abcode_dbe_get_temp_dir(); $files = array_merge( glob($dir . '/*.sql') ?: [], glob($dir . '/*.csv') ?: [], glob($dir . '/*.sql.gz') ?: [], glob($dir . '/*.gz') ?: [] ); if (!$files) return; $cutoff = time() - 3600; // 1 hour foreach ($files as $file) { if (filemtime($file) < $cutoff) { wp_delete_file($file); } } } /** * Build the SQL header as a string. * * @param string $server_version MySQL server version string. * @return string The SQL header content. */ function abcode_dbe_build_sql_header($server_version) { $lines = []; $lines[] = "-- phpMyAdmin SQL Dump"; $lines[] = "-- https://www.phpmyadmin.net/"; $lines[] = "--"; $lines[] = "-- Host: " . DB_HOST; $lines[] = "-- Generation Time: " . gmdate('M d, Y \a\t h:i A') . " UTC"; $lines[] = "-- Server version: " . $server_version; $lines[] = "-- PHP Version: " . phpversion(); $lines[] = ""; $lines[] = "SET SQL_MODE = \"NO_AUTO_VALUE_ON_ZERO\";"; $lines[] = "START TRANSACTION;"; $lines[] = "SET time_zone = \"+00:00\";"; $lines[] = ""; $lines[] = ""; $lines[] = "/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;"; $lines[] = "/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;"; $lines[] = "/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;"; $lines[] = "/*!40101 SET NAMES utf8mb4 */;"; $lines[] = ""; $lines[] = "--"; $lines[] = "-- Database: `" . DB_NAME . "`"; $lines[] = "--"; $lines[] = ""; $lines[] = "-- --------------------------------------------------------"; $lines[] = ""; return implode("\n", $lines) . "\n"; } /** * Build the SQL footer as a string. * * @return string The SQL footer content. */ function abcode_dbe_build_sql_footer() { $lines = []; $lines[] = "COMMIT;"; $lines[] = ""; $lines[] = "/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;"; $lines[] = "/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;"; $lines[] = "/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;"; return implode("\n", $lines) . "\n"; } /** * Export a single table's structure and data, appending SQL to a file. * * Uses $wpdb with chunked SELECT queries (1000 rows at a time) to keep memory * usage constant regardless of table size. Uses file_put_contents with FILE_APPEND * for writing chunks to disk. * * @param string $filepath Path to the export file. * @param string $table Table name (already validated against SHOW TABLES). */ function abcode_dbe_export_table($filepath, $table) { global $wpdb; $safe_table = esc_sql($table); $sql = ''; $sql .= "--\n"; $sql .= "-- Table structure for table `{$table}`\n"; $sql .= "--\n\n"; $sql .= "DROP TABLE IF EXISTS `{$table}`;\n"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Read-only SHOW CREATE TABLE; identifier cannot use %s. $create_row = $wpdb->get_row("SHOW CREATE TABLE `{$safe_table}`", ARRAY_N); $sql .= $create_row[1] . ";\n\n"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $row_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM `{$safe_table}`"); if ($row_count === 0) { $sql .= "-- --------------------------------------------------------\n\n"; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Appending SQL chunk; WP_Filesystem has no append mode. file_put_contents($filepath, $sql, FILE_APPEND | LOCK_EX); return; } $sql .= "--\n"; $sql .= "-- Dumping data for table `{$table}`\n"; $sql .= "--\n\n"; // Get column info. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $fields = $wpdb->get_results("SHOW COLUMNS FROM `{$safe_table}`", ARRAY_A); $columns = []; $is_numeric = []; $is_binary = []; foreach ($fields as $field) { $columns[] = '`' . $field['Field'] . '`'; $t = strtolower($field['Type']); $is_numeric[] = ( strpos($t, 'int') !== false || strpos($t, 'decimal') !== false || strpos($t, 'float') !== false || strpos($t, 'double') !== false ); // Binary payloads must be dumped as hex literals, because quoting raw bytes // produces a file that is not valid UTF-8 and re-imports corrupted. $is_binary[] = ( strpos($t, 'blob') !== false || strpos($t, 'binary') !== false ); } $col_list = implode(', ', $columns); $insert_prefix = "INSERT INTO `{$table}` ({$col_list}) VALUES\n"; // Write header chunk before starting data loop. // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents file_put_contents($filepath, $sql, FILE_APPEND | LOCK_EX); $sql = ''; // Process data in chunks of 1000 rows. $chunk_size = 1000; $offset = 0; $rows_in_batch = 0; while ($offset < $row_count) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Chunked read of a user-chosen table; there is nothing to cache. $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- A table name is an identifier, so it cannot be a %s placeholder. It is validated against SHOW TABLES by abcode_dbe_validate_table() and escaped with esc_sql(); the offset and limit are prepared. $wpdb->prepare("SELECT * FROM `{$safe_table}` LIMIT %d, %d", $offset, $chunk_size), ARRAY_N ); if (empty($rows)) { break; } foreach ($rows as $row) { if ($rows_in_batch === 0) { $sql .= $insert_prefix; } $values = []; foreach ($row as $i => $val) { if ($val === null) { $values[] = 'NULL'; } elseif ($is_numeric[$i] && is_numeric($val)) { $values[] = $val; } elseif ($is_binary[$i]) { $values[] = '' === $val ? "''" : '0x' . bin2hex($val); } else { $values[] = "'" . esc_sql($val) . "'"; } } if ($rows_in_batch > 0) { $sql .= ",\n"; } $sql .= '(' . implode(', ', $values) . ')'; $rows_in_batch++; if ($rows_in_batch >= 100) { $sql .= ";\n"; $rows_in_batch = 0; } } // Flush this chunk to disk to keep memory low. // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents file_put_contents($filepath, $sql, FILE_APPEND | LOCK_EX); $sql = ''; $offset += $chunk_size; } if ($rows_in_batch > 0) { $sql .= ";\n"; } $sql .= "\n"; $sql .= "-- --------------------------------------------------------\n\n"; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents file_put_contents($filepath, $sql, FILE_APPEND | LOCK_EX); } // ─── Chunked export via AJAX ───────────────────────────────────────────────── // Step 1: Start export. Write the SQL header to a temp file, return export ID add_action('wp_ajax_abcode_dbe_start', function () { abcode_dbe_verify_request(); abcode_dbe_cleanup_old_files(); $export_id = wp_generate_password(16, false); $tmp_dir = abcode_dbe_get_temp_dir(); $filepath = $tmp_dir . '/' . $export_id . '.sql'; global $wpdb; $server_version = $wpdb->db_version(); $header = abcode_dbe_build_sql_header($server_version); global $wp_filesystem; if (!WP_Filesystem()) { wp_send_json_error(__('Cannot initialise filesystem', 'abcode-database-exporter')); } if (!$wp_filesystem->put_contents($filepath, $header, FS_CHMOD_FILE)) { wp_send_json_error(__('Cannot create temp file', 'abcode-database-exporter')); } // Record the session so later steps can validate the export ID abcode_dbe_store_export($export_id, $filepath); wp_send_json_success(['export_id' => $export_id]); }); // Step 2: Export a single table. Append it to the temp file add_action('wp_ajax_abcode_dbe_table', function () { abcode_dbe_verify_request(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing -- Nonce and capability verified by abcode_dbe_verify_request() above. $export_id = isset($_POST['export_id']) ? sanitize_text_field(wp_unslash($_POST['export_id'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing -- Nonce and capability verified by abcode_dbe_verify_request() above. $table = isset($_POST['table']) ? sanitize_text_field(wp_unslash($_POST['table'])) : ''; if (!$export_id || !$table) { wp_send_json_error(__('Missing parameters', 'abcode-database-exporter')); } $filepath = abcode_dbe_get_export_path($export_id); if (!$filepath) { wp_send_json_error(__('Export session expired or invalid', 'abcode-database-exporter')); } if (!abcode_dbe_validate_table($table)) { wp_send_json_error(__('Invalid table name', 'abcode-database-exporter')); } // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Large table exports may exceed default limits. @set_time_limit(300); abcode_dbe_export_table($filepath, $table); // Refresh the session TTL abcode_dbe_store_export($export_id, $filepath); wp_send_json_success(['table' => $table]); }); // Step 3: Finish export. Write the SQL footer add_action('wp_ajax_abcode_dbe_finish', function () { abcode_dbe_verify_request(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing -- Nonce and capability verified by abcode_dbe_verify_request() above. $export_id = isset($_POST['export_id']) ? sanitize_text_field(wp_unslash($_POST['export_id'])) : ''; if (!$export_id) { wp_send_json_error(__('Missing export ID', 'abcode-database-exporter')); } $filepath = abcode_dbe_get_export_path($export_id); if (!$filepath) { wp_send_json_error(__('Export session expired or invalid', 'abcode-database-exporter')); } // Append the small SQL footer to the export file. // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Appending footer; WP_Filesystem has no append mode. file_put_contents($filepath, abcode_dbe_build_sql_footer(), FILE_APPEND | LOCK_EX); $filesize = filesize($filepath); // Refresh the session TTL so the download handler can find the file. abcode_dbe_store_export($export_id, $filepath); update_option('abcode_dbe_last_export', time(), false); wp_send_json_success([ 'export_id' => $export_id, 'filesize' => $filesize, ]); }); // Step 4: Serve the completed file as a download add_action('admin_post_abcode_dbe_download', 'abcode_dbe_download'); function abcode_dbe_download() { abcode_dbe_verify_request(false); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing -- Nonce and capability verified by abcode_dbe_verify_request() above. $export_id = isset($_GET['export_id']) ? sanitize_text_field(wp_unslash($_GET['export_id'])) : ''; if (!$export_id) { wp_die(esc_html__('Missing export ID', 'abcode-database-exporter')); } $filepath = abcode_dbe_get_export_path($export_id); if (!$filepath) { delete_transient('abcode_dbe_' . $export_id); wp_die(esc_html__('Export session expired. Please run the export again.', 'abcode-database-exporter')); } $is_csv = substr($filepath, -4) === '.csv'; $ext = $is_csv ? '.csv' : '.sql'; // sanitize_file_name() keeps quotes and control characters out of the // Content-Disposition header, which DB_NAME is otherwise free to contain. $filename = sanitize_file_name(DB_NAME . '-' . gmdate('Y-m-d-His') . $ext); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing -- Nonce and capability verified by abcode_dbe_verify_request() above. $use_gzip = !empty($_GET['gzip']) && function_exists('gzopen') && !$is_csv; while (ob_get_level()) { ob_end_clean(); } // phpcs:disable WordPress.WP.AlternativeFunctions -- Streaming file download; WP_Filesystem cannot stream to browser. if ($use_gzip) { // Stream gzip to avoid loading the entire file into memory. $gz_path = $filepath . '.gz'; $gz = gzopen($gz_path, 'wb9'); $in = $gz ? fopen($filepath, 'rb') : false; if (!$gz || !$in) { if ($gz) { gzclose($gz); } wp_die(esc_html__('Could not compress the export file.', 'abcode-database-exporter')); } while (!feof($in)) { gzwrite($gz, fread($in, 524288)); // 512 KB chunks } fclose($in); gzclose($gz); $filename .= '.gz'; header('Content-Type: application/gzip'); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Content-Length: ' . filesize($gz_path)); header('X-Content-Type-Options: nosniff'); header('Cache-Control: no-cache, no-store, must-revalidate'); header('Pragma: no-cache'); header('Expires: 0'); readfile($gz_path); // The .gz is a throwaway rebuilt on every download, so don't leave copies // of the database lying around in the uploads directory. wp_delete_file($gz_path); } else { $mime = $is_csv ? 'text/csv' : 'application/octet-stream'; header('Content-Type: ' . $mime); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Content-Length: ' . filesize($filepath)); header('X-Content-Type-Options: nosniff'); header('Cache-Control: no-cache, no-store, must-revalidate'); header('Pragma: no-cache'); header('Expires: 0'); readfile($filepath); } // phpcs:enable WordPress.WP.AlternativeFunctions // Keep the file and session alive so the user can re-download. // The hourly cleanup (abcode_dbe_cleanup_old_files) handles removal. abcode_dbe_store_export($export_id, $filepath); exit; } // Step 5: Export a single table as a standalone .sql download add_action('wp_ajax_abcode_dbe_single_table', function () { abcode_dbe_verify_request(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing -- Nonce and capability verified by abcode_dbe_verify_request() above. $table = isset($_POST['table']) ? sanitize_text_field(wp_unslash($_POST['table'])) : ''; if (!$table) { wp_send_json_error(__('Missing table name', 'abcode-database-exporter')); } global $wpdb; if (!abcode_dbe_validate_table($table)) { wp_send_json_error(__('Invalid table name', 'abcode-database-exporter')); } // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Large table exports may exceed default limits. @set_time_limit(300); abcode_dbe_cleanup_old_files(); $export_id = wp_generate_password(16, false); $tmp_dir = abcode_dbe_get_temp_dir(); $filepath = $tmp_dir . '/' . $export_id . '.sql'; $server_version = $wpdb->db_version(); $header = abcode_dbe_build_sql_header($server_version); global $wp_filesystem; if (!WP_Filesystem()) { wp_send_json_error(__('Cannot initialise filesystem', 'abcode-database-exporter')); } if (!$wp_filesystem->put_contents($filepath, $header, FS_CHMOD_FILE)) { wp_send_json_error(__('Cannot create temp file', 'abcode-database-exporter')); } abcode_dbe_export_table($filepath, $table); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Appending footer; WP_Filesystem has no append mode. file_put_contents($filepath, abcode_dbe_build_sql_footer(), FILE_APPEND | LOCK_EX); abcode_dbe_store_export($export_id, $filepath); update_option('abcode_dbe_last_export', time(), false); wp_send_json_success(['export_id' => $export_id]); }); // ─── AJAX: get CREATE TABLE statement for clipboard copy ───────────────────── add_action('wp_ajax_abcode_dbe_schema', function () { abcode_dbe_verify_request(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing -- Nonce and capability verified by abcode_dbe_verify_request() above. $table = isset($_POST['table']) ? sanitize_text_field(wp_unslash($_POST['table'])) : ''; if (!$table) { wp_send_json_error(__('Missing table name', 'abcode-database-exporter')); } global $wpdb; if (!abcode_dbe_validate_table($table)) { wp_send_json_error(__('Invalid table name', 'abcode-database-exporter')); } $safe_table = esc_sql($table); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name validated against SHOW TABLES above; identifiers cannot use %s placeholders. $row = $wpdb->get_row( "SHOW CREATE TABLE `{$safe_table}`", ARRAY_N ); if (!$row) { wp_send_json_error(__('Could not retrieve table schema', 'abcode-database-exporter')); } $sql = $row[1] . ';'; wp_send_json_success(['sql' => $sql]); }); // ─── AJAX: export single table as CSV ──────────────────────────────────────── add_action('wp_ajax_abcode_dbe_csv', function () { abcode_dbe_verify_request(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing -- Nonce and capability verified by abcode_dbe_verify_request() above. $table = isset($_POST['table']) ? sanitize_text_field(wp_unslash($_POST['table'])) : ''; if (!$table) { wp_send_json_error(__('Missing table name', 'abcode-database-exporter')); } global $wpdb; if (!abcode_dbe_validate_table($table)) { wp_send_json_error(__('Invalid table name', 'abcode-database-exporter')); } // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Large table exports may exceed default limits. @set_time_limit(300); abcode_dbe_cleanup_old_files(); $export_id = wp_generate_password(16, false); $tmp_dir = abcode_dbe_get_temp_dir(); $filepath = $tmp_dir . '/' . $export_id . '.csv'; $safe_table = esc_sql($table); // Get column headers. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $fields = $wpdb->get_results("SHOW COLUMNS FROM `{$safe_table}`", ARRAY_A); $columns = []; foreach ($fields as $field) { $columns[] = $field['Field']; } // Build CSV content in chunks. // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- fputcsv requires a file handle. $out = fopen($filepath, 'w'); if (!$out) { wp_send_json_error(__('Cannot create temp file', 'abcode-database-exporter')); } // The explicit empty escape character gives RFC 4180 output and avoids the // PHP 8.4 deprecation of the default backslash escaping. fputcsv($out, $columns, ',', '"', ''); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $row_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM `{$safe_table}`"); $chunk_size = 1000; $offset = 0; while ($offset < $row_count) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Chunked read of a user-chosen table; there is nothing to cache. $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- A table name is an identifier, so it cannot be a %s placeholder. It is validated against SHOW TABLES by abcode_dbe_validate_table() and escaped with esc_sql(); the offset and limit are prepared. $wpdb->prepare("SELECT * FROM `{$safe_table}` LIMIT %d, %d", $offset, $chunk_size), ARRAY_N ); if (empty($rows)) { break; } foreach ($rows as $row) { fputcsv($out, $row, ',', '"', ''); } $offset += $chunk_size; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose fclose($out); abcode_dbe_store_export($export_id, $filepath); wp_send_json_success(['export_id' => $export_id]); }); // ─── Admin page ─────────────────────────────────────────────────────────────── function abcode_dbe_admin_page() { if (!current_user_can(abcode_dbe_capability())) { wp_die(esc_html__('Unauthorized', 'abcode-database-exporter'), 403); } $has_gzip = function_exists('gzopen'); ?>

›
By Ash Brentnall | ABCode
—
—
—
0%