@@ -20,6 +20,25 @@ class ExportService {
2020 final SettingsService _settingsService = SettingsService .instance;
2121 final DatabaseService _databaseService = DatabaseService .instance;
2222
23+ /// Escapes a string for use in a CSV file.
24+ ///
25+ /// If the string contains a comma, a double quote, or a newline character,
26+ /// it will be enclosed in double quotes. Any double quotes within the string
27+ /// will be replaced by two double quotes.
28+ String _csvEscape (String ? text) {
29+ if (text == null ) {
30+ return '' ;
31+ }
32+ // If the text contains a comma, a quote, or a newline, it needs to be quoted.
33+ if (text.contains (',' ) || text.contains ('"' ) || text.contains ('\n ' )) {
34+ // In a quoted field, any double-quote characters must be escaped by doubling them.
35+ final escapedText = text.replaceAll ('"' , '""' );
36+ return '"$escapedText "' ;
37+ }
38+ // If no special characters are present, the text can be used as-is.
39+ return text;
40+ }
41+
2342 Future <ServiceResult > _getAndSetExportPath () async {
2443 final PermissionStatus status;
2544 if (Platform .isAndroid) {
@@ -146,22 +165,25 @@ class ExportService {
146165 // 3. CSV Rows
147166 for (final entry in newActions) {
148167 final action = entry.value;
149- // Convert timestamp back to ISO string for the export file
150168 final timestampString =
151169 DateTime .fromMillisecondsSinceEpoch (action.timestamp)
152170 .toIso8601String ();
153- final row = [
154- entry.key,
155- action.actionType.name, // Use .name for the enum
156- '"$timestampString "' , // Quote the timestamp string
171+
172+ // Use a list to hold the properly escaped cell values for the current row
173+ final rowValues = < String > [
174+ // ID doesn't need escaping as it's a number.
175+ entry.key.toString (),
176+ // Escape all other string values to handle special characters.
177+ _csvEscape (action.actionType.name),
178+ _csvEscape (timestampString),
157179 ];
158180
159- // Add values for each dynamic answer column
181+ // Add values for each dynamic answer column, escaping each one.
160182 for (final key in sortedAnswerKeys) {
161- // Append the answer if it exists, otherwise append an empty string
162- row .add (action.answers ? [key] ? . toString () ?? '' );
183+ final value = action.answers ? [key] ? . toString ();
184+ rowValues .add (_csvEscape (value) );
163185 }
164- csvBuffer.writeln (row .join (',' ));
186+ csvBuffer.writeln (rowValues .join (',' ));
165187 }
166188
167189 // 4. Write file
0 commit comments