Fix Firefox export authentication issue

Requires Kalamar v0.64

Resolves #183

Change-Id: I0ff128b63748acc66de43ce64e96b6d35e1615bb
diff --git a/src/main/java/de/ids_mannheim/korap/plkexport/Service.java b/src/main/java/de/ids_mannheim/korap/plkexport/Service.java
index 61a4564..c7d5826 100644
--- a/src/main/java/de/ids_mannheim/korap/plkexport/Service.java
+++ b/src/main/java/de/ids_mannheim/korap/plkexport/Service.java
@@ -16,7 +16,7 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import jakarta.ws.rs.BadRequestException;
+
 import jakarta.ws.rs.WebApplicationException;
 import jakarta.ws.rs.DefaultValue;
 import jakarta.ws.rs.FormParam;
@@ -143,7 +143,8 @@
                              int hitc,
                              EventOutput eventOutput,
                              boolean randomizePageOrder,
-                             long seed
+                             long seed,
+                             String authToken
         ) throws WebApplicationException {
         
         // These parameters are mandatory
@@ -223,6 +224,11 @@
 
             auth = authFromCookie(servletReq);
         };
+        
+        // Override auth if provided
+        if ((auth == null || auth.isEmpty()) && authToken != null) {
+            auth = authToken;
+        }
     
         String resp;
         WebTarget resource;
@@ -455,7 +461,7 @@
 
         boolean randomize = "true".equals(randomizePageOrderStr);
 
-        Exporter exp = export(fname, format, q, cq, ql, cutoffStr, hitc, null, randomize, seed);
+        Exporter exp = export(fname, format, q, cq, ql, cutoffStr, hitc, null, randomize, seed, null);
         
         return exp.serve().build();
     };
@@ -495,7 +501,8 @@
         @QueryParam("cutoff") String cutoffStr,
         @QueryParam("hitc") int hitc,
         @QueryParam("randomizePageOrder") String randomizePageOrderStr,
-        @DefaultValue("42") @QueryParam("seed") long seed
+        @DefaultValue("42") @QueryParam("seed") long seed,
+        @QueryParam("auth") String authToken
         ) throws InterruptedException {
 
         boolean randomize = "true".equals(randomizePageOrderStr);
@@ -523,7 +530,7 @@
                         eventBuilder.data("init");
                         eventOutput.write(eventBuilder.build());
                         Exporter exp = export(
-                            fname, format, q, cq, ql, cutoffStr, hitc, eventOutput, randomize, seed
+                            fname, format, q, cq, ql, cutoffStr, hitc, eventOutput, randomize, seed, authToken
                             );
 
                         if (eventOutput.isClosed())
@@ -577,17 +584,63 @@
         t.start();      
 //        t.join();
 
-        String origin = prop.getProperty("server.origin","*");
+        String origin = prop.getProperty("server.origin", "*");
+        String reqOrigin = null;
         if (servletReq != null) {
-            // This is temporary to allow for session riding
-            origin = servletReq.getHeader("Origin");
-        };
+            reqOrigin = servletReq.getHeader("Origin");
+            
+            // Treat "null" string (sent by browsers for privacy/sandboxing) same as missing
+            if (reqOrigin != null && reqOrigin.equals("null")) {
+                reqOrigin = null;
+            }
+            
+            // If Origin is missing, try to construct it from the request (for same-origin)
+            if (reqOrigin == null || reqOrigin.isEmpty()) {
+                String host = servletReq.getHeader("Host");
+                String scheme = servletReq.getScheme();
+                
+                // Check X-Forwarded-Proto for proxy scenarios
+                String forwardedProto = servletReq.getHeader("X-Forwarded-Proto");
+                if (forwardedProto != null) {
+                    scheme = forwardedProto;
+                }
+                
+                if (host != null) {
+                    reqOrigin = scheme + "://" + host;
+                }
+            }
 
-        return Response.ok(eventOutput, String.valueOf(SseFeature.SERVER_SENT_EVENTS_TYPE))
-            .header("Access-Control-Allow-Origin", origin)
-            .header("Access-Control-Allow-Credentials", "true")
-            .header("Vary","Origin")
-            .build();
+            // Fallback: If still no origin, try Referer
+            if (reqOrigin == null || reqOrigin.isEmpty()) {
+                String referer = servletReq.getHeader("Referer");
+                if (referer != null) {
+                    try {
+                        java.net.URI refUri = java.net.URI.create(referer);
+                        if (refUri.getScheme() != null && refUri.getAuthority() != null) {
+                            reqOrigin = refUri.getScheme() + "://" + refUri.getAuthority();
+                        }
+                    } catch (Exception e) {
+                        // Ignore invalid/missing referer
+                    }
+                }
+            }
+        }
+
+        if (reqOrigin != null && !reqOrigin.isEmpty()) {
+            origin = reqOrigin;
+        }
+
+        ResponseBuilder builder = Response.ok(eventOutput, String.valueOf(SseFeature.SERVER_SENT_EVENTS_TYPE))
+            .header("Vary", "Origin");
+
+        // Always use specific origin (echoed or fallback) with Credentials=true
+        // This supports both cookie-based and token-based auth securely
+        if (!origin.equals("*")) {
+            builder.header("Access-Control-Allow-Origin", origin);
+            builder.header("Access-Control-Allow-Credentials", "true");
+        }
+
+        return builder.build();
     };
 
 
@@ -716,13 +769,17 @@
         for (int i = 0; i < cookies.length; i++) {
 
             // Check the valid name and ignore irrelevant cookies
-            if (cookieName == "") {
-                if (!cookies[i].getName().equals("kalamar")) {
-                    continue;
-                }
-            } else if (!cookies[i].getName().equals(cookieName)) {
-                continue;
-            };
+            boolean match = false;
+            // Strict match if configured
+            if (!cookieName.isEmpty() && cookies[i].getName().equals(cookieName)) {
+                match = true;
+            }
+            // Prefix match (fallback or default)
+            else if (cookies[i].getName().startsWith("kalamar")) {
+                match = true;
+            }
+            
+            if (!match) continue;
 
             // Get the value
             String b64 = cookies[i].getValue();
@@ -858,7 +915,7 @@
     }
 
     private Locale getPreferredSupportedLocale() throws IOException {
-        Locale fallback = new Locale("en");
+        Locale fallback = Locale.forLanguageTag("en");
 
         if (req != null) {
             for (Locale l : req.getAcceptableLanguages()) {
diff --git a/src/main/resources/assets/export.js b/src/main/resources/assets/export.js
index af5ae84..0220849 100644
--- a/src/main/resources/assets/export.js
+++ b/src/main/resources/assets/export.js
@@ -3,6 +3,19 @@
 function pluginit(P) {
 
   // Request query params from the embedding window
+  let authToken = null;
+  P.requestMsg(
+    {
+      'action':'get',
+      'key':'User'
+    },
+    function (d) {
+      if (d.value && d.value.auth) {
+        authToken = d.value.auth;
+      };
+    }
+  );
+
   P.requestMsg(
     {
       'action':'get',
@@ -95,6 +108,10 @@
       };
     };
     
+    if (authToken) {
+      query.append("auth", authToken);
+    };
+    
     reqStream(url.href);
     return false;
   };
@@ -118,8 +135,12 @@
     prog.style.display = "none";
     sse.close();
     window.Plugin.resize();
-    console.log(e.data);
-    window.Plugin.log(0, e.data);
+    let msg = "Connection error";
+    if (e.data !== undefined) {
+        msg = e.data;
+    };
+    console.log(msg);
+    window.Plugin.log(0, msg);
   };
 
   sse.addEventListener('Error', err);