Merge mozilla-central and tracemonkey. (a=blockers)
[mozilla-central.git] / dom / system / NetworkGeolocationProvider.js
blobfe48420f4b49e18da636e8a12f18e10658ededd8
1 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
3 const Ci = Components.interfaces;
4 const Cc = Components.classes;
6 var gLoggingEnabled = false;
7 var gTestingEnabled = false;
9 function nowInSeconds()
11     return Date.now() / 1000;
14 function LOG(aMsg) {
15   if (gLoggingEnabled)
16   {
17     aMsg = "*** WIFI GEO: " + aMsg + "\n";
18     Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).logStringMessage(aMsg);
19     dump(aMsg);
20   }
23 function WifiGeoAddressObject(streetNumber, street, premises, city, county, region, country, countryCode, postalCode) {
25   this.streetNumber = streetNumber;
26   this.street       = street;
27   this.premises     = premises;
28   this.city         = city;
29   this.county       = county;
30   this.region       = region;
31   this.country      = country;
32   this.countryCode  = countryCode;
33   this.postalCode   = postalCode;
36 WifiGeoAddressObject.prototype = {
38     QueryInterface:   XPCOMUtils.generateQI([Ci.nsIDOMGeoPositionAddress, Ci.nsIClassInfo]),
40     getInterfaces: function(countRef) {
41         var interfaces = [Ci.nsIDOMGeoPositionAddress, Ci.nsIClassInfo, Ci.nsISupports];
42         countRef.value = interfaces.length;
43         return interfaces;
44     },
46     getHelperForLanguage: function(language) null,
47     implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
48     flags: Ci.nsIClassInfo.DOM_OBJECT,
51 function WifiGeoCoordsObject(lat, lon, acc, alt, altacc) {
52     this.latitude = lat;
53     this.longitude = lon;
54     this.accuracy = acc;
55     this.altitude = alt;
56     this.altitudeAccuracy = altacc;
59 WifiGeoCoordsObject.prototype = {
61     QueryInterface:   XPCOMUtils.generateQI([Ci.nsIDOMGeoPositionCoords, Ci.nsIClassInfo]),
63     getInterfaces: function(countRef) {
64         var interfaces = [Ci.nsIDOMGeoPositionCoords, Ci.nsIClassInfo, Ci.nsISupports];
65         countRef.value = interfaces.length;
66         return interfaces;
67     },
69     getHelperForLanguage: function(language) null,
70     classDescription: "wifi geo position coords object",
71     implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
72     flags: Ci.nsIClassInfo.DOM_OBJECT,
74     latitude: 0,
75     longitude: 0,
76     accuracy: 0,
77     altitude: 0,
78     altitudeAccuracy: 0,
82 function WifiGeoPositionObject(location) {
84     this.coords = new WifiGeoCoordsObject(location.latitude,
85                                           location.longitude,
86                                           location.accuracy || 12450, // .5 * circumference of earth.
87                                           location.altitude || 0,
88                                           location.altitude_accuracy || 0);
90     if (location.address) {
91         let address = location.address;
92         this.address = new WifiGeoAddressObject(address.street_number || null,
93                                                 address.street || null,
94                                                 address.premises || null,
95                                                 address.city || null,
96                                                 address.county || null,
97                                                 address.region || null,
98                                                 address.country || null,
99                                                 address.country_code || null,
100                                                 address.postal_code || null);
101     }
102     else
103       this.address = null;
105     this.timestamp = Date.now();
108 WifiGeoPositionObject.prototype = {
110     QueryInterface:   XPCOMUtils.generateQI([Ci.nsIDOMGeoPosition, Ci.nsIClassInfo]),
112     // Class Info is required to be able to pass objects back into the DOM.
113     getInterfaces: function(countRef) {
114         var interfaces = [Ci.nsIDOMGeoPosition, Ci.nsIClassInfo, Ci.nsISupports];
115         countRef.value = interfaces.length;
116         return interfaces;
117     },
119     getHelperForLanguage: function(language) null,
120     classDescription: "wifi geo location position object",
121     implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
122     flags: Ci.nsIClassInfo.DOM_OBJECT,
124     coords: null,
125     timestamp: 0,
128 function HELD() {};
129  // For information about the HELD format, see:
130  // http://tools.ietf.org/html/draft-thomson-geopriv-held-measurements-05
131 HELD.encode = function(requestObject) {
132     // XML Header
133     var requestString = "<locationRequest xmlns=\"urn:ietf:params:xml:ns:geopriv:held\">";
134     // Measurements
135     if (requestObject.wifi_towers && requestObject.wifi_towers.length > 0) {
136       requestString += "<measurements xmlns=\"urn:ietf:params:xml:ns:geopriv:lm\">";
137       requestString += "<wifi xmlns=\"urn:ietf:params:xml:ns:geopriv:lm:wifi\">";
138       for (var i=0; i < requestObject.wifi_towers.length; ++i) {
139         requestString += "<neighbourWap>";
140         requestString += "<bssid>" + requestObject.wifi_towers[i].mac_address     + "</bssid>";
141         requestString += "<ssid>"  + requestObject.wifi_towers[i].ssid            + "</ssid>";
142         requestString += "<rssi>"  + requestObject.wifi_towers[i].signal_strength + "</rssi>";
143         requestString += "</neighbourWap>";
144       }
145       // XML Footer
146       requestString += "</wifi></measurements>";
147     }
148     requestString += "</locationRequest>";
149     return requestString;
152 // Decode a HELD response into a Gears-style object
153 HELD.decode = function(responseXML) {
154     // Find a Circle object in PIDF-LO and decode
155     function nsResolver(prefix) {
156         var ns = {
157             'held': 'urn:ietf:params:xml:ns:geopriv:held',
158             'pres': 'urn:ietf:params:xml:ns:pidf',
159             'gp': 'urn:ietf:params:xml:ns:pidf:geopriv10',
160             'gml': 'http://www.opengis.net/gml',
161             'gs': 'http://www.opengis.net/pidflo/1.0',
162         };
163         return ns[prefix] || null;
164     }
166     var xpathEval = Components.classes["@mozilla.org/dom/xpath-evaluator;1"].createInstance(Ci.nsIDOMXPathEvaluator);
168     // Grab values out of XML via XPath
169     var pos = xpathEval.evaluate(
170         '/held:locationResponse/pres:presence/pres:tuple/pres:status/gp:geopriv/gp:location-info/gs:Circle/gml:pos',
171         responseXML,
172         nsResolver,
173         Ci.nsIDOMXPathResult.STRING_TYPE,
174         null);
176     var rad = xpathEval.evaluate(
177         '/held:locationResponse/pres:presence/pres:tuple/pres:status/gp:geopriv/gp:location-info/gs:Circle/gs:radius',
178         responseXML,
179         nsResolver,
180         Ci.nsIDOMXPathResult.NUMBER_TYPE,
181         null );
183     var uom = xpathEval.evaluate(
184         '/held:locationResponse/pres:presence/pres:tuple/pres:status/gp:geopriv/gp:location-info/gs:Circle/gs:radius/@uom',
185         responseXML,
186         nsResolver,
187         Ci.nsIDOMXPathResult.STRING_TYPE,
188         null);
190     // Bail if we don't have a valid result (all values && uom==meters)
191     if ((pos.stringValue == null) ||
192         (rad.numberValue == null) ||
193         (uom.stringValue == null) ||
194         (uom.stringValue != "urn:ogc:def:uom:EPSG::9001")) {
195         return null;
196     }
198     // Split the pos value into lat/long
199     var coords = pos.stringValue.split(/[ \t\n]+/);
201     // Fill out the object to return:
202     var obj = {
203         location: {
204             latitude: parseFloat(coords[0]),
205             longitude: parseFloat(coords[1]),
206             accuracy: rad.numberValue
207         }
208     };
209     return obj;
210 }  
212 function WifiGeoPositionProvider() {
213     this.prefService = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch).QueryInterface(Ci.nsIPrefService);
214     try {
215         gLoggingEnabled = this.prefService.getBoolPref("geo.wifi.logging.enabled");
216     } catch (e) {}
218     try {
219         gTestingEnabled = this.prefService.getBoolPref("geo.wifi.testing");
220     } catch (e) {}
224 WifiGeoPositionProvider.prototype = {
225     classID:          Components.ID("{77DA64D3-7458-4920-9491-86CC9914F904}"),
226     QueryInterface:   XPCOMUtils.generateQI([Ci.nsIGeolocationProvider,
227                                              Ci.nsIWifiListener,
228                                              Ci.nsITimerCallback]),
230     prefService:     null,
231     wifi_service:    null,
232     timer:           null,
233     hasSeenWiFi:     false,
234     started:         false,
236     startup:         function() {
237         if (this.started == true)
238             return;
240         this.started = true;
242         LOG("startup called.  testing mode is" + gTestingEnabled);
243         // if we don't see anything in 5 seconds, kick of one IP geo lookup.
244         // if we are testing, just hammer this callback so that we are more or less
245         // always sending data.  It doesn't matter if we have an access point or not.
246         this.hasSeenWiFi = false;
247         this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
248         if (gTestingEnabled == false)
249             this.timer.initWithCallback(this, 5000, this.timer.TYPE_ONE_SHOT);
250         else
251             this.timer.initWithCallback(this, 200, this.timer.TYPE_REPEATING_SLACK);
252     },
254     watch: function(c) {
255         LOG("watch called");
256         if (!this.wifi_service) {
257             this.wifi_service = Cc["@mozilla.org/wifi/monitor;1"].getService(Components.interfaces.nsIWifiMonitor);
258             this.wifi_service.startWatching(this);
259         }
260     },
262     shutdown: function() { 
263         LOG("shutdown  called");
264         if(this.wifi_service)
265             this.wifi_service.stopWatching(this);
266         this.wifi_service = null;
268         if (this.timer != null) {
269             this.timer.cancel();
270             this.timer = null;
271         }
273         // Although we aren't using cookies, we should err on the side of not
274         // saving any access tokens if the user asked us not to save cookies or
275         // has changed the lifetimePolicy.  The access token in these cases is
276         // used and valid for the life of this object (eg. between startup and
277         // shutdown).e
278         let prefBranch = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
279         if (prefBranch.getIntPref("network.cookie.lifetimePolicy") != 0)
280             prefBranch.deleteBranch("geo.wifi.access_token.");
282         this.started = false;
283     },
285     getAccessTokenForURL: function(url)
286     {
287         // check to see if we have an access token:
288         var accessToken = "";
289         
290         try {
291             var accessTokenPrefName = "geo.wifi.access_token." + url;
292             accessToken = this.prefService.getCharPref(accessTokenPrefName);
293             
294             // check to see if it has expired
295             var accessTokenDate = this.prefService.getIntPref(accessTokenPrefName + ".time");
296             
297             var accessTokenInterval = 1209600;  /* seconds in 2 weeks */
298             try {
299                 accessTokenInterval = this.prefService.getIntPref("geo.wifi.access_token.recycle_interval");
300             } catch (e) {}
301             
302             if (nowInSeconds() - accessTokenDate > accessTokenInterval)
303                 accessToken = "";
304         }
305         catch (e) {
306             accessToken = "";
307         }
308         return accessToken;
309     },
311     onChange: function(accessPoints) {
313         LOG("onChange called");
314         this.hasSeenWiFi = true;
316         // send our request to a wifi geolocation network provider:
317         var xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
318                             .createInstance(Ci.nsIXMLHttpRequest);
320         // This is a background load
321         xhr.mozBackgroundRequest = true;
323         var provider_url      = this.prefService.getCharPref("geo.wifi.uri");
324         var provider_protocol = 0;
325         try {
326             provider_protocol = this.prefService.getIntPref("geo.wifi.protocol");
327         } catch (e) {}
329         LOG("provider url = " + provider_url);
331         xhr.open("POST", provider_url, false);
332         
333         // set something so that we can strip cookies
334         xhr.channel.loadFlags = Ci.nsIChannel.LOAD_ANONYMOUS;
335             
336         xhr.onerror = function(req) {
337             LOG("onerror: " + req);
338         };
340         xhr.onload = function (req) {  
342             LOG("xhr onload...");
344             try { 
345                 // if we get a bad response, we will throw and never report a location
346                 var response;
347                 switch (provider_protocol) {
348                     case 1:
349                         LOG("service returned: " + req.target.responseXML);
350                         response = HELD.decode(req.target.responseXML);
351                         break;
352                     case 0:
353                     default:
354                         LOG("service returned: " + req.target.responseText);
355                         response = JSON.parse(req.target.responseText);
356                 }
357             } catch (e) {
358                 LOG("Parse failed");
359                 return;
360             }
362             // response looks something like:
363             // {"location":{"latitude":51.5090332,"longitude":-0.1212726,"accuracy":150.0},"access_token":"2:jVhRZJ-j6PiRchH_:RGMrR0W1BiwdZs12"}
365             // Check to see if we have a new access token
366             var newAccessToken = response.access_token;
367             if (newAccessToken != undefined)
368             {
369                 var prefService = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
370                 var accessToken = "";
371                 var accessTokenPrefName = "geo.wifi.access_token." + req.target.channel.URI.spec;
372                 try { accessToken = prefService.getCharPref(accessTokenPrefName); } catch (e) {}
374                 if (accessToken != newAccessToken) {
375                     // no match, lets cache
376                     LOG("New Access Token: " + newAccessToken + "\n" + accessTokenPrefName);
377                     
378                     try {
379                         prefService.setIntPref(accessTokenPrefName + ".time", nowInSeconds());
380                         prefService.setCharPref(accessTokenPrefName, newAccessToken);
381                     } catch (x) {
382                         // XXX temporary hack for bug 575346 to allow geolocation to function
383                     }
384                 }
385             }
387             if (response.location) {
388                 var newLocation = new WifiGeoPositionObject(response.location);
390                 var update = Cc["@mozilla.org/geolocation/service;1"].getService(Ci.nsIGeolocationUpdate);
391                 update.update(newLocation);
392             }
393         };
395         var accessToken = this.getAccessTokenForURL(provider_url);
397         var request = {
398             version: "1.1.0",
399             request_address: true,
400         };
402         if (accessToken != "")
403             request.access_token = accessToken;
405         if (accessPoints != null) {
406             function filterBlankSSIDs(ap) ap.ssid != ""
407             function deconstruct(ap) ({
408                     mac_address: ap.mac,
409                         ssid: ap.ssid,
410                         signal_strength: ap.signal
411                         })
412             request.wifi_towers = accessPoints.filter(filterBlankSSIDs).map(deconstruct);
413         }
415         var requestString;
416         switch (provider_protocol) {
417           case 1:
418               requestString = HELD.encode(request);
419               break;
420           case 0:
421           default:
422               requestString = JSON.stringify(request);
423         }
424         LOG("client sending: " + requestString);
426         try {
427           xhr.send(requestString);
428         } catch (e) {}
429     },
431     onError: function (code) {
432         LOG("wifi error: " + code);
433     },
435     notify: function (timer) {
436         if (!gTestingEnabled) {
437             if (this.hasSeenWiFi == false)
438                 this.onChange(null);
439             this.timer = null;
440             return;
441         }
442         // if we are testing, we need to hammer this.
443         this.onChange(null);
444     },
448 var NSGetFactory = XPCOMUtils.generateNSGetFactory([WifiGeoPositionProvider]);