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.

Nonce verification Capability checks admin-ajax Signed downloads Protected temp files

How export security is layered

Exporting a table is a two-request handshake, and each request is independently secured:

  1. Generate — the browser POSTs to the tc_export_data AJAX action. This is where the nonce and capability checks live. On success the server writes a temporary file and returns a download URL plus an export_id.
  2. Download — the browser follows the returned URL, which hits the tc_download_export action. 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

GuardAction / argumentBehavior on failure
Noncewp_verify_nonce($nonce, 'tc_export_nonce')Returns "Invalid nonce" JSON error
Capabilitycurrent_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:

⚠️

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.

ActionNonce actionNonce sourceReject with
tc_export_datatc_export_nonce$_POST['nonce']wp_send_json_error()
tc_download_exporttc_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:

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.

AttributeStatusDefaultEffect
exportOptionalfalseRenders the export dropdown and wires it to the tc_export_data action
sourceRequiredData 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:

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

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.