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