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