From 5a1ed35be7cb9b260ec7b7389ba62b6afecc5aec Mon Sep 17 00:00:00 2001 From: Anant Narayanan Date: Sat, 2 Jun 2012 23:32:37 -0700 Subject: [PATCH] Bug 757261: Apps in the Cloud Manager and Service; r=mconnor --- browser/installer/package-manifest.in | 2 + services/aitc/Aitc.js | 121 ++++ services/aitc/AitcComponents.manifest | 4 +- services/aitc/Makefile.in | 2 +- services/aitc/modules/main.js | 161 +++++ services/aitc/modules/manager.js | 573 ++++++++++++++++++ services/aitc/service.js | 47 -- services/aitc/services-aitc.js | 18 +- services/aitc/tests/unit/test_load_modules.js | 2 + 9 files changed, 878 insertions(+), 52 deletions(-) create mode 100644 services/aitc/Aitc.js create mode 100644 services/aitc/modules/main.js create mode 100644 services/aitc/modules/manager.js delete mode 100644 services/aitc/service.js diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index 15e47b2d4f21..00c678d0c263 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -438,6 +438,7 @@ @BINPATH@/components/SyncComponents.manifest @BINPATH@/components/AitcComponents.manifest @BINPATH@/components/Weave.js +@BINPATH@/components/Aitc.js #endif @BINPATH@/components/TelemetryPing.js @BINPATH@/components/TelemetryPing.manifest @@ -512,6 +513,7 @@ @BINPATH@/@PREF_DIR@/firefox-branding.js #ifdef MOZ_SERVICES_SYNC @BINPATH@/@PREF_DIR@/services-sync.js +@BINPATH@/@PREF_DIR@/services-aitc.js #endif @BINPATH@/greprefs.js @BINPATH@/defaults/autoconfig/platform.js diff --git a/services/aitc/Aitc.js b/services/aitc/Aitc.js new file mode 100644 index 000000000000..ac9a6a42d995 --- /dev/null +++ b/services/aitc/Aitc.js @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/PlacesUtils.jsm"); + +Cu.import("resource://services-common/utils.js"); + +function AitcService() { + this.aitc = null; + this.wrappedJSObject = this; +} +AitcService.prototype = { + classID: Components.ID("{a3d387ca-fd26-44ca-93be-adb5fda5a78d}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsINavHistoryObserver, + Ci.nsISupportsWeakReference]), + + observe: function observe(subject, topic, data) { + switch (topic) { + case "app-startup": + // We listen for this event beacause Aitc won't work until there is + // atleast 1 visible top-level XUL window. + Services.obs.addObserver(this, "sessionstore-windows-restored", true); + break; + case "sessionstore-windows-restored": + Services.obs.removeObserver(this, "sessionstore-windows-restored"); + + // Don't start AITC if classic sync is on. + Cu.import("resource://services-common/preferences.js"); + if (Preferences.get("services.sync.engine.apps", false)) { + return; + } + // Start AITC only if it is enabled. + if (!Preferences.get("services.aitc.enabled", false)) { + return; + } + + // Start AITC service if apps.enabled is true. If false, we look + // in the browser history to determine if they're an "apps user". If + // an entry wasn't found, we'll watch for navigation to either the + // marketplace or dashboard and switch ourselves on then. + + if (Preferences.get("apps.enabled", false)) { + this.start(); + return; + } + + // Set commonly used URLs. + this.DASHBOARD_URL = CommonUtils.makeURI( + Preferences.get("services.aitc.dashboard.url") + ); + this.MARKETPLACE_URL = CommonUtils.makeURI( + Preferences.get("services.aitc.marketplace.url") + ); + + if (this.hasUsedApps()) { + Preferences.set("apps.enabled", true); + this.start(); + return; + } + + // Wait and see if the user wants anything apps related. + PlacesUtils.history.addObserver(this, true); + break; + } + }, + + start: function start() { + Cu.import("resource://services-aitc/main.js"); + if (!this.aitc) { + this.aitc = new Aitc(); + } + }, + + hasUsedApps: function hasUsedApps() { + // There is no easy way to determine whether a user is "using apps". + // The best we can do right now is to see if they have visited either + // the Mozilla dashboard or Marketplace. See bug 760898. + let gh = PlacesUtils.ghistory2; + if (gh.isVisited(this.DASHBOARD_URL)) { + return true; + } + if (gh.isVisited(this.MARKETPLACE_URL)) { + return true; + } + return false; + }, + + // nsINavHistoryObserver. We are only interested in onVisit(). + onBeforeDeleteURI: function() {}, + onBeginUpdateBatch: function() {}, + onClearHistory: function() {}, + onDeleteURI: function() {}, + onDeleteVisits: function() {}, + onEndUpdateBatch: function() {}, + onPageChanged: function() {}, + onPageExpired: function() {}, + onTitleChanged: function() {}, + + onVisit: function onVisit(uri) { + if (!uri.equals(this.MARKETPLACE_URL) && !uri.equals(this.DASHBOARD_URL)) { + return; + } + + PlacesUtils.history.removeObserver(this); + Preferences.set("apps.enabled", true); + this.start(); + return; + }, +}; + +const components = [AitcService]; +const NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/services/aitc/AitcComponents.manifest b/services/aitc/AitcComponents.manifest index 02ddd00bc95d..3f435ba7f1ac 100644 --- a/services/aitc/AitcComponents.manifest +++ b/services/aitc/AitcComponents.manifest @@ -1,5 +1,5 @@ -# service.js -component {a3d387ca-fd26-44ca-93be-adb5fda5a78d} service.js +# Aitc.js +component {a3d387ca-fd26-44ca-93be-adb5fda5a78d} Aitc.js contract @mozilla.org/services/aitc;1 {a3d387ca-fd26-44ca-93be-adb5fda5a78d} category app-startup AitcService service,@mozilla.org/services/aitc;1 # Register resource aliases diff --git a/services/aitc/Makefile.in b/services/aitc/Makefile.in index dc0b0b60635a..e336b2fc0478 100644 --- a/services/aitc/Makefile.in +++ b/services/aitc/Makefile.in @@ -11,7 +11,7 @@ include $(DEPTH)/config/autoconf.mk EXTRA_COMPONENTS = \ AitcComponents.manifest \ - service.js \ + Aitc.js \ $(NULL) PREF_JS_EXPORTS = $(srcdir)/services-aitc.js diff --git a/services/aitc/modules/main.js b/services/aitc/modules/main.js new file mode 100644 index 000000000000..81acff2e8ff4 --- /dev/null +++ b/services/aitc/modules/main.js @@ -0,0 +1,161 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EXPORTED_SYMBOLS = ["Aitc"]; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Webapps.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +Cu.import("resource://services-aitc/manager.js"); +Cu.import("resource://services-common/utils.js"); +Cu.import("resource://services-common/log4moz.js"); +Cu.import("resource://services-common/preferences.js"); + +function Aitc() { + this._log = Log4Moz.repository.getLogger("Service.AITC"); + this._log.level = Log4Moz.Level[Preferences.get( + "services.aitc.service.log.level" + )]; + this._log.info("Loading AitC"); + + this.DASHBOARD_ORIGIN = CommonUtils.makeURI( + Preferences.get("services.aitc.dashboard.url") + ).prePath; + + this._manager = new AitcManager(this._init.bind(this)); +} +Aitc.prototype = { + // The goal of the init function is to be ready to activate the AITC + // client whenever the user is looking at the dashboard. + _init: function init() { + let self = this; + + // This is called iff the user is currently looking the dashboard. + function dashboardLoaded(browser) { + let win = browser.contentWindow; + self._log.info("Dashboard was accessed " + win); + + // If page is ready to go, fire immediately. + if (win.document && win.document.readyState == "complete") { + self._manager.userActive(win); + return; + } + + // Only fire event after the page fully loads. + browser.contentWindow.addEventListener( + "DOMContentLoaded", + function _contentLoaded(event) { + self._manager.userActive(win); + }, + false + ); + } + + // This is called when the user's attention is elsewhere. + function dashboardUnloaded() { + self._log.info("Dashboard closed or in background"); + self._manager.userIdle(); + } + + // Called when a URI is loaded in any tab. We have to listen for this + // because tabSelected is not called if I open a new tab which loads + // about:home and then navigate to the dashboard, or navigation via + // links on the currently open tab. + let listener = { + onLocationChange: function onLocationChange(browser, pr, req, loc, flag) { + let win = Services.wm.getMostRecentWindow("navigator:browser"); + if (win.gBrowser.selectedBrowser == browser) { + if (loc.prePath == self.DASHBOARD_ORIGIN) { + dashboardLoaded(browser); + } + } + } + }; + // Called when the current tab selection changes. + function tabSelected(event) { + let browser = event.target.linkedBrowser; + if (browser.currentURI.prePath == self.DASHBOARD_ORIGIN) { + dashboardLoaded(browser); + } else { + dashboardUnloaded(); + } + } + + // Add listeners for all windows opened in the future. + function winWatcher(subject, topic) { + if (topic != "domwindowopened") { + return; + } + subject.addEventListener("load", function winWatcherLoad() { + subject.removeEventListener("load", winWatcherLoad, false); + let doc = subject.document.documentElement; + if (doc.getAttribute("windowtype") == "navigator:browser") { + let browser = subject.gBrowser; + browser.addTabsProgressListener(listener); + browser.tabContainer.addEventListener("TabSelect", tabSelected); + } + }, false); + } + Services.ww.registerNotification(winWatcher); + + // Add listeners for all current open windows. + let enumerator = Services.wm.getEnumerator("navigator:browser"); + while (enumerator.hasMoreElements()) { + let browser = enumerator.getNext().gBrowser; + browser.addTabsProgressListener(listener); + browser.tabContainer.addEventListener("TabSelect", tabSelected); + + // Also check the currently open URI. + if (browser.currentURI.prePath == this.DASHBOARD_ORIGIN) { + dashboardLoaded(browser); + } + } + + // Add listeners for app installs/uninstall. + Services.obs.addObserver(this, "webapps-sync-install", false); + Services.obs.addObserver(this, "webapps-sync-uninstall", false); + + // Add listener for idle service. + let idleSvc = Cc["@mozilla.org/widget/idleservice;1"]. + getService(Ci.nsIIdleService); + idleSvc.addIdleObserver(this, + Preferences.get("services.aitc.main.idleTime")); + }, + + observe: function(subject, topic, data) { + let app; + switch (topic) { + case "webapps-sync-install": + app = JSON.parse(data); + this._log.info(app.origin + " was installed, initiating PUT"); + this._manager.appEvent("install", app); + break; + case "webapps-sync-uninstall": + app = JSON.parse(data); + this._log.info(app.origin + " was uninstalled, initiating PUT"); + this._manager.appEvent("uninstall", app); + break; + case "idle": + this._log.info("User went idle"); + if (this._manager) { + this._manager.userIdle(); + } + break; + case "back": + this._log.info("User is no longer idle"); + let win = Services.wm.getMostRecentWindow("navigator:browser"); + if (win && win.gBrowser.currentURI.prePath == this.DASHBOARD_ORIGIN && + this._manager) { + this._manager.userActive(); + } + break; + } + }, + +}; diff --git a/services/aitc/modules/manager.js b/services/aitc/modules/manager.js new file mode 100644 index 000000000000..3f9d58b6cd46 --- /dev/null +++ b/services/aitc/modules/manager.js @@ -0,0 +1,573 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EXPORTED_SYMBOLS = ["AitcManager"]; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Webapps.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); + +Cu.import("resource://services-aitc/client.js"); +Cu.import("resource://services-aitc/browserid.js"); +Cu.import("resource://services-aitc/storage.js"); +Cu.import("resource://services-common/log4moz.js"); +Cu.import("resource://services-common/preferences.js"); +Cu.import("resource://services-common/tokenserverclient.js"); +Cu.import("resource://services-common/utils.js"); + +const PREFS = new Preferences("services.aitc."); +const TOKEN_TIMEOUT = 240000; // 4 minutes +const DASHBOARD_URL = PREFS.get("dashboard.url"); +const MARKETPLACE_URL = PREFS.get("marketplace.url"); + +/** + * The constructor for the manager takes a callback, which will be invoked when + * the manager is ready (construction is asynchronous). *DO NOT* call any + * methods on this object until the callback has been invoked, doing so will + * lead to undefined behaviour. + */ +function AitcManager(cb) { + this._client = null; + this._getTimer = null; + this._putTimer = null; + + this._lastToken = 0; + this._lastEmail = null; + this._dashboardWindow = null; + + this._log = Log4Moz.repository.getLogger("Service.AITC.Manager"); + this._log.level = Log4Moz.Level[Preferences.get("manager.log.level")]; + this._log.info("Loading AitC manager module"); + + // Check if we have pending PUTs from last time. + let self = this; + this._pending = new AitcQueue("webapps-pending.json", function _queueDone() { + // Inform the AitC service that we're good to go! + self._log.info("AitC manager has finished loading"); + try { + cb(true); + } catch (e) { + self._log.error(new Error("AitC manager callback threw " + e)); + } + + // Schedule them, but only if we can get a silent assertion. + self._makeClient(function(err, client) { + if (!err && client) { + self._client = client; + self._processQueue(); + } + }, false); + }); +} +AitcManager.prototype = { + /** + * State of the user. ACTIVE implies user is looking at the dashboard, + * PASSIVE means either not at the dashboard or the idle timer started. + */ + _ACTIVE: 1, + _PASSIVE: 2, + + /** + * Smart setter that will only call _setPoll is the value changes. + */ + _clientState: null, + get _state() { + return this._clientState; + }, + set _state(value) { + if (this._clientState == value) { + return; + } + this._clientState = value; + this._setPoll(); + }, + + /** + * Local app was just installed or uninstalled, ask client to PUT if user + * is logged in. + */ + appEvent: function appEvent(type, app) { + // Add this to the equeue. + let self = this; + let obj = {type: type, app: app, retries: 0, lastTime: 0}; + this._pending.enqueue(obj, function _enqueued(err, rec) { + if (err) { + self._log.error("Could not add " + type + " " + app + " to queue"); + return; + } + + // If we already have a client (i.e. user is logged in), attempt to PUT. + if (self._client) { + self._processQueue(); + return; + } + + // If not, try a silent client creation. + self._makeClient(function(err, client) { + if (!err && client) { + self._client = client; + self._processQueue(); + } + // If user is not logged in, we'll just have to try later. + }); + }); + }, + + /** + * User is looking at dashboard. Start polling actively, but if user isn't + * logged in, prompt for them to login via a dialog. + */ + userActive: function userActive(win) { + // Stash a reference to the dashboard window in case we need to prompt + this._dashboardWindow = win; + + if (this._client) { + this._state = this._ACTIVE; + return; + } + + // Make client will first try silent login, if it doesn't work, a popup + // will be shown in the context of the dashboard. We shouldn't be + // trying to make a client every time this function is called, there is + // room for optimization (Bug 750607). + let self = this; + this._makeClient(function(err, client) { + if (err) { + // Notify user of error (Bug 750610). + self._log.error("Client not created at Dashboard"); + return; + } + self._client = client; + self._state = self._ACTIVE; + }, true, win); + }, + + /** + * User is idle, (either by idle observer, or by not being on the dashboard). + * When the user is no longer idle and the dashboard is the current active + * page, a call to userActive MUST be made. + */ + userIdle: function userIdle() { + this._state = this._PASSIVE; + this._dashboardWindow = null; + }, + + /** + * Poll the AITC server for any changes and process them. It is safe to call + * this function multiple times. Last caller wins. The function will + * grab the current user state from _state and act accordingly. + * + * Invalid states will cause this function to throw. + */ + _setPoll: function _setPoll() { + if (this._state == this._ACTIVE && !this._client) { + throw new Error("_setPoll(ACTIVE) called without client"); + } + if (this._state != this._ACTIVE && this._state != this._PASSIVE) { + throw new Error("_state is invalid " + this._state); + } + + if (!this._client) { + // User is not logged in, we cannot do anything. + self._log.warn("_setPoll called but user not logged in, ignoring"); + return; + } + + // Check if there are any PUTs pending first. + if (this._pending.length && !(this._putTimer)) { + // There are pending PUTs and no timer, so let's process them. GETs will + // resume after the PUTs finish (see processQueue) + this._processQueue(); + return; + } + + // Do one GET soon, but only if user is active. + let getFreq; + if (this._state == this._ACTIVE) { + CommonUtils.nextTick(this._checkServer, this); + getFreq = PREFS.get("manager.getActiveFreq"); + } else { + getFreq = PREFS.get("manager.getPassiveFreq"); + } + + // Cancel existing timer, if any. + if (this._getTimer) { + this._getTimer.cancel(); + this._getTimer = null; + } + + // Start the timer for GETs. + let self = this; + this._log.info("Starting GET timer"); + this._getTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._getTimer.initWithCallback({notify: this._checkServer.bind(this)}, + getFreq, Ci.nsITimer.TYPE_REPEATING_SLACK); + + this._log.info("GET timer set, next attempt in " + getFreq + "ms"); + }, + + /** + * Checks if the current token we hold is valid. If not, we obtain a new one + * and execute the provided func. If a token could not be obtained, func will + * not be called and an error will be logged. + */ + _validateToken: function _validateToken(func) { + if (Date.now() - this._lastToken < TOKEN_TIMEOUT) { + func(); + return; + } + + let win; + if (this._state == this.ACTIVE) { + win = this._dashboardWindow; + } + + let self = this; + this._refreshToken(function(err, done) { + if (!done) { + this._log.warn("_checkServer could not refresh token, aborting"); + return; + } + func(); + }, win); + }, + + /** + * Do a GET check on the server to see if we have any new apps. Abort if + * there are pending PUTs. If we GET some apps, send to storage for + * further processing. + */ + _checkServer: function _checkServer() { + if (!this._client) { + throw new Error("_checkServer called without a client"); + } + + if (this._pending.length) { + this._log.warn("_checkServer aborted because of pending PUTs"); + return; + } + + this._validateToken(this._getApps.bind(this)); + }, + + _getApps: function _getApps() { + // Do a GET + this._log.info("Attempting to getApps"); + + let self = this; + this._client.getApps(function gotApps(err, apps) { + if (err) { + // Error was logged in client. + return; + } + if (!apps) { + // No changes, got 304. + return; + } + if (!apps.length) { + // Empty array, nothing to process + self._log.info("No apps found on remote server"); + return; + } + + // Send list of remote apps to storage to apply locally + AitcStorage.processApps(apps, function processedApps() { + self._log.info("processApps completed successfully, changes applied"); + }); + }); + }, + + /** + * Go through list of apps to PUT and attempt each one. If we fail, try + * again in PUT_FREQ. Will throw if called with an empty, _reschedule() + * makes sure that we don't. + */ + _processQueue: function _processQueue() { + if (!this._client) { + throw new Error("_processQueue called without a client"); + } + if (!this._pending.length) { + throw new Error("_processQueue called with an empty queue"); + } + + if (this._putInProgress) { + // The network request sent out as a result to the last call to + // _processQueue still isn't done. A timer is created they all + // finish to make sure this function is called again if neccessary. + return; + } + + this._validateToken(this._putApps.bind(this)); + }, + + _putApps: function _putApps() { + this._putInProgress = true; + let record = this._pending.peek(); + + this._log.info("Processing record type " + record.type); + + let self = this; + function _clientCallback(err, done) { + // Send to end of queue if unsuccessful or err.removeFromQueue is false. + if (err && !err.removeFromQueue) { + self._log.info("PUT failed, re-adding to queue"); + + // Update retries and time + record.retries += 1; + record.lastTime = new Date().getTime(); + + // Add updated record to the end of the queue. + self._pending.enqueue(record, function(err, done) { + if (err) { + self._log.error("Enqueue failed " + err); + _reschedule(); + return; + } + // If record was successfully added, remove old record. + self._pending.dequeue(function(err, done) { + if (err) { + self._log.error("Dequeue failed " + err); + } + _reschedule(); + return; + }); + }); + } + + // If succeeded or client told us to remove from queue + self._log.info("_putApp asked us to remove it from queue"); + self._pending.dequeue(function(err, done) { + if (err) { + self._log.error("Dequeue failed " + e); + } + _reschedule(); + }); + } + + function _reschedule() { + // Release PUT lock + self._putInProgress = false; + + // We just finished PUTting an object, try the next one immediately, + // but only if haven't tried it already in the last putFreq (ms). + if (!self._pending.length) { + // Start GET timer now that we're done with PUTs. + self._setPoll(); + return; + } + + let obj = self._pending.peek(); + let cTime = new Date().getTime(); + let freq = PREFS.get("manager.putFreq"); + + // We tried this object recently, we'll come back to it later. + if (obj.lastTime && ((cTime - obj.lastTime) < freq)) { + self._log.info("Scheduling next processQueue in " + freq); + CommonUtils.namedTimer(self._processQueue, freq, self, "_putTimer"); + return; + } + + // Haven't tried this PUT yet, do it immediately. + self._log.info("Queue non-empty, processing next PUT"); + self._processQueue(); + } + + switch (record.type) { + case "install": + this._client.remoteInstall(record.app, _clientCallback); + break; + case "uninstall": + record.app.deleted = true; + this._client.remoteUninstall(record.app, _clientCallback); + break; + default: + this._log.warn( + "Unrecognized type " + record.type + " in queue, removing" + ); + let self = this; + this._pending.dequeue(function _dequeued(err) { + if (err) { + self._log.error("Dequeue of unrecognized app type failed"); + } + _reschedule(); + }); + } + }, + + /* Obtain a (new) token from the Sagrada token server. If win is is specified, + * the user will be asked to login via UI, if required. The callback's + * signature is cb(err, done). If a token is obtained successfully, done will + * be true and err will be null. + */ + _refreshToken: function _refreshToken(cb, win) { + if (!this._client) { + throw new Error("_refreshToken called without an active client"); + } + + this._log.info("Token refresh requested"); + + let self = this; + function refreshedAssertion(err, assertion) { + if (!err) { + self._getToken(assertion, function(err, token) { + if (err) { + cb(err, null); + return; + } + self._lastToken = Date.now(); + self._client.updateToken(token); + cb(null, true); + }); + return; + } + + // Silent refresh was asked for. + if (!win) { + cb(err, null); + return; + } + + // Prompt user to login. + self._makeClient(function(err, client) { + if (err) { + cb(err, null); + return; + } + + // makeClient sets an updated token. + self._client = client; + cb(null, true); + }, win); + } + + let options = { audience: DASHBOARD_URL }; + if (this._lastEmail) { + options.requiredEmail = this._lastEmail; + } else { + options.sameEmailAs = MARKETPLACE_URL; + } + BrowserID.getAssertion(refreshedAssertion, options); + }, + + /* Obtain a token from Sagrada token server, given a BrowserID assertion + * cb(err, token) will be invoked on success or failure. + */ + _getToken: function _getToken(assertion, cb) { + let url = PREFS.get("tokenServer.url") + "/1.0/aitc/1.0"; + let client = new TokenServerClient(); + + this._log.info("Obtaining token from " + url); + + let self = this; + try { + client.getTokenFromBrowserIDAssertion(url, assertion, function(err, tok) { + self._gotToken(err, tok, cb); + }); + } catch (e) { + cb(new Error(e), null); + } + }, + + // Token recieved from _getToken. + _gotToken: function _gotToken(err, tok, cb) { + if (!err) { + this._log.info("Got token from server: " + JSON.stringify(tok)); + cb(null, tok); + return; + } + + let msg = err.name + " in _getToken: " + err.error; + this._log.error(msg); + cb(msg, null); + }, + + // Extract the email address from a BrowserID assertion. + _extractEmail: function _extractEmail(assertion) { + // Please look the other way while I do this. Thanks. + let chain = assertion.split("~"); + let len = chain.length; + if (len < 2) { + return null; + } + + try { + // We need CommonUtils.decodeBase64URL. + let cert = JSON.parse(atob( + chain[0].split(".")[1].replace("-", "+", "g").replace("_", "/", "g") + )); + return cert.principal.email; + } catch (e) { + return null; + } + }, + + /* To start the AitcClient we need a token, for which we need a BrowserID + * assertion. If login is true, makeClient will ask the user to login in + * the context of win. cb is called with (err, client). + */ + _makeClient: function makeClient(cb, login, win) { + if (!cb) { + throw new Error("makeClient called without callback"); + } + if (login && !win) { + throw new Error("makeClient called with login as true but no win"); + } + + let self = this; + let ctxWin = win; + function processAssertion(val) { + // Store the email we got the token for so we can refresh. + self._lastEmail = self._extractEmail(val); + self._log.info("Got assertion from BrowserID, creating token"); + self._getToken(val, function(err, token) { + if (err) { + cb(err, null); + return; + } + + // Store when we got the token so we can refresh it as needed. + self._lastToken = Date.now(); + + // We only create one client instance, store values in a pref tree + cb(null, new AitcClient( + token, new Preferences("services.aitc.client.") + )); + }); + } + function gotSilentAssertion(err, val) { + self._log.info("gotSilentAssertion called"); + if (err) { + // If we were asked to let the user login, do the popup method. + if (login) { + self._log.info("Could not obtain silent assertion, retrying login"); + BrowserID.getAssertionWithLogin(function gotAssertion(err, val) { + if (err) { + self._log.error(err); + cb(err, false); + return; + } + processAssertion(val); + }, ctxWin); + return; + } + self._log.warn("Could not obtain assertion in _makeClient"); + cb(err, false); + } else { + processAssertion(val); + } + } + + // Check if we can get assertion silently first + self._log.info("Attempting to obtain assertion silently") + BrowserID.getAssertion(gotSilentAssertion, { + audience: DASHBOARD_URL, + sameEmailAs: MARKETPLACE_URL + }); + }, + +}; diff --git a/services/aitc/service.js b/services/aitc/service.js deleted file mode 100644 index 52e71f379b42..000000000000 --- a/services/aitc/service.js +++ /dev/null @@ -1,47 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); - -function AitcService() { - this.wrappedJSObject = this; -} -AitcService.prototype = { - classID: Components.ID("{a3d387ca-fd26-44ca-93be-adb5fda5a78d}"), - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, - Ci.nsISupportsWeakReference]), - - observe: function observe(subject, topic, data) { - switch (topic) { - case "app-startup": - let os = Cc["@mozilla.org/observer-service;1"] - .getService(Ci.nsIObserverService); - os.addObserver(this, "final-ui-startup", true); - break; - case "final-ui-startup": - // Start AITC service after 2000ms, only if classic sync is off. - Cu.import("resource://services-common/preferences.js"); - if (Preferences.get("services.sync.engine.apps", false)) { - return; - } - if (!Preferences.get("services.aitc.enabled", true)) { - return; - } - - Cu.import("resource://services-common/utils.js"); - CommonUtils.namedTimer(function() { - // Kick-off later! - }, 2000, this, "timer"); - break; - } - } -}; - -const components = [AitcService]; -const NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/services/aitc/services-aitc.js b/services/aitc/services-aitc.js index c69f2a5d9313..6a0931e73342 100644 --- a/services/aitc/services-aitc.js +++ b/services/aitc/services-aitc.js @@ -2,8 +2,22 @@ pref("services.aitc.browserid.url", "https://browserid.org/sign_in"); pref("services.aitc.browserid.log.level", "Debug"); pref("services.aitc.client.log.level", "Debug"); -pref("services.aitc.storage.log.level", "Debug"); +pref("services.aitc.client.timeout", 120); // 120 seconds -pref("services.aitc.client.timeout", 120); +pref("services.aitc.dashboard.url", "https://myapps.mozillalabs.com"); + +pref("services.aitc.main.idleTime", 120000); // 2 minutes + +pref("services.aitc.manager.putFreq", 10000); // 10 seconds +pref("services.aitc.manager.getActiveFreq", 120000); // 2 minutes +pref("services.aitc.manager.getPassiveFreq", 7200000); // 2 hours +pref("services.aitc.manager.log.level", "Debug"); + +pref("services.aitc.marketplace.url", "https://marketplace.mozilla.org"); + +pref("services.aitc.service.log.level", "Debug"); + +// Temporary value. Change to the production server when we get the OK from server ops +pref("services.aitc.tokenServer.url", "https://stage-token.services.mozilla.com"); pref("services.aitc.storage.log.level", "Debug"); diff --git a/services/aitc/tests/unit/test_load_modules.js b/services/aitc/tests/unit/test_load_modules.js index fa575f2b297b..266e8bbb422d 100644 --- a/services/aitc/tests/unit/test_load_modules.js +++ b/services/aitc/tests/unit/test_load_modules.js @@ -1,6 +1,8 @@ const modules = [ "client.js", "browserid.js", + "main.js", + "manager.js", "storage.js" ];