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