don't actually contact remote server when track panel isn't opened (initialize to...
[lantea.git] / js / ui.js
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 // Get the best-available objects for indexedDB and requestAnimationFrame.
6 window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB;
7 window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
8
9 var mainDB;
10 var gAppInitDone = false;
11 var firstRun = false;
12 var gUIHideCountdown = 0;
13 var gWaitCounter = 0;
14 var gTrackUpdateInterval;
15 var gAction, gActionLabel;
16 var authData = null, userData = null;
17 var gBackendURL = "https://backend.lantea.kairo.at";
18 var gAuthClientID = "lantea";
19
20 window.onload = function() {
21   gAction = document.getElementById("action");
22   gActionLabel = document.getElementById("actionlabel");
23
24   var mSel = document.getElementById("mapSelector");
25   for (var mapStyle in gMapStyles) {
26     var opt = document.createElement("option");
27     opt.value = mapStyle;
28     opt.text = gMapStyles[mapStyle].name;
29     mSel.add(opt, null);
30   }
31
32   var areas = document.getElementsByClassName("autoFade");
33   for (var i = 0; i <= areas.length - 1; i++) {
34     areas[i].addEventListener("mouseup", uiEvHandler, false);
35     areas[i].addEventListener("mousemove", uiEvHandler, false);
36     areas[i].addEventListener("mousedown", uiEvHandler, false);
37     areas[i].addEventListener("mouseout", uiEvHandler, false);
38
39     areas[i].addEventListener("touchstart", uiEvHandler, false);
40     areas[i].addEventListener("touchmove", uiEvHandler, false);
41     areas[i].addEventListener("touchend", uiEvHandler, false);
42     areas[i].addEventListener("touchcancel", uiEvHandler, false);
43     areas[i].addEventListener("touchleave", uiEvHandler, false);
44   }
45
46   document.getElementById("body").addEventListener("keydown", uiEvHandler, false);
47
48   if (navigator.platform.length == "") {
49     // For Firefox OS, don't display the "save" button.
50     // Do this by setting the debugHide class for testing in debug mode.
51     document.getElementById("saveTrackButton").classList.add("debugHide");
52   }
53
54   // Set backend URL in a way that it works for testing on localhost as well as
55   // both the lantea.kairo.at and lantea-dev.kairo.at deployments.
56   if (window.location.host == "localhost") {
57     gBackendURL = window.location.protocol + '//' + window.location.host + "/lantea-backend/";
58   }
59   else {
60     gBackendURL = window.location.protocol + '//' + "backend." + window.location.host + "/";
61   }
62   // Make sure to use a different login client ID for the -dev setup.
63   if (/\-dev\./.test(window.location.host)) {
64     gAuthClientID += "-dev";
65   }
66
67   document.getElementById("libCloseButton").onclick = hideLibrary;
68
69   // Set up the login area.
70   document.getElementById("loginbtn").onclick = startLogin;
71   document.getElementById("logoutbtn").onclick = doLogout;
72   // Put in a logged-out state by default.
73   // Opening the track drawer will update this correctly.
74   displayLogout();
75
76   gAction.addEventListener("dbinit-done", initMap, false);
77   gAction.addEventListener("mapinit-done", postInit, false);
78   console.log("starting DB init...");
79   initDB();
80 }
81
82 function postInit(aEvent) {
83   gAction.removeEventListener(aEvent.type, postInit, false);
84   console.log("init done, draw map.");
85   gMapPrefsLoaded = true;
86   gAppInitDone = true;
87   //gMap.resizeAndDraw();  <-- HACK: This triggers bug 1001853, work around with a delay.
88   window.setTimeout(gMap.resizeAndDraw, 100);
89   gActionLabel.textContent = "";
90   gAction.style.display = "none";
91   setTracking(document.getElementById("trackCheckbox"));
92   gPrefs.get("devicename", function(aValue) {
93     if (aValue) {
94       document.getElementById("uploadDevName").value = aValue;
95     }
96   });
97   if (firstRun) {
98     showFirstRunDialog();
99   }
100   else {
101     gPrefs.get("lastInfoShown", function(aValue) {
102       if (!aValue || !parseInt(aValue) || parseInt(aValue) < 1) {
103         showInfoDialog();
104       }
105     });
106   }
107   gPrefs.set("lastInfoShown", 1);
108 }
109
110 window.onresize = function() {
111   gMap.resizeAndDraw();
112 }
113
114 function startLogin() {
115   if (!authData || !authData["state"]) {
116     // We have no oAuth state, try to fetch it and call ourselves again if it worked.
117     prepareLoginButton(function() {
118       if (authData && authData["state"]) {
119         startLogin();
120       }
121       else if (!userData) {
122         // Only warn if we didn't actually end up being logged in.
123         console.log("No OAuth state and fetching fails, client or server may be offline.");
124       }
125     });
126     return;
127   }
128   var authURL = authData["url"] + "authorize?response_type=code&client_id=" + gAuthClientID + "&scope=email" +
129                 "&state=" + authData["state"] + "&redirect_uri=" + encodeURIComponent(getRedirectURI());
130   if (window.open(authURL, "KaiRoAuth", 'height=450,width=600')) {
131     console.log("Sign In window open.");
132   }
133   else {
134     console.log("Opening Sign In window failed.");
135   }
136 }
137
138 function getRedirectURI() {
139   return window.location.protocol + '//' + window.location.host + window.location.pathname.replace("index.html", "") + "login.html";
140 }
141
142 function doLogout() {
143   fetchBackend("logout", "GET", null,
144      function(aResult, aStatus) {
145         if (aStatus < 400) {
146           prepareLoginButton();
147         }
148         else {
149           console.log("Backend issue trying to log out.");
150         }
151       },
152       {}
153   );
154 }
155
156 function prepareLoginButton(aCallback) {
157   fetchBackend("oauth_state", "GET", null,
158       function(aResult, aStatus) {
159         if (aStatus == 200) {
160           if (aResult["logged_in"]) {
161             userData = {
162               "email": aResult["email"],
163               "permissions": aResult["permissions"],
164             };
165             authData = null;
166             displayLogin();
167           }
168           else {
169             authData = {"state": aResult["state"], "url": aResult["url"]};
170             userData = null;
171             displayLogout();
172           }
173         }
174         else {
175           console.log("Backend error " + aStatus + " fetching OAuth state: " + aResult["message"]);
176         }
177         if (aCallback) { aCallback(); }
178       },
179       {}
180   );
181 }
182
183 function finishLogin(aCode, aState) {
184   if (aState == authData["state"]) {
185     fetchBackend("login?code=" + aCode + "&state=" + aState + "&redirect_uri=" + encodeURIComponent(getRedirectURI()), "GET", null,
186         function(aResult, aStatus) {
187           if (aStatus == 200) {
188             userData = {
189               "email": aResult["email"],
190               "permissions": aResult["permissions"],
191             };
192             displayLogin();
193           }
194           else {
195             console.log("Login error " + aStatus + ": " + aResult["message"]);
196             prepareLoginButton();
197           }
198         },
199         {}
200     );
201   }
202   else {
203     console.log("Login state did not match, not continuing with login.");
204   }
205 }
206
207 function displayLogin() {
208   document.getElementById("loginbtn").classList.add("hidden");
209   document.getElementById("logindesc").classList.add("hidden");
210   document.getElementById("username").classList.remove("hidden");
211   document.getElementById("username").textContent = userData.email;
212   document.getElementById("uploadTrackButton").disabled = false;
213   document.getElementById("libraryShowLine").classList.remove("hidden");
214   document.getElementById("logoutbtn").classList.remove("hidden");
215 }
216
217 function displayLogout() {
218   document.getElementById("logoutbtn").classList.add("hidden");
219   document.getElementById("username").classList.add("hidden");
220   document.getElementById("username").textContent = "";
221   document.getElementById("uploadTrackButton").disabled = true;
222   document.getElementById("libraryShowLine").classList.add("hidden");
223   document.getElementById("loginbtn").classList.remove("hidden");
224   document.getElementById("logindesc").classList.remove("hidden");
225 }
226
227 function initDB(aEvent) {
228   // Open DB.
229   if (aEvent)
230     gAction.removeEventListener(aEvent.type, initDB, false);
231   var request = window.indexedDB.open("MainDB-lantea", 2);
232   request.onerror = function(event) {
233     // Errors can be handled here. Error codes explain in:
234     // https://developer.mozilla.org/en/IndexedDB/IDBDatabaseException#Constants
235     if (gDebug)
236       console.log("error opening mainDB: " + event.target.errorCode);
237   };
238   request.onsuccess = function(event) {
239     mainDB = request.result;
240     var throwEv = new CustomEvent("dbinit-done");
241     gAction.dispatchEvent(throwEv);
242   };
243   request.onupgradeneeded = function(event) {
244     mainDB = request.result;
245     var ver = mainDB.version || 0; // version is empty string for a new DB
246     if (gDebug)
247       console.log("mainDB has version " + ver + ", upgrade needed.");
248     if (!mainDB.objectStoreNames.contains("prefs")) {
249       // Create a "prefs" objectStore.
250       var prefsStore = mainDB.createObjectStore("prefs");
251       firstRun = true;
252     }
253     if (!mainDB.objectStoreNames.contains("track")) {
254       // Create a "track" objectStore.
255       var trackStore = mainDB.createObjectStore("track", {autoIncrement: true});
256     }
257     if (!mainDB.objectStoreNames.contains("tilecache")) {
258       // Create a "tilecache" objectStore.
259       var tilecacheStore = mainDB.createObjectStore("tilecache");
260     }
261     mainDB.onversionchange = function(event) {
262       mainDB.close();
263       mainDB = undefined;
264       initDB();
265     };
266   };
267 }
268
269 function showUI() {
270   if (gUIHideCountdown <= 0) {
271     var areas = document.getElementsByClassName('autoFade');
272     for (var i = 0; i <= areas.length - 1; i++) {
273       areas[i].classList.remove("hidden");
274     }
275     setTimeout(maybeHideUI, 1000);
276   }
277   gUIHideCountdown = 5;
278 }
279
280 function maybeHideUI() {
281   gUIHideCountdown--;
282   if (gUIHideCountdown <= 0) {
283     var areas = document.getElementsByClassName('autoFade');
284     for (var i = 0; i <= areas.length - 1; i++) {
285       areas[i].classList.add("hidden");
286     }
287   }
288   else {
289     setTimeout(maybeHideUI, 1000);
290   }
291 }
292
293 function updateTrackInfo() {
294   document.getElementById("trackLengthNum").textContent = calcTrackLength().toFixed(1);
295   var duration = calcTrackDuration();
296   var durationM = Math.round(duration/60);
297   var durationH = Math.floor(durationM/60); durationM = durationM - durationH * 60;
298   document.getElementById("trackDurationH").style.display = durationH ? "inline" : "none";
299   document.getElementById("trackDurationHNum").textContent = durationH;
300   document.getElementById("trackDurationMNum").textContent = durationM;
301 }
302
303 function toggleTrackArea() {
304   var fs = document.getElementById("trackArea");
305   if (fs.classList.contains("hidden")) {
306     prepareLoginButton();
307     fs.classList.remove("hidden");
308     showUI();
309     gTrackUpdateInterval = setInterval(updateTrackInfo, 1000);
310   }
311   else {
312     clearInterval(gTrackUpdateInterval);
313     fs.classList.add("hidden");
314   }
315 }
316
317 function toggleSettings() {
318   var fs = document.getElementById("settingsArea");
319   if (fs.classList.contains("hidden")) {
320     fs.classList.remove("hidden");
321     showUI();
322   }
323   else {
324     fs.classList.add("hidden");
325   }
326 }
327
328 function toggleFullscreen() {
329   if ((document.fullScreenElement && document.fullScreenElement !== null) ||
330       (document.mozFullScreenElement && document.mozFullScreenElement !== null) ||
331       (document.webkitFullScreenElement && document.webkitFullScreenElement !== null)) {
332     if (document.cancelFullScreen) {
333       document.cancelFullScreen();
334     } else if (document.mozCancelFullScreen) {
335       document.mozCancelFullScreen();
336     } else if (document.webkitCancelFullScreen) {
337       document.webkitCancelFullScreen();
338     }
339   }
340   else {
341     var elem = document.getElementById("body");
342     if (elem.requestFullScreen) {
343       elem.requestFullScreen();
344     } else if (elem.mozRequestFullScreen) {
345       elem.mozRequestFullScreen();
346     } else if (elem.webkitRequestFullScreen) {
347       elem.webkitRequestFullScreen();
348     }
349   }
350 }
351
352 function showUploadDialog() {
353   var dia = document.getElementById("trackDialogArea");
354   var areas = dia.children;
355   for (var i = 0; i <= areas.length - 1; i++) {
356     areas[i].style.display = "none";
357   }
358   document.getElementById("uploadDialog").style.display = "block";
359   document.getElementById("uploadTrackButton").disabled = true;
360   dia.classList.remove("hidden");
361 }
362
363 function cancelTrackDialog() {
364   document.getElementById("trackDialogArea").classList.add("hidden");
365   document.getElementById("uploadTrackButton").disabled = false;
366 }
367
368 function showGLWarningDialog() {
369   var dia = document.getElementById("dialogArea");
370   var areas = dia.children;
371   for (var i = 0; i <= areas.length - 1; i++) {
372     areas[i].style.display = "none";
373   }
374   document.getElementById("noGLwarning").style.display = "block";
375   dia.classList.remove("hidden");
376 }
377
378 function showFirstRunDialog() {
379   var dia = document.getElementById("dialogArea");
380   var areas = dia.children;
381   for (var i = 0; i <= areas.length - 1; i++) {
382     areas[i].style.display = "none";
383   }
384   document.getElementById("firstRunIntro").style.display = "block";
385   dia.classList.remove("hidden");
386 }
387
388 function closeDialog() {
389   document.getElementById("dialogArea").classList.add("hidden");
390 }
391
392 function showInfoDialog() {
393   var dia = document.getElementById("dialogArea");
394   var areas = dia.children;
395   for (var i = 0; i <= areas.length - 1; i++) {
396     areas[i].style.display = "none";
397   }
398   document.getElementById("infoDialog").style.display = "block";
399   dia.classList.remove("hidden");
400 }
401
402 var uiEvHandler = {
403   handleEvent: function(aEvent) {
404     var touchEvent = aEvent.type.indexOf('touch') != -1;
405
406     switch (aEvent.type) {
407       case "mousedown":
408       case "touchstart":
409       case "mousemove":
410       case "touchmove":
411       case "mouseup":
412       case "touchend":
413       case "keydown":
414         showUI();
415         break;
416     }
417   }
418 };
419
420 function setUploadField(aField) {
421   switch (aField.id) {
422     case "uploadDevName":
423       gPrefs.set("devicename", aField.value);
424       break;
425   }
426 }
427
428 function makeISOString(aTimestamp) {
429   // ISO time format is YYYY-MM-DDTHH:mm:ssZ
430   var tsDate = new Date(aTimestamp);
431   // Note that .getUTCMonth() returns a number between 0 and 11 (0 for January)!
432   return tsDate.getUTCFullYear() + "-" +
433          (tsDate.getUTCMonth() < 9 ? "0" : "") + (tsDate.getUTCMonth() + 1 ) + "-" +
434          (tsDate.getUTCDate() < 10 ? "0" : "") + tsDate.getUTCDate() + "T" +
435          (tsDate.getUTCHours() < 10 ? "0" : "") + tsDate.getUTCHours() + ":" +
436          (tsDate.getUTCMinutes() < 10 ? "0" : "") + tsDate.getUTCMinutes() + ":" +
437          (tsDate.getUTCSeconds() < 10 ? "0" : "") + tsDate.getUTCSeconds() + "Z";
438 }
439
440 function convertTrack(aTargetFormat) {
441   var out = "";
442   switch (aTargetFormat) {
443     case "gpx":
444       out += '<?xml version="1.0" encoding="UTF-8" ?>' + "\n\n";
445       out += '<gpx version="1.0" creator="Lantea" xmlns="http://www.topografix.com/GPX/1/0">' + "\n";
446       if (gTrack.length) {
447         out += '  <trk>' + "\n";
448         out += '    <trkseg>' + "\n";
449         for (var i = 0; i < gTrack.length; i++) {
450           if (gTrack[i].beginSegment && i > 0) {
451             out += '    </trkseg>' + "\n";
452             out += '    <trkseg>' + "\n";
453           }
454           out += '      <trkpt lat="' + gTrack[i].coords.latitude + '" lon="' +
455                                         gTrack[i].coords.longitude + '">' + "\n";
456           if (gTrack[i].coords.altitude) {
457             out += '        <ele>' + gTrack[i].coords.altitude + '</ele>' + "\n";
458           }
459           out += '        <time>' + makeISOString(gTrack[i].time) + '</time>' + "\n";
460           out += '      </trkpt>' + "\n";
461         }
462         out += '    </trkseg>' + "\n";
463         out += '  </trk>' + "\n";
464       }
465       out += '</gpx>' + "\n";
466       break;
467     case "json":
468       out = JSON.stringify(gTrack);
469       break;
470     default:
471       break;
472   }
473   return out;
474 }
475
476 function saveTrack() {
477   if (gTrack.length) {
478     var outDataURI = "data:application/gpx+xml," +
479                      encodeURIComponent(convertTrack("gpx"));
480     window.open(outDataURI, 'GPX Track');
481   }
482 }
483
484 function saveTrackDump() {
485   if (gTrack.length) {
486     var outDataURI = "data:application/json," +
487                      encodeURIComponent(convertTrack("json"));
488     window.open(outDataURI, 'JSON dump');
489   }
490 }
491
492 function uploadTrack() {
493   // Hide all areas in the dialog.
494   var dia = document.getElementById("trackDialogArea");
495   var areas = dia.children;
496   for (var i = 0; i <= areas.length - 1; i++) {
497     areas[i].style.display = "none";
498   }
499   // Reset all the fields in the status area.
500   document.getElementById("uploadStatusCloseButton").disabled = true;
501   document.getElementById("uploadInProgress").style.display = "block";
502   document.getElementById("uploadSuccess").style.display = "none";
503   document.getElementById("uploadFailed").style.display = "none";
504   document.getElementById("uploadError").style.display = "none";
505   document.getElementById("uploadErrorMsg").textContent = "";
506   // Now show the status area.
507   document.getElementById("uploadStatus").style.display = "block";
508
509   // Assemble field to post to the backend.
510   var formData = new FormData();
511   formData.append("jsondata", convertTrack("json"));
512   var desc = document.getElementById("uploadDesc").value;
513   formData.append("comment",
514                   desc.length ? desc : "Track recorded via Lantea Maps");
515   formData.append("devicename",
516                   document.getElementById("uploadDevName").value);
517   formData.append("public",
518                   document.getElementById("uploadPublic").value);
519
520   fetchBackend("save_track", "POST", formData,
521     function(aResult, aStatusCode) {
522       if (aStatusCode >= 400) {
523         reportUploadStatus(false, aResult);
524       }
525       else if (aResult["id"]) {
526         reportUploadStatus(true);
527       }
528       else { // If no ID is returned, we assume a general error.
529         reportUploadStatus(false);
530       }
531     }
532   );
533 }
534
535 function reportUploadStatus(aSuccess, aResponse) {
536   document.getElementById("uploadStatusCloseButton").disabled = false;
537   document.getElementById("uploadInProgress").style.display = "none";
538   if (aSuccess) {
539     document.getElementById("uploadSuccess").style.display = "block";
540   }
541   else if (aResponse && aResponse["message"]) {
542     document.getElementById("uploadErrorMsg").textContent = aResponse["message"];
543     if (aResponse["errortype"]) {
544       document.getElementById("uploadErrorMsg").textContent += " (" + aResponse["errortype"] + ")";
545     }
546     document.getElementById("uploadError").style.display = "block";
547   }
548   else if (aResponse) {
549     document.getElementById("uploadErrorMsg").textContent = aResponse;
550     document.getElementById("uploadError").style.display = "block";
551   }
552   else {
553     document.getElementById("uploadFailed").style.display = "block";
554   }
555 }
556
557 var gPrefs = {
558   objStore: "prefs",
559
560   get: function(aKey, aCallback) {
561     if (!mainDB)
562       return;
563     var transaction = mainDB.transaction([this.objStore]);
564     var request = transaction.objectStore(this.objStore).get(aKey);
565     request.onsuccess = function(event) {
566       aCallback(request.result, event);
567     };
568     request.onerror = function(event) {
569       // Errors can be handled here.
570       aCallback(undefined, event);
571     };
572   },
573
574   set: function(aKey, aValue, aCallback) {
575     if (!mainDB)
576       return;
577     var success = false;
578     var transaction = mainDB.transaction([this.objStore], "readwrite");
579     var objStore = transaction.objectStore(this.objStore);
580     var request = objStore.put(aValue, aKey);
581     request.onsuccess = function(event) {
582       success = true;
583       if (aCallback)
584         aCallback(success, event);
585     };
586     request.onerror = function(event) {
587       // Errors can be handled here.
588       if (aCallback)
589         aCallback(success, event);
590     };
591   },
592
593   unset: function(aKey, aCallback) {
594     if (!mainDB)
595       return;
596     var success = false;
597     var transaction = mainDB.transaction([this.objStore], "readwrite");
598     var request = transaction.objectStore(this.objStore).delete(aKey);
599     request.onsuccess = function(event) {
600       success = true;
601       if (aCallback)
602         aCallback(success, event);
603     };
604     request.onerror = function(event) {
605       // Errors can be handled here.
606       if (aCallback)
607         aCallback(success, event);
608     }
609   }
610 };
611
612 var gTrackStore = {
613   objStore: "track",
614
615   getList: function(aCallback) {
616     if (!mainDB)
617       return;
618     var transaction = mainDB.transaction([this.objStore]);
619     var objStore = transaction.objectStore(this.objStore);
620     if (objStore.getAll) { // currently Mozilla-specific
621       objStore.getAll().onsuccess = function(event) {
622         aCallback(event.target.result);
623       };
624     }
625     else { // Use cursor (standard method).
626       var tPoints = [];
627       objStore.openCursor().onsuccess = function(event) {
628         var cursor = event.target.result;
629         if (cursor) {
630           tPoints.push(cursor.value);
631           cursor.continue();
632         }
633         else {
634           aCallback(tPoints);
635         }
636       };
637     }
638   },
639
640   getListStepped: function(aCallback) {
641     if (!mainDB)
642       return;
643     var transaction = mainDB.transaction([this.objStore]);
644     var objStore = transaction.objectStore(this.objStore);
645     // Use cursor in reverse direction (so we get the most recent position first)
646     objStore.openCursor(null, "prev").onsuccess = function(event) {
647       var cursor = event.target.result;
648       if (cursor) {
649         aCallback(cursor.value);
650         cursor.continue();
651       }
652       else {
653         aCallback(null);
654       }
655     };
656   },
657
658   push: function(aValue, aCallback) {
659     if (!mainDB)
660       return;
661     var transaction = mainDB.transaction([this.objStore], "readwrite");
662     var objStore = transaction.objectStore(this.objStore);
663     var request = objStore.add(aValue);
664     request.onsuccess = function(event) {
665       if (aCallback)
666         aCallback(request.result, event);
667     };
668     request.onerror = function(event) {
669       // Errors can be handled here.
670       if (aCallback)
671         aCallback(false, event);
672     };
673   },
674
675   clear: function(aCallback) {
676     if (!mainDB)
677       return;
678     var success = false;
679     var transaction = mainDB.transaction([this.objStore], "readwrite");
680     var request = transaction.objectStore(this.objStore).clear();
681     request.onsuccess = function(event) {
682       success = true;
683       if (aCallback)
684         aCallback(success, event);
685     };
686     request.onerror = function(event) {
687       // Errors can be handled here.
688       if (aCallback)
689         aCallback(success, event);
690     }
691   }
692 };
693
694 function fetchBackend(aEndpoint, aMethod, aSendData, aCallback, aCallbackForwards) {
695   var XHR = new XMLHttpRequest();
696   XHR.onreadystatechange = function() {
697     if (XHR.readyState == 4) {
698       // State says we are fully loaded.
699       var result = {};
700       if (XHR.getResponseHeader("Content-Type") == "application/json") {
701         // Got a JSON object, see if we have success.
702         try {
703           result = JSON.parse(XHR.responseText);
704         }
705         catch (e) {
706           console.log(e);
707           result = {"error": e,
708                     "message": XHR.responseText};
709         }
710       }
711       else {
712         result = XHR.responseText;
713       }
714       aCallback(result, XHR.status, aCallbackForwards);
715     }
716   };
717   XHR.open(aMethod, gBackendURL + aEndpoint, true);
718   XHR.withCredentials = "true";
719   //XHR.setRequestHeader("Accept", "application/json");
720   try {
721     XHR.send(aSendData); // Send actual form data.
722   }
723   catch (e) {
724     aCallback(e, 500, aCallbackForwards);
725   }
726 }