From 644d2b9ce51321b8219a9902b8bfcac8b9443506 Mon Sep 17 00:00:00 2001 From: Scott McGrath Date: Thu, 9 Jan 2025 03:34:13 -0500 Subject: [PATCH] fix(legacy): additional specifics added to CSVexport.js for RFC 4180 (#3131) ### Description The existing implementation for exporting playout logs to a CSV file incorporates a very simplified CSV format. Some aspects of the complete [RFC](https://www.rfc-editor.org/rfc/rfc4180) are missing, such as escaping of quotes, and quoting of fields that contain certain characters. This is problematic for common office spreadsheet tools, and practically, anything else. Many radio stations rely on this functionality to work well for exporting playout data, for example, in order to compile data for reporting requirements. **This is a new feature**: The changes in this PR add quoting of fields containing a comma, as well as those containing a CR/LF. It also escapes quotes by doubling them. I'm not sure it makes CSVexport.js completely RFC 4180 compliant, but it is much closer than it was. **I have updated the documentation to reflect these changes**: I don't think there are any documentation changes necessary; this is probably expected behavior for anyone trying to use the CSV exporter. ### Testing Notes **What I did:** To validate this, I did a clean install of Debian, cloned from the official libretime repo, and applied the code as a patch to the installer. I then proceeded with the install and then loaded a database from a running system (so that I had some playout data to test with). I then performed the playout history export and examined the resulting CSV file and after seeing previously problematic fields properly quoted, was convinced it looked the way I expected. I loaded the csv file into Libreoffice Calc and did not see any errors. **How you can replicate my testing:** See "What I did" above, basically run the patch after cloning the installer from the repo. You could also apply the changes to a running system by applying the patch to the file: /usr/share/libretime/legacy/public/js/libs/CSVexport.js Be sure to clear your browser cache and do a hard reload of the web interface, before re-testing. ### **Links** Closes: #2477 --- legacy/application/assets.json | 2 +- legacy/public/js/libs/CSVexport.js | 26 +++++++++++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/legacy/application/assets.json b/legacy/application/assets.json index 6968c6759..96ffb6af7 100644 --- a/legacy/application/assets.json +++ b/legacy/application/assets.json @@ -122,7 +122,7 @@ "js/jplayer/skin/jplayer.airtime.audio.preview.css": "c721fe0587569391dcfa7d0f7b440d2b", "js/jplayer/skin/jplayer.audio-preview.blue.monday.css": "8565bf8e077eeb5bbba3c88bed42a590", "js/jplayer/skin/jplayer.blue.monday.css": "491c7a82ae4571ae4a73645bf9755eaf", - "js/libs/CSVexport.js": "07a91f824f41d2d0d1884b5dd10f63e4", + "js/libs/CSVexport.js": "42c1fcfff5c717482f21bff1a9002295", "js/libs/angular.min.js": "26680517e1024ca2c4f9ed4e0aa1619e", "js/libs/dayjs.min.js": "bea3f1180a3e2e45eccf9d76f990f3b4", "js/libs/dropzone.min.js": "f59ac59a89d9c569a27ca7ead38d2396", diff --git a/legacy/public/js/libs/CSVexport.js b/legacy/public/js/libs/CSVexport.js index bb246a767..ea60ee3b9 100644 --- a/legacy/public/js/libs/CSVexport.js +++ b/legacy/public/js/libs/CSVexport.js @@ -17,21 +17,29 @@ Compress with: http://jscompress.com/ csvEncoding = 'data:text/csv;charset=utf-8,', csvOutput = "", csvRows = [], - BREAK = '\r\n', + BREAK = '\r\n', // Use CRLF for line breaks DELIMITER = ',', - FILENAME = "export.csv"; + FILENAME = "export.csv"; // Get and Write the headers csvHeaders = Object.keys(csvData[0]); - csvOutput += csvHeaders.join(',') + BREAK; + csvOutput += csvHeaders.join(DELIMITER) + BREAK; for (var i = 0; i < csvData.length; i++) { - var rowElements = []; - for(var k = 0; k < csvHeaders.length; k++) { - rowElements.push(csvData[i][csvHeaders[k]]); - } // Write the row array based on the headers - csvRows.push(rowElements.join(DELIMITER)); - } + var rowElements = []; + for (var k = 0; k < csvHeaders.length; k++) { + var cell = csvData[i][csvHeaders[k]]; + if (typeof cell === 'string') { + if (cell.includes('"')) { + cell = '"' + cell.replace(/"/g, '""') + '"'; // Escape double quotes by doubling them + } else if (cell.includes(DELIMITER) || cell.includes('\r') || cell.includes('\n')) { + cell = '"' + cell + '"'; // Enclose in double quotes if it contains commas, CR, or LF + } + } + rowElements.push(cell); + } // Write the row array based on the headers + csvRows.push(rowElements.join(DELIMITER)); + } csvOutput += csvRows.join(BREAK);