Sanitize CSV export for security reasons

This follows the recommendations in
https://wiki.mozilla.org/images/6/6f/Phpmyadmin-report.pdf to sanitize
the export, preventing the execution of arbitrary code in spreadsheet
editors such as Excel.

Change-Id: Icb91c425f6fec0bc3d7f83ad3bdcc7b11b483983
diff --git a/Changes b/Changes
index 73d2732..5cc9ed9 100644
--- a/Changes
+++ b/Changes
@@ -1,7 +1,8 @@
-0.2.5 2021-05-07
+0.2.5 2021-05-27
     - Silence logging in tests.
+    - Sanitize CSV export for security reasons.
 
-0.2.4 2021-04-27
+0.2.4 2021-05-07
     - Fix temporary session-riding capabilities.
     - Introduced central tinylog.
     - Fix nullpointer in RTF export.
diff --git a/src/main/java/de/ids_mannheim/korap/plkexport/CsvExporter.java b/src/main/java/de/ids_mannheim/korap/plkexport/CsvExporter.java
index 111bc45..3ac7854 100644
--- a/src/main/java/de/ids_mannheim/korap/plkexport/CsvExporter.java
+++ b/src/main/java/de/ids_mannheim/korap/plkexport/CsvExporter.java
@@ -102,21 +102,43 @@
             return;
 
         // If meta characters exist, make a quote
-        if (s.contains(",")  ||
+        if (s.startsWith("=") ||
+            s.startsWith("-") ||
+            s.startsWith("\"") ||
+            s.startsWith("+") ||
+            s.startsWith("@") ||
+            s.contains(",")  ||
             s.contains("\"") ||
             s.contains("\n") ||
             s.contains(" ")  ||
             s.contains("\t") ||
-            s.contains(";")) {
+            s.contains(";")
+          ) {
 
             // Iterate over all characters
             // and turn '"' into '""'.
             w.append('"');
+
+            // Sanitize following recommendations in
+            // https://wiki.mozilla.org/images/6/6f/Phpmyadmin-report.pdf
+            if (s.startsWith("=") ||
+                s.startsWith("-") ||
+                s.startsWith("\"") ||
+                s.startsWith("+") ||
+                s.startsWith("@")) {
+              w.append(" ");
+            };
+
+            
             for (int i = 0; i < s.length(); i++) {
                 final char c = s.charAt(i);
                 if (c == '"') {
                     w.append('"').append('"');
                 }
+                // Sanitize
+                else if (c == '\t') {
+                  w.append(' ');
+                }
                 else {
                     w.append(c);
                 };
diff --git a/src/test/java/de/ids_mannheim/korap/plkexport/CsvExporterTest.java b/src/test/java/de/ids_mannheim/korap/plkexport/CsvExporterTest.java
index 17648ea..59cba3b 100644
--- a/src/test/java/de/ids_mannheim/korap/plkexport/CsvExporterTest.java
+++ b/src/test/java/de/ids_mannheim/korap/plkexport/CsvExporterTest.java
@@ -54,7 +54,7 @@
         String[] lines = x.split("\n");
         assertEquals(lines[0],"HasMoreLeft,leftContext,Match,rightContext,HasMoreRight,isCutted,textSigle,author,pubDate,title");
         assertEquals(lines[1],",Simple,match1,Snippet,,,RTF/G59/34284,Goethe,20051103,Title1");
-        assertEquals(lines[2],"...,\"Simpler, \"\"faster\"\"\",\"\"\"match2\"\"\",Snippet,...,,RTF/G59/34285,Schiller,20051104,\"Title2, the\"");
+        assertEquals(lines[2],"...,\"Simpler, \"\"faster\"\"\",\" \"\"match2\"\"\",Snippet,...,,RTF/G59/34285,Schiller,20051104,\"Title2, the\"");
         assertEquals(lines.length,3);
 
         csv = new CsvExporter();
@@ -81,6 +81,38 @@
     };
 
     @Test
+    public void testCsvSanitize () throws IOException {
+        CsvExporter csv = new CsvExporter();
+        csv.init("{\"meta\":\"ja\",\"collection\":\"hm\",\"query\":\"cool\"," +
+                 "\"matches\":["+
+                 "{\"author\":\"Goethe\","+
+                 "\"title\":\"@Title1\","+
+                 "\"pubDate\":\"20051103\","+
+                 "\"textSigle\":\"RTF/G59/34284\","+
+                 "\"snippet\":\"=Simple <mark>-match1</mark> +Snippet\"}"+
+                 ","+
+                 "{\"author\":\"+Schiller\","+
+                 "\"title\":\"-Title2,\\tthe\","+
+                 "\"pubDate\":\"20051104\","+
+                 "\"textSigle\":\"RTF/G59/34285\","+
+                 "\"snippet\":\"<span class=\\\"context-left\\\"><span class=\\\"more\\\"></span>"+
+                 "Simpler, \\\"faster\\\" </span><span class=\\\"match\\\"><mark>&quot;match2&quot;</mark></span>"+
+                 "<span class=\\\"context-right\\\"> Snippet"+
+                 "<span class=\\\"more\\\"></span></span>\"}"+
+                 "]}");
+        csv.finish();
+
+        Response resp = csv.serve().build();
+        String x = (String) resp.getEntity();
+        resp.close();
+        String[] lines = x.split("\n");
+        assertEquals(lines[0],"HasMoreLeft,leftContext,Match,rightContext,HasMoreRight,isCutted,textSigle,author,pubDate,title");
+        assertEquals(lines[1],",\" =Simple\",\" -match1\",\" +Snippet\",,,RTF/G59/34284,Goethe,20051103,\" @Title1\"");
+        assertEquals(lines[2],"...,\"Simpler, \"\"faster\"\"\",\" \"\"match2\"\"\",Snippet,...,,RTF/G59/34285,\" +Schiller\",20051104,\" -Title2, the\"");
+        assertEquals(lines.length,3);
+    };
+  
+    @Test
     public void testAttributes () throws IOException {
         CsvExporter csv = new CsvExporter();
         csv.setFileName("Beispiel");
diff --git a/src/test/java/de/ids_mannheim/korap/plkexport/ServiceTest.java b/src/test/java/de/ids_mannheim/korap/plkexport/ServiceTest.java
index e554807..6b6e994 100644
--- a/src/test/java/de/ids_mannheim/korap/plkexport/ServiceTest.java
+++ b/src/test/java/de/ids_mannheim/korap/plkexport/ServiceTest.java
@@ -679,7 +679,7 @@
 
         assertEquals(lines.length,10);
         assertEquals(lines[0],"HasMoreLeft,leftContext,Match,rightContext,HasMoreRight,isCutted,textSigle,author,pubDate,title");
-        assertEquals(lines[1],"...,\"1 Tag gesperrt. 24h Urlaub.^^ LG;--  17:40, 11. Jan. 2011 (CET) Danke ich habe die nahezu zeitgleichen VMs von Dir und Ironhoof gesehen. Ob es ein Grund zum Jubeln ist, sei dahin gestellt. Immerhin habe ich für 24 Stunden einen \"\"\",Plagegeist,\"\"\" weniger. Sag mal, zum Kölner Stammtisch isses doch nicht so weit ... wie wär's ? Besten  17:49, 11. Jan. 2011 (CET) Er wurde gesperrt. Nach dem Theater hier zurecht. ABER: auch deine Beiträge hier, die er versuchte zu löschen, sorgen nicht für\",...,,WUD17/G59/34284,\"Umherirrender, u.a.\",2017-07-01,\"Benutzer Diskussion:Gruß Tom/Archiv/2011\"");
+        assertEquals(lines[1],"...,\"1 Tag gesperrt. 24h Urlaub.^^ LG;--  17:40, 11. Jan. 2011 (CET) Danke ich habe die nahezu zeitgleichen VMs von Dir und Ironhoof gesehen. Ob es ein Grund zum Jubeln ist, sei dahin gestellt. Immerhin habe ich für 24 Stunden einen \"\"\",Plagegeist,\" \"\" weniger. Sag mal, zum Kölner Stammtisch isses doch nicht so weit ... wie wär's ? Besten  17:49, 11. Jan. 2011 (CET) Er wurde gesperrt. Nach dem Theater hier zurecht. ABER: auch deine Beiträge hier, die er versuchte zu löschen, sorgen nicht für\",...,,WUD17/G59/34284,\"Umherirrender, u.a.\",2017-07-01,\"Benutzer Diskussion:Gruß Tom/Archiv/2011\"");
 
         frmap.putSingle("hitc", "7");