diff --git a/src/js/common/browser.js b/src/js/common/browser.js index cfc5b2f..0cb53c5 100644 --- a/src/js/common/browser.js +++ b/src/js/common/browser.js @@ -35,10 +35,9 @@ Browser._main_script = function() { // some operations cannot be done by other scripts, so we set // handlers to do them in the main script // - Browser.rpc.register('refreshIcon', function(tabId, callerTabId, replyHandler) { + Browser.rpc.register('refreshIcon', async function(tabId, callerTabId) { // 'self' tabId in the content script means refresh its own tab - Browser.gui.refreshIcon(tabId == 'self' ? callerTabId : tabId, replyHandler); - return true; // will reply later + await Browser.gui.refreshIcon(tabId == 'self' ? callerTabId : tabId); }); Browser.rpc.register('closeTab', function(tabId) { @@ -98,19 +97,28 @@ Browser.rpc._listener = function(message, sender, replyHandler) { // add tabId and replyHandler to the arguments var args = message.args || []; var tabId = sender.tab ? sender.tab.id : null; - args.push(tabId, replyHandler); - - return handler.apply(null, args); + args.push(tabId); + + const res = handler.apply(null, args); + if(res instanceof Promise) { + // handler returned a Promise. We return true here, to indicate that the response will come later. + res.then(replyHandler); + return true; + } else { + // We have a response right now + replyHandler(res); + return false; + } }; -Browser.rpc.call = function(tabId, name, args, cb) { - var message = { method: name, args: args }; - if(!cb) cb = function() {}; // we get error of not cb is passed - - if(tabId) - browser.tabs.sendMessage(tabId, message, cb); - else - browser.runtime.sendMessage(null, message, cb); +Browser.rpc.call = async function(tabId, name, args) { + return new Promise(resolve => { + var message = { method: name, args: args }; + if(tabId) + browser.tabs.sendMessage(tabId, message, resolve); + else + browser.runtime.sendMessage(null, message, resolve); + }); } @@ -124,35 +132,41 @@ Browser.rpc.call = function(tabId, name, args, cb) { // Browser.storage._key = "global"; // store everything under this key -Browser.storage.get = function(cb) { - browser.storage.local.get(Browser.storage._key, function(items) { - var st = items[Browser.storage._key]; - - // default values - if(!st) { - st = Browser.storage._default; - Browser.storage.set(st); - } - cb(st); +Browser.storage.get = async function() { + return new Promise(resolve => { + browser.storage.local.get(Browser.storage._key, function(items) { + var st = items[Browser.storage._key]; + + // default values + if(!st) { + st = Browser.storage._default; + Browser.storage.set(st); + } + resolve(st); + }); }); }; -Browser.storage.set = function(st, handler) { - Browser.log('saving st', st); - var items = {}; - items[Browser.storage._key] = st; - browser.storage.local.set(items, handler); +Browser.storage.set = async function(st) { + return new Promise(resolve => { + Browser.log('saving st', st); + var items = {}; + items[Browser.storage._key] = st; + browser.storage.local.set(items, resolve); + }); }; -Browser.storage.clear = function(handler) { - browser.storage.local.clear(handler); +Browser.storage.clear = async function() { + return new Promise(resolve => { + browser.storage.local.clear(resolve); + }); }; //////////////////// gui /////////////////////////// // // -Browser.gui.refreshIcon = function(tabId, cb) { +Browser.gui.refreshIcon = async function(tabId) { // delegate the call to the 'main' script if: // - we're in 'content': browser.pageAction/browserAction is not available there // - we use the FF pageAction workaround: we need to update Browser.gui.iconShown in 'main' @@ -160,16 +174,15 @@ Browser.gui.refreshIcon = function(tabId, cb) { if(Browser._script == 'content' || (Browser._script != 'main' && Browser.capabilities.needsPAManualHide()) ) { - Browser.rpc.call(null, 'refreshIcon', [tabId], cb); + await Browser.rpc.call(null, 'refreshIcon', [tabId]); return; } - Util.getIconInfo(tabId, function(info) { - if(Browser.capabilities.permanentIcon()) - Browser.gui._refreshBrowserAction(tabId, info, cb); - else - Browser.gui._refreshPageAction(tabId, info, cb); - }); + const info = await Util.getIconInfo(tabId); + if(Browser.capabilities.permanentIcon()) + await Browser.gui._refreshBrowserAction(tabId, info); + else + await Browser.gui._refreshPageAction(tabId, info); }; Browser.gui._icons = function(private) { @@ -180,24 +193,22 @@ Browser.gui._icons = function(private) { return ret; } -Browser.gui._refreshPageAction = function(tabId, info, cb) { +Browser.gui._refreshPageAction = function(tabId, info) { if(info.hidden || info.apiCalls == 0) { browser.pageAction.hide(tabId); - if(cb) cb(); return; } - if(Browser.gui.iconShown) - Browser.gui.iconShown[tabId] = 1; + return new Promise(resolve => { + if(Browser.gui.iconShown) + Browser.gui.iconShown[tabId] = 1; - browser.pageAction.setPopup({ - tabId: tabId, - popup: "popup.html?tabId=" + tabId // pass tabId in the url - }); - browser.pageAction.show(tabId); + browser.pageAction.setPopup({ + tabId: tabId, + popup: "popup.html?tabId=" + tabId // pass tabId in the url + }); + browser.pageAction.show(tabId); - // Firefox on Android (version 56) doesn't support pageAction.setIcon/setTitle so we try/catch - try { browser.pageAction.setTitle({ tabId: tabId, title: info.title @@ -205,47 +216,47 @@ Browser.gui._refreshPageAction = function(tabId, info, cb) { browser.pageAction.setIcon({ tabId: tabId, path: Browser.gui._icons(info.private) - }, cb); // setIcon is the only pageAction.set* method with a callback - } catch(e) { - if(cb) cb(); - } + }, resolve); // setIcon is the only pageAction.set* method with a callback + }); } -Browser.gui._refreshBrowserAction = function(tabId, info, cb) { - browser.browserAction.setTitle({ - tabId: tabId, - title: info.title - }); - browser.browserAction.setBadgeText({ - tabId: tabId, - text: (info.apiCalls || "").toString() - }); - browser.browserAction.setBadgeBackgroundColor({ - tabId: tabId, - color: "#b12222" - }); - browser.browserAction.setPopup({ - tabId: tabId, - popup: "popup.html" + (tabId ? "?tabId="+tabId : "") // pass tabId in the url +Browser.gui._refreshBrowserAction = function(tabId, info) { + return new Promise(resolve => { + browser.browserAction.setTitle({ + tabId: tabId, + title: info.title + }); + browser.browserAction.setBadgeText({ + tabId: tabId, + text: (info.apiCalls || "").toString() + }); + browser.browserAction.setBadgeBackgroundColor({ + tabId: tabId, + color: "#b12222" + }); + browser.browserAction.setPopup({ + tabId: tabId, + popup: "popup.html" + (tabId ? "?tabId="+tabId : "") // pass tabId in the url + }); + browser.browserAction.setIcon({ + tabId: tabId, + path: Browser.gui._icons(info.private) + }, resolve); // setIcon is the only browserAction.set* method with a callback }); - browser.browserAction.setIcon({ - tabId: tabId, - path: Browser.gui._icons(info.private) - }, cb); // setIcon is the only browserAction.set* method with a callback } -Browser.gui.refreshAllIcons = function(cb) { - browser.tabs.query({}, function(tabs) { - // for browser action, also refresh default state (null tabId) - if(Browser.capabilities.permanentIcon()) - tabs.push({ id: null }); - - var done = 0; - for(var i = 0; i < tabs.length; i++) - Browser.gui.refreshIcon(tabs[i].id, function() { - if(++done == tabs.length && cb) - cb(); - }); +Browser.gui.refreshAllIcons = async function() { + return new Promise(resolve => { + browser.tabs.query({}, async function(tabs) { + // for browser action, also refresh default state (null tabId) + if(Browser.capabilities.permanentIcon()) + tabs.push({ id: null }); + + for(var i = 0; i < tabs.length; i++) + await Browser.gui.refreshIcon(tabs[i].id); + + resolve(); + }); }); }; @@ -253,24 +264,24 @@ Browser.gui.showPage = function(name) { browser.tabs.create({ url: browser.extension.getURL(name) }); }; -Browser.gui.getCallUrl = function(tabId, handler) { - function fetch(tabId) { - // we call getState from the content script - // - Browser.rpc.call(tabId, 'getState', [], function(state) { - handler(state && state.callUrl); // state might be null if no content script runs in the tab - }); +Browser.gui.getCallUrl = async function(tabId) { + async function getActiveTabId() { + return new Promise(resolve => { + browser.tabs.query({ + active: true, // Select active tabs + lastFocusedWindow: true // In the current window + }, async function(tabs) { + resolve(tabs[0].id); + }); + }) } - if(tabId) - fetch(tabId); - else - browser.tabs.query({ - active: true, // Select active tabs - lastFocusedWindow: true // In the current window - }, function(tabs) { - fetch(tabs[0].id) - }); + if(!tabId) + tabId = await getActiveTabId(); + + // we call getState from the content script + const state = await Browser.rpc.call(tabId, 'getState', []); + return state && state.callUrl; // state might be null if no content script runs in the tab }; Browser.gui.closePopup = function() { diff --git a/src/js/common/browser_base.js b/src/js/common/browser_base.js index 2b2f6ae..3c41241 100644 --- a/src/js/common/browser_base.js +++ b/src/js/common/browser_base.js @@ -30,29 +30,24 @@ const Browser = { // // Registers a method to be callable from other scripts. // handler should be a function - // function(...args..., tabId, replyHandler) + // async function(...args..., tabId) // // The function receives any arguments passed during the call (see Browser.rpc.call) - // Moreover, two extra arguments are automatically added: + // Moreover, one extra arguments are automatically added: // tabId: the tabId of the caller, or null if the call is made from the main script - // replyHandler: function for asynchronously returning a result by calling replyHandler(result) - // - // IMPORTANT: If handler does not immediately return a result but stores replyHandler to do it asynchronously later, - // it should return a true value to keep replyHandler open. // register: function(name, handler) {}, - // Browser.rpc.call(tabId, name, args, handler) + // Browser.rpc.call(tabId, name, args) // // Calls a remote method. // tabId: tab id of the script to call, or null to call the main script // name: method name // args: array of arguments to pass - // handler: function(res), will be called when the result is received // - // If the call cannot be made to the specific tabId, handler will be called with no arguments. + // If the call cannot be made to the specific tabId, null is returned. // - call: function(tabId, name, args, handler) {} + call: async function(tabId, name, args) {} }, // Browser.storage @@ -62,24 +57,24 @@ const Browser = { // else that needs to be stored. It is fetched and stored as a whole. // storage: { - // browser.storage.get(handler) + // browser.storage.get() // - // fetches the storage object and passes it to the handler. + // fetches the storage object. // The default object is returned if the storage is empty. // - get: function(handler) {}, + get: async function() {}, - // browser.storage.set(st, handler) + // browser.storage.set(st) // - // Stores the give storage object. Calls the handler when finished. + // Stores the give storage object. // - set: function(st, handler) {}, + set: async function(st) {}, - // browser.storage.clear(handler) + // browser.storage.clear() // - // Clears the storage. Calls the handler when finished. + // Clears the storage. // - clear: function(handler) {}, + clear: async function() {}, // default storage object // @@ -135,14 +130,14 @@ const Browser = { // If called from a content script and tabId = 'self' it refreshes the icon of the content script's tab. // getIconInfo should be called to get the icon's info // - refreshIcon: function(tabId) {}, + refreshIcon: async function(tabId) {}, // Browser.gui.refreshAllIcons() // // Refreshes the icons of all tabs. // getIconInfo should be called to get the icon's info // - refreshAllIcons: function() {}, + refreshAllIcons: async function() {}, // Browser.gui.showPage(name) // @@ -151,11 +146,11 @@ const Browser = { // showPage: function(name) {}, - // Browser.gui.getCallUrl(tabId, handler) + // Browser.gui.getCallUrl(tabId) // - // Gets the callUrl of given tab and passes it to 'handler' + // Gets the callUrl of given tab. // - getActiveCallUrl: function(tabId, handler) {}, + getCallUrl: async function(tabId) {}, // Browser.gui.closePopup() // diff --git a/src/js/common/post-rpc.js b/src/js/common/post-rpc.js index d0f6573..d95890d 100644 --- a/src/js/common/post-rpc.js +++ b/src/js/common/post-rpc.js @@ -24,15 +24,14 @@ function _code() { // include all code here to inject easily PostRPC.prototype.register = function(name, fun) { this._methods[name] = fun; }; - PostRPC.prototype.call = function(method, args, handler) { - var callId; - if(handler) { - callId = Math.floor(Math.random()*1000000); - this._calls[callId] = handler; - } - if(!args) args = []; + PostRPC.prototype.call = function(method, args) { + return new Promise(resolve => { + var callId = Math.floor(Math.random()*1000000); + this._calls[callId] = resolve; + if(!args) args = []; - this._sendMessage({ method: method, args: args, callId: callId, from: this._id }); + this._sendMessage({ method: method, args: args, callId: callId, from: this._id }); + }); }; // private methods for sending/receiving messages @@ -43,7 +42,7 @@ function _code() { // include all code here to inject easily this._sendObj.postMessage(temp, this._targetOrigin); } - PostRPC.prototype._receiveMessage = function(event) { + PostRPC.prototype._receiveMessage = async function(event) { var data = event.data && event.data[this._ns]; // everything is inside ns, to minimize conflicts with other message if(!data) return; @@ -56,22 +55,9 @@ function _code() { // include all code here to inject easily return; } - // pass returnHandler, used to send back the result - var replyHandler; - if(data.callId) { - var _this = this; - replyHandler = function() { - var args = Array.prototype.slice.call(arguments); // arguments in real array - _this._sendMessage({ callId: data.callId, value: args }); - }; - } else { - replyHandler = function() {}; // no result expected, use dummy handler - } - - var dataArgs = Array.prototype.slice.call(data.args); // cannot modify data.args in Firefox 32, clone as workaround - dataArgs.push(replyHandler); - - this._methods[data.method].apply(null, dataArgs); + // call the handler, send back the result + const res = await this._methods[data.method].apply(null, data.args); + this._sendMessage({ callId: data.callId, value: [res] }); } else { // return value diff --git a/src/js/common/util.js b/src/js/common/util.js index 2bba44b..db8e875 100644 --- a/src/js/common/util.js +++ b/src/js/common/util.js @@ -41,40 +41,37 @@ var Util = { // title: icon's title } // // - getIconInfo: function(about, handler) { - if(typeof(about) == 'object') // null or state object - Util._getStateIconInfo(about, handler); - else { // tabId + getIconInfo: async function(about) { + if(typeof(about) != 'object') { // tabId const Browser = require('./browser'); - Browser.rpc.call(about, 'getState', [], function(state) { - Util._getStateIconInfo(state, handler); - }); + about = await Browser.rpc.call(about, 'getState', []); } + + return await Util._getStateIconInfo(about); }, - _getStateIconInfo: function(state, handler) { + _getStateIconInfo: async function(state) { // return info for the default icon if state is null state = state || { callUrl: '', apiCalls: 0 }; const Browser = require('./browser'); - Browser.storage.get(function(st) { - var domain = Util.extractDomain(state.callUrl); - var level = st.domainLevel[domain] || st.defaultLevel; + const st = await Browser.storage.get(); + var domain = Util.extractDomain(state.callUrl); + var level = st.domainLevel[domain] || st.defaultLevel; - var info = { - hidden: st.hideIcon, - private: !st.paused && level != 'real', - defaultPrivate: !st.paused && st.defaultLevel != 'real', - apiCalls: state.apiCalls, - title: - "Location Guard\n" + - (st.paused ? "Paused" : - level == 'real' ? "Using your real location" : - level == 'fixed'? "Using a fixed location" : - "Privacy level: " + level) - }; - handler(info); - }); + var info = { + hidden: st.hideIcon, + private: !st.paused && level != 'real', + defaultPrivate: !st.paused && st.defaultLevel != 'real', + apiCalls: state.apiCalls, + title: + "Location Guard\n" + + (st.paused ? "Paused" : + level == 'real' ? "Using your real location" : + level == 'fixed'? "Using a fixed location" : + "Privacy level: " + level) + }; + return info; }, events: { diff --git a/src/js/content/content.js b/src/js/content/content.js index 38d4de8..8ac9308 100644 --- a/src/js/content/content.js +++ b/src/js/content/content.js @@ -74,69 +74,78 @@ var callUrl = myUrl; // the url from which the last call is _shown_ to be ma // methods called by the page // var rpc = new PostRPC('page-content', window, window, window.origin); -rpc.register('getNoisyPosition', function(options, replyHandler) { +rpc.register('getNoisyPosition', async function(options) { + callUrl = myUrl; // last call happened here if(inFrame) { // we're in a frame, we need to notify the top window, and get back the *url used in the permission dialog* // (which might be either the iframe url, or the top window url, depending on how the browser handles permissions). // To avoid cross-origin issues, we call apiCalledInFrame in the main script, which echoes the // call back to this tab to be answered by the top window - Browser.rpc.call(null, 'apiCalledInFrame', [myUrl], function(topUrl) { - callUrl = Browser.capabilities.iframeGeoFromOwnDomain() ? myUrl : topUrl; - getNoisyPosition(options, replyHandler); - }); - + if(!Browser.capabilities.iframeGeoFromOwnDomain()) + callUrl = await Browser.rpc.call(null, 'apiCalledInFrame', [myUrl]); } else { // refresh icon before fetching the location apiCalls++; - callUrl = myUrl; // last call happened here Browser.gui.refreshIcon('self'); - getNoisyPosition(options, replyHandler); } - return true; // will reply later + return await getNoisyPosition(options); +}); +rpc.register('watchAllowed', async function(firstCall) { + // Returns true if using the real watch is allowed. Only if paused or level == 'real'. + // Also don't allow in iframes (to simplify the code). + const st = await Browser.storage.get(); + var level = st.domainLevel[Util.extractDomain(myUrl)] || st.defaultLevel; + var allowed = !inFrame && (st.paused || level == 'real'); + + if(allowed && !firstCall) { + apiCalls++; + Browser.gui.refreshIcon('self'); + } + return allowed; }); // gets the options passed to the fake navigator.geolocation.getCurrentPosition. // Either returns fixed pos directly, or calls the real one, then calls addNoise. // -function getNoisyPosition(options, replyHandler) { - Browser.storage.get(function(st) { - // if level == 'fixed' and fixedPosNoAPI == true, then we return the - // fixed position without calling the geolocation API at all. - // - var domain = Util.extractDomain(callUrl); - var level = st.domainLevel[domain] || st.defaultLevel; - - if(!st.paused && level == 'fixed' && st.fixedPosNoAPI) { - var noisy = { - coords: { - latitude: st.fixedPos.latitude, - longitude: st.fixedPos.longitude, - accuracy: 10, - altitude: null, - altitudeAccuracy: null, - heading: null, - speed: null - }, - timestamp: new Date().getTime() - }; - replyHandler(true, noisy); - Browser.log("returning fixed", noisy); - return; - } +async function getNoisyPosition(options) { + const st = await Browser.storage.get(); + + // if level == 'fixed' and fixedPosNoAPI == true, then we return the + // fixed position without calling the geolocation API at all. + // + var domain = Util.extractDomain(callUrl); + var level = st.domainLevel[domain] || st.defaultLevel; + if(!st.paused && level == 'fixed' && st.fixedPosNoAPI) { + var noisy = { + coords: { + latitude: st.fixedPos.latitude, + longitude: st.fixedPos.longitude, + accuracy: 10, + altitude: null, + altitudeAccuracy: null, + heading: null, + speed: null + }, + timestamp: new Date().getTime() + }; + Browser.log("returning fixed", noisy); + return { success: true, position: noisy }; + } + + return new Promise(resolve => { // we call getCurrentPosition here in the content script, instead of // inside the page, because the content-script/page communication is not secure // getCurrentPosition.apply(navigator.geolocation, [ - function(position) { + async function(position) { // clone, modifying/sending the native object returns error - addNoise(Util.clone(position), function(noisy) { - replyHandler(true, noisy); - }); + const noisy = await addNoise(Util.clone(position)); + resolve({ success: true, position: noisy }); }, function(error) { - replyHandler(false, Util.clone(error)); // clone, sending the native object returns error + resolve({ success: false, position: Util.clone(error) }); // clone, sending the native object returns error }, options ]); @@ -145,105 +154,104 @@ function getNoisyPosition(options, replyHandler) { // gets position, returs noisy version based on the privacy options // -function addNoise(position, handler) { - Browser.storage.get(function(st) { - var domain = Util.extractDomain(callUrl); - var level = st.domainLevel[domain] || st.defaultLevel; - - if(st.paused || level == 'real') { - // do nothing, use real location - - } else if(level == 'fixed') { - position.coords = { - latitude: st.fixedPos.latitude, - longitude: st.fixedPos.longitude, - accuracy: 10, - altitude: null, - altitudeAccuracy: null, - heading: null, - speed: null - }; - - } else if(st.cachedPos[level] && ((new Date).getTime() - st.cachedPos[level].epoch)/60000 < st.levels[level].cacheTime) { - position = st.cachedPos[level].position; - Browser.log('using cached', position); +async function addNoise(position) { + const st = await Browser.storage.get(); + var domain = Util.extractDomain(callUrl); + var level = st.domainLevel[domain] || st.defaultLevel; + + if(st.paused || level == 'real') { + // do nothing, use real location + + } else if(level == 'fixed') { + position.coords = { + latitude: st.fixedPos.latitude, + longitude: st.fixedPos.longitude, + accuracy: 10, + altitude: null, + altitudeAccuracy: null, + heading: null, + speed: null + }; + + } else if(st.cachedPos[level] && ((new Date).getTime() - st.cachedPos[level].epoch)/60000 < st.levels[level].cacheTime) { + position = st.cachedPos[level].position; + Browser.log('using cached', position); - } else { - // add noise - var epsilon = st.epsilon / st.levels[level].radius; + } else { + // add noise + var epsilon = st.epsilon / st.levels[level].radius; - const PlanarLaplace = require('../common/laplace'); - var pl = new PlanarLaplace(); - var noisy = pl.addNoise(epsilon, position.coords); + const PlanarLaplace = require('../common/laplace'); + var pl = new PlanarLaplace(); + var noisy = pl.addNoise(epsilon, position.coords); - position.coords.latitude = noisy.latitude; - position.coords.longitude = noisy.longitude; + position.coords.latitude = noisy.latitude; + position.coords.longitude = noisy.longitude; - // update accuracy - if(position.coords.accuracy && st.updateAccuracy) - position.coords.accuracy += Math.round(pl.alphaDeltaAccuracy(epsilon, .9)); + // update accuracy + if(position.coords.accuracy && st.updateAccuracy) + position.coords.accuracy += Math.round(pl.alphaDeltaAccuracy(epsilon, .9)); - // don't know how to add noise to those, so we set to null (they're most likely null anyway) - position.altitude = null; - position.altitudeAccuracy = null; - position.heading = null; - position.speed = null; + // don't know how to add noise to those, so we set to null (they're most likely null anyway) + position.altitude = null; + position.altitudeAccuracy = null; + position.heading = null; + position.speed = null; - // cache - st.cachedPos[level] = { epoch: (new Date).getTime(), position: position }; - Browser.storage.set(st); + // cache + st.cachedPos[level] = { epoch: (new Date).getTime(), position: position }; + await Browser.storage.set(st); - Browser.log('noisy coords', position.coords); - } + Browser.log('noisy coords', position.coords); + } - // return noisy position - handler(position); - }); + // return noisy position + return position; } -Browser.init('content'); +(async function() { + Browser.init('content'); -// if a browser action (always visible button) is used, we need to refresh the -// icon immediately (before the API is called). HOWEVER: Browser.gui.refreshIcon -// causes the background script to be awaken. To avoid doing this on every page, -// we only call it if the icon is different than the default icon! -// -if(Browser.capabilities.permanentIcon() && !inFrame) { - Util.getIconInfo({ callUrl: myUrl, apiCalls: 0 }, function(info) { + // if a browser action (always visible button) is used, we need to refresh the + // icon immediately (before the API is called). HOWEVER: Browser.gui.refreshIcon + // causes the background script to be awaken. To avoid doing this on every page, + // we only call it if the icon is different than the default icon! + // + if(Browser.capabilities.permanentIcon() && !inFrame) { + const info = await Util.getIconInfo({ callUrl: myUrl, apiCalls: 0 }); if(info.private != info.defaultPrivate) // the icon for myUrl is different than the default Browser.gui.refreshIcon('self'); - }) -} + } -// only the top frame handles getState and apiCalledInFrame requests -if(!inFrame) { - Browser.rpc.register('getState', function(tabId, replyHandler) { - replyHandler({ - callUrl: callUrl, - apiCalls: apiCalls + // only the top frame handles getState and apiCalledInFrame requests + if(!inFrame) { + Browser.rpc.register('getState', function(tabId) { + return { + callUrl: callUrl, + apiCalls: apiCalls + }; }); - }); - Browser.rpc.register('apiCalledInFrame', function(iframeUrl, tabId, replyHandler) { - apiCalls++; - if(Browser.capabilities.iframeGeoFromOwnDomain()) - callUrl = iframeUrl; - Browser.gui.refreshIcon('self'); + Browser.rpc.register('apiCalledInFrame', function(iframeUrl, tabId) { + apiCalls++; + if(Browser.capabilities.iframeGeoFromOwnDomain()) + callUrl = iframeUrl; + Browser.gui.refreshIcon('self'); - replyHandler(myUrl); - }); -} + return myUrl; + }); + } -if(Browser.testing) { - // test for nested calls, and for correct passing of tabId - // - Browser.rpc.register('nestedTestTab', function(tabId, replyHandler) { - Browser.log("in nestedTestTab, returning 'content'"); - replyHandler("content"); - }); + if(Browser.testing) { + // test for nested calls, and for correct passing of tabId + // + Browser.rpc.register('nestedTestTab', function(tabId) { + Browser.log("in nestedTestTab, returning 'content'"); + return "content"; + }); - Browser.log("calling nestedTestMain"); - Browser.rpc.call(null, 'nestedTestMain', [], function(res) { + Browser.log("calling nestedTestMain"); + const res = await Browser.rpc.call(null, 'nestedTestMain', []); Browser.log('got from nestedTestMain', res); - }); -} + } +}()) \ No newline at end of file diff --git a/src/js/content/injected.js b/src/js/content/injected.js index fc53332..7bb5db2 100644 --- a/src/js/content/injected.js +++ b/src/js/content/injected.js @@ -4,34 +4,57 @@ module.exports = function(PostRPC) { if(navigator.geolocation) { // the geolocation API exists var prpc; - - // we replace geolocation methods with our own - // the real methods will be called by the content script (not by the page) - // so we dont need to keep them at all. - - navigator.geolocation.getCurrentPosition = function(cb1, cb2, options) { + function getPostRPC() { // create a PostRPC object only when getCurrentPosition is called. This // avoids having our own postMessage handler on every page if(!prpc) prpc = new PostRPC('page-content', window, window, window.origin); // This PostRPC is created by the injected code! + return prpc; + } + async function callCb(cb, pos, checkAllowed) { + if(cb && (!checkAllowed || await getPostRPC().call('watchAllowed', [false]))) + cb(pos); + } + // We replace geolocation methods with our own. + // getCurrentPosition will be called by the content script (not by the page) + // so we dont need to keep it at all. + + navigator.geolocation.getCurrentPosition = async function(cb1, cb2, options) { // call getNoisyPosition on the content-script - prpc.call('getNoisyPosition', [options], function(success, res) { - // call cb1 on success, cb2 on failure - var f = success ? cb1 : cb2; - if(f) f(res); - }); + // call cb1 on success, cb2 on failure + const res = await getPostRPC().call('getNoisyPosition', [options]); + callCb(res.success ? cb1 : cb2, res.position, false); }; + const watchPosition = navigator.geolocation.watchPosition; + const handlers = {}; navigator.geolocation.watchPosition = function(cb1, cb2, options) { - // we don't install a real watch, just return the position once - // TODO: implement something closer to a watch - this.getCurrentPosition(cb1, cb2, options); - return Math.floor(Math.random()*10000); // return random id, it's not really used + // We need to return a handler synchronously, but decide whether we'll use the real watchPosition or not + // asynchronously. So we create our own handler, and we'll associate it with the real one later. + const handler = Math.floor(Math.random()*10000); + + (async () => { + if(await getPostRPC().call('watchAllowed', [true])) + // We're allowed to call the real watchPosition (note: remember the handler) + handlers[handler] = watchPosition.apply(navigator.geolocation, [ + position => callCb(cb1, position, true), // ignore the call if privacy protection + error => callCb(cb2, error, true), // becomes active later! + options + ]); + else + // Not allowed, we don't install a real watch, just return the position once + this.getCurrentPosition(cb1, cb2, options); + })(); + return handler; }; - navigator.geolocation.clearWatch = function () { - // nothing to do + const clearWatch = navigator.geolocation.clearWatch; + navigator.geolocation.clearWatch = function (handler) { + if(handler in handlers) { + clearWatch.apply(navigator.geolocation, [handlers[handler]]); + delete handlers[handler]; + } }; } diff --git a/src/js/gui/demo.js b/src/js/gui/demo.js index 7c87cad..f224aff 100644 --- a/src/js/gui/demo.js +++ b/src/js/gui/demo.js @@ -7,7 +7,7 @@ require('leaflet.locatecontrol'); const Browser = require('../common/browser'); Browser.inDemo = true; // used in content.js -var intro; +var intro, steps; var demoMap; var showPressed, geoDone; @@ -76,7 +76,7 @@ function showCurrentPosition() { intro.goToStep(3); } -function drawPosition(pos) { +async function drawPosition(pos) { var latlng = [pos.coords.latitude, pos.coords.longitude]; var acc = pos.coords.accuracy; @@ -101,13 +101,25 @@ function drawPosition(pos) { demoMap.fitBounds(demoMap.accuracy.getBounds()); geoDone = true; - if(intro._currentStep + 1 <= 3) + if(intro._currentStep + 1 <= 3) { + const st = await Browser.storage.get(); + var level = st.paused ? 'real' : (st.domainLevel['demo-page'] || st.defaultLevel); + + var s = + level == 'fixed' ? 'Location Guard replaced it with your configured fixed location.' : + level == 'real' ? 'Location Guard did not modify it.' : + 'Location Guard added "noise" to it so that it\'s not very accurate.'; + + // hacky way to modify the step's message + intro._introItems[3].intro = steps[3].intro.replace("%s", s); + intro.goToStep(4); + } } function startDemo() { var iconClass = Browser.capabilities.permanentIcon() ? 'lg-icon-browseraction' : 'lg-icon-pageaction'; - var steps = [ { + steps = [ { element: ".placeholder-step1", intro: '
Location Guard was successfully installed.
This demo illustrates its use.
', position: "floating", @@ -133,25 +145,13 @@ function startDemo() { }, { element: ".placeholder-step4", intro: - 'This is the location disclosed by your browser. Location Guard added "noise" to it so that it\'s not ' + - 'very accurate.
Click on the icon to try more options.
', + 'This is the location disclosed by your browser. %s
Click on the icon to try more options.
', position: "bottom-middle-aligned", highlightClass: 'highlight-step4', tooltipClass: 'tooltip-step4', } ]; - // demo not working on Desktop Firefox 41 - if(~navigator.userAgent.indexOf('Firefox/41') && !~navigator.userAgent.indexOf('Android')) - steps = [{ - element: ".placeholder-step1", - intro: - 'Location Guard was successfully installed.
To see it in action, try this online demo.
' + - 'Note: the current page used to be a demo of Location Guard, but it was disabled in Firefox 41 due to a browser bug. ' +
- 'It will be available again in Firefox 42.
Location Guard, however, is still enabled on all pages!
This is the location reported when the privacy level is set to "Use fixed location".
' + - 'Click on the map or drag the marker to set a new fixed location.
' + - 'This is the location reported when the privacy level is set to "Use fixed location".
' + + 'Click on the map or drag the marker to set a new fixed location.
' + + '