[[header...], [row...], ...]]. * CSV gets the first sheet. Other formats come from add-ons through the gatecraft_send_table filter. */ function send_table(string $filename, string $format, array $sheets): never { $safe = preg_replace('/[^A-Za-z0-9_-]+/', '-', $filename); header('Cache-Control: no-store'); header('X-Content-Type-Options: nosniff'); if ($format !== 'csv' && apply_filters('gatecraft_send_table', false, $safe, $format, $sheets)) exit; header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="' . $safe . '.csv"'); $out = fopen('php://output', 'w'); fwrite($out, "\xEF\xBB\xBF"); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- streaming a CSV download to php://output // UTF-8 BOM so Excel reads Japanese and accents correctly foreach (reset($sheets) as $row) fputcsv($out, array_map(fn($v) => is_int($v) || is_float($v) ? $v : csv_safe($v), $row)); exit; }