f451fbe52932ec53f900bcd15943431e99b49d76
[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 {
531         reportUploadStatus(true);
532       }
533     }
534   );
535 }
536
537 function reportUploadStatus(aSuccess, aMessage) {
538   document.getElementById("uploadStatusCloseButton").disabled = false;
539   document.getElementById("uploadInProgress").style.display = "none";
540   if (aSuccess) {
541     document.getElementById("uploadSuccess").style.display = "block";
542   }
543   else if (aMessage) {
544     document.getElementById("uploadErrorMsg").textContent = aMessage;
545     document.getElementById("uploadError").style.display = "block";
546   }
547   else {
548     document.getElementById("uploadFailed").style.display = "block";
549   }
550 }
551
552 var gPrefs = {
553   objStore: "prefs",
554
555   get: function(aKey, aCallback) {
556     if (!mainDB)
557       return;
558     var transaction = mainDB.transaction([this.objStore]);
559     var request = transaction.objectStore(this.objStore).get(aKey);
560     request.onsuccess = function(event) {
561       aCallback(request.result, event);
562     };
563     request.onerror = function(event) {
564       // Errors can be handled here.
565       aCallback(undefined, event);
566     };
567   },
568
569   set: function(aKey, aValue, aCallback) {
570     if (!mainDB)
571       return;
572     var success = false;
573     var transaction = mainDB.transaction([this.objStore], "readwrite");
574     var objStore = transaction.objectStore(this.objStore);
575     var request = objStore.put(aValue, aKey);
576     request.onsuccess = function(event) {
577       success = true;
578       if (aCallback)
579         aCallback(success, event);
580     };
581     request.onerror = function(event) {
582       // Errors can be handled here.
583       if (aCallback)
584         aCallback(success, event);
585     };
586   },
587
588   unset: function(aKey, aCallback) {
589     if (!mainDB)
590       return;
591     var success = false;
592     var transaction = mainDB.transaction([this.objStore], "readwrite");
593     var request = transaction.objectStore(this.objStore).delete(aKey);
594     request.onsuccess = function(event) {
595       success = true;
596       if (aCallback)
597         aCallback(success, event);
598     };
599     request.onerror = function(event) {
600       // Errors can be handled here.
601       if (aCallback)
602         aCallback(success, event);
603     }
604   }
605 };
606
607 var gTrackStore = {
608   objStore: "track",
609
610   getList: function(aCallback) {
611     if (!mainDB)
612       return;
613     var transaction = mainDB.transaction([this.objStore]);
614     var objStore = transaction.objectStore(this.objStore);
615     if (objStore.getAll) { // currently Mozilla-specific
616       objStore.getAll().onsuccess = function(event) {
617         aCallback(event.target.result);
618       };
619     }
620     else { // Use cursor (standard method).
621       var tPoints = [];
622       objStore.openCursor().onsuccess = function(event) {
623         var cursor = event.target.result;
624         if (cursor) {
625           tPoints.push(cursor.value);
626           cursor.continue();
627         }
628         else {
629           aCallback(tPoints);
630         }
631       };
632     }
633   },
634
635   getListStepped: function(aCallback) {
636     if (!mainDB)
637       return;
638     var transaction = mainDB.transaction([this.objStore]);
639     var objStore = transaction.objectStore(this.objStore);
640     // Use cursor in reverse direction (so we get the most recent position first)
641     objStore.openCursor(null, "prev").onsuccess = function(event) {
642       var cursor = event.target.result;
643       if (cursor) {
644         aCallback(cursor.value);
645         cursor.continue();
646       }
647       else {
648         aCallback(null);
649       }
650     };
651   },
652
653   push: function(aValue, aCallback) {
654     if (!mainDB)
655       return;
656     var transaction = mainDB.transaction([this.objStore], "readwrite");
657     var objStore = transaction.objectStore(this.objStore);
658     var request = objStore.add(aValue);
659     request.onsuccess = function(event) {
660       if (aCallback)
661         aCallback(request.result, event);
662     };
663     request.onerror = function(event) {
664       // Errors can be handled here.
665       if (aCallback)
666         aCallback(false, event);
667     };
668   },
669
670   clear: function(aCallback) {
671     if (!mainDB)
672       return;
673     var success = false;
674     var transaction = mainDB.transaction([this.objStore], "readwrite");
675     var request = transaction.objectStore(this.objStore).clear();
676     request.onsuccess = function(event) {
677       success = true;
678       if (aCallback)
679         aCallback(success, event);
680     };
681     request.onerror = function(event) {
682       // Errors can be handled here.
683       if (aCallback)
684         aCallback(success, event);
685     }
686   }
687 };
688
689 function fetchBackend(aEndpoint, aMethod, aSendData, aCallback, aCallbackForwards) {
690   var XHR = new XMLHttpRequest();
691   XHR.onreadystatechange = function() {
692     if (XHR.readyState == 4) {
693       // State says we are fully loaded.
694       var result = {};
695       if (XHR.getResponseHeader("Content-Type") == "application/json") {
696         // Got a JSON object, see if we have success.
697         try {
698           result = JSON.parse(XHR.responseText);
699         }
700         catch (e) {
701           console.log(e);
702           result = {"error": e,
703                     "message": XHR.responseText};
704         }
705       }
706       else {
707         result = XHR.responseText;
708       }
709       aCallback(result, XHR.status, aCallbackForwards);
710     }
711   };
712   XHR.open(aMethod, gBackendURL + aEndpoint, true);
713   XHR.withCredentials = "true";
714   //XHR.setRequestHeader("Accept", "application/json");
715   try {
716     XHR.send(aSendData); // Send actual form data.
717   }
718   catch (e) {
719     aCallback(e, 500, aCallbackForwards);
720   }
721 }
722
723 function getParameterByName(aName) {
724   // from http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
725   name = aName.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
726   var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
727       results = regex.exec(location.search);
728   return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
729 }