Export Permissions & Security
TableCrafter gates every table export behind a WordPress nonce and a capability check before any CSV, XLSX, or PDF file is generated, then serves the result through a separate, single-use signed download URL. This page documents exactly which checks run, in what order, and how to reason about who can export.
How export security is layered
Exporting a table is a two-request handshake, and each request is independently secured:
- Generate — the browser POSTs to the
tc_export_dataAJAX action. This is where the nonce and capability checks live. On success the server writes a temporary file and returns a download URL plus anexport_id. - Download — the browser follows the returned URL, which hits the
tc_download_exportaction. This request carries its own dedicated nonce and serves the file exactly once before deleting it.
Both actions are registered in the main plugin file (tablecrafter.php) and routed through WordPress core's admin-ajax.php endpoint:
add_action('wp_ajax_tc_export_data', ...);
add_action('wp_ajax_nopriv_tc_export_data', ...);
add_action('wp_ajax_tc_download_export', ...);
add_action('wp_ajax_nopriv_tc_download_export', ...);
The nopriv hooks are registered so logged-out visitors reach the handler instead of getting a bare 0 response, but the handler itself still runs the capability check below and rejects anonymous users. The hook registration does not grant access.
The generate step: nonce, then capability
When the export request reaches ajax_export_data(), two guards run before any data is touched. Order matters: the nonce is verified first, then the user capability.
// 1. Verify the nonce (sanitized, unslashed first).
$nonce = sanitize_text_field(wp_unslash($_POST['nonce']));
if (empty($nonce) || !wp_verify_nonce($nonce, 'tc_export_nonce')) {
wp_send_json_error('Invalid nonce');
return;
}
// 2. Check the capability.
if (!current_user_can('read')) {
wp_send_json_error('Insufficient permissions');
return;
}
Both failures return a JSON error via wp_send_json_error() and stop execution, so a request that fails either check never reaches the export handler and never produces a file.
What each guard verifies
| Guard | Action / argument | Behavior on failure |
|---|---|---|
| Nonce | wp_verify_nonce($nonce, 'tc_export_nonce') | Returns "Invalid nonce" JSON error |
| Capability | current_user_can('read') | Returns "Insufficient permissions" JSON error |
Who can export: the read capability
The generate step is gated on the read capability. In a default WordPress install read is granted to every role — Subscriber, Contributor, Author, Editor, and Administrator — but is denied to logged-out visitors. The practical effect:
- Any logged-in user with a standard role can trigger an export.
- Anonymous visitors are rejected by
current_user_can('read')even though thenoprivhook routes them to the handler.
The export action does not enforce manage_options or edit_posts. If your tables are placed on pages that already require login (for example a members area), the read check is sufficient. If you embed a table with export="true" on a fully public page, be aware that any visitor who is logged in at all can export the rendered rows.
The download step: a second, dedicated nonce
A successful generate call stores the file in a five-minute transient and returns a download URL of this exact shape:
/wp-admin/admin-ajax.php?action=tc_download_export&export_id=tc_export_…&nonce=…
That URL hits ajax_download_export(), which verifies a different nonce — tc_download_nonce, read from $_GET['nonce'] — before streaming the file:
$nonce = sanitize_text_field(wp_unslash($_GET['nonce']));
if (empty($nonce) || !wp_verify_nonce($nonce, 'tc_download_nonce')) {
wp_die('Invalid nonce');
}
The download handler then validates the export_id, confirms the transient and the file still exist, sends an attachment Content-Disposition, streams the bytes with readfile(), and finally cleans up — deleting the temp file and the transient so the link cannot be replayed.
| Action | Nonce action | Nonce source | Reject with |
|---|---|---|---|
tc_export_data | tc_export_nonce | $_POST['nonce'] | wp_send_json_error() |
tc_download_export | tc_download_nonce | $_GET['nonce'] | wp_die() |
The five-minute transient lifetime plus the delete-on-download behavior means an export link is effectively single-use and short-lived. After download, or after five minutes, the link returns "Export not found or expired."
Where the export nonce comes from
The frontend never hardcodes a nonce. TableCrafter publishes tc_export_nonce through three redundant channels during register_assets() and wp_head/admin_head, so the export button always has a valid token to send:
- Localized data —
tablecrafterData.exportNonceviawp_localize_script(). - Inline global —
window.tcExportNonce, printed before the frontend script. - Meta tag —
<meta name="tc-export-nonce" content="…">, emitted byadd_export_nonce()into the page head.
The client library resolves the token in getExportNonce(), preferring a data-export-nonce attribute on the container, then the meta[name="tc-export-nonce"] tag, then the window.tcExportNonce global. The resolved value is appended to the request as the nonce field:
formData.append('action', 'tc_export_data');
formData.append('format', format); // csv | xlsx | pdf
formData.append('nonce', this.getExportNonce());
Because every nonce is generated per-session by WordPress, a token printed for one user cannot be reused by another, and tokens expire on the normal nonce lifecycle (roughly 12–24 hours).
Enabling the secured export UI on a table
Export controls only render when the table opts in. Add the export attribute to the [tablecrafter] shortcode — it is false by default:
[tablecrafter source="https://example.com/data.csv" export="true"]
That attribute is rendered onto the container as data-export="true", which the frontend reads to decide whether to build the export dropdown (the .tc-export-controls / .tc-export-dropdown UI offering CSV, Excel/XLSX, and PDF). The security guards above apply identically no matter which format the user picks.
| Attribute | Status | Default | Effect |
|---|---|---|---|
export | Optional | false | Renders the export dropdown and wires it to the tc_export_data action |
source | Required | — | Data source the table (and any server-side export) reads from |
Input handling inside the handler
Beyond authentication, the handler sanitizes every parameter it consumes, following the WordPress unslash-then-sanitize pattern:
formatviasanitize_text_field(), then constrained to the supported setcsv,xlsx,pdfby the export handler.filenameviasanitize_file_name(), so the downloaded file name can never inject a path.sourceviaesc_url_raw();templateviasanitize_text_field().- Client-supplied rows and columns (the no-source frontend contract) are decoded with
json_decode()and rejected if they are not non-empty arrays.
Generated files are written to a protected tablecrafter-exports/ directory under wp-uploads, hardened with an .htaccess (Require all denied) and a silencing index.php so the temp files cannot be fetched directly — they are only ever reachable through the nonce-checked download action.
Cleanup is automatic: each file is deleted immediately after its single download, and cleanup_old_exports() sweeps anything older than an hour. You do not need to prune the export directory by hand.
Quick security checklist
- Export generation requires a valid
tc_export_nonceand thereadcapability. - Anonymous (logged-out) requests are rejected by the capability check.
- Downloads require a separate
tc_download_nonceand are single-use within a 5-minute window. - Temp files live in a directory blocked from direct web access and are deleted after serving.
- If a public page exposes
export="true", scope it to logged-in audiences when the data is sensitive.
Next, see data-export-formats.html for the CSV/XLSX/PDF output details, and export-templates.html for shaping headers, metadata, and number formatting in the exported file.