diff --git a/internal/check-batch-selection-export.mjs b/internal/check-batch-selection-export.mjs index 171ca9adc..42ab25233 100644 --- a/internal/check-batch-selection-export.mjs +++ b/internal/check-batch-selection-export.mjs @@ -45,8 +45,9 @@ const records = [ { slug: 'q-comma', name: 'Bhil, Tadvi "Hill"', country_code: 'IND', population: 3100 }, { slug: 'q-formula', name: '=HYPERLINK("x")', country_code: 'NPL', population: 450 }, { slug: 'q-newline', name: 'Line one\nLine two', country_code: 'NPL', population: 7 }, + { slug: 'q-tab', name: '\t=cmd', country_code: 'NPL', population: 1 }, ] -const from = new Map([['agaria', ['Mahārāshtra', 'Goa']], ['q-comma', ['Goa']], ['q-formula', ['Nepal']], ['q-newline', ['Nepal']]]) +const from = new Map([['agaria', ['Mahārāshtra', 'Goa']], ['q-comma', ['Goa']], ['q-formula', ['Nepal']], ['q-newline', ['Nepal']], ['q-tab', ['Nepal']]]) const color = '#e69f00' const csv = X.selectionCsv(records, from, { color }) const text = X.selectionClipboard(selection, { color }, csv) @@ -86,6 +87,8 @@ ok(col(rows[4], 'name') === 'Line one\nLine two', `a name with a line break read 'commas, quotes and line breaks survive a real CSV parse') ok(col(rows[3], 'name') === '\'=HYPERLINK("x")', `a leading = reads back as ${JSON.stringify(col(rows[3], 'name'))}: a spreadsheet would run it`, 'a leading = + - @ is neutralised') +ok(col(rows[5], 'name') === "'\t=cmd", `a leading TAB reads back as ${JSON.stringify(col(rows[5], 'name'))}: OWASP lists it as a formula trigger`, + 'a leading TAB or CR is neutralised too (OWASP)') ok(col(rows[1], 'territories') === 'Mahārāshtra; Goa' && col(rows[1], 'color') === color && col(rows[1], 'slug') === 'agaria', `row 1 is ${JSON.stringify(rows[1])}`, 'each row carries its slug, the territories that contain it, and the colour') diff --git a/plugin-designs/my-upg-100-list/data/selection-export.js b/plugin-designs/my-upg-100-list/data/selection-export.js index 6d61861e8..212248cd4 100644 --- a/plugin-designs/my-upg-100-list/data/selection-export.js +++ b/plugin-designs/my-upg-100-list/data/selection-export.js @@ -13,14 +13,15 @@ // // THE CSV: one row per people group, header first, CRLF line ends. The cells follow the library's rules // (useCsvExport's esc): quote a cell holding , " CR or LF and double its quotes; prefix a leading = + - @ -// with a single quote, so a spreadsheet never runs a name as a formula. +// with a single quote, so a spreadsheet never runs a name as a formula. Also a leading TAB or CR (OWASP's +// full list of formula triggers; the library's esc stops at = + - @). import { formatClipboard } from './territory-counts.js'; export const SELECTION_CSV_COLUMNS = ['slug', 'name', 'country_code', 'population', 'territories', 'color']; export function csvCell(v) { let s = v === null || v === undefined ? '' : String(v); - if (/^[=+\-@]/.test(s)) s = "'" + s; + if (/^[=+\-@\t\r]/.test(s)) s = "'" + s; if (/[",\n\r]/.test(s)) s = '"' + s.replace(/"/g, '""') + '"'; return s; }