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