mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-26 22:32:46 +00:00
Bug 757261: Apps in the Cloud Manager and Service; r=mconnor
This commit is contained in:
parent
19204af86d
commit
5a1ed35be7
@ -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
|
||||
|
121
services/aitc/Aitc.js
Normal file
121
services/aitc/Aitc.js
Normal file
@ -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);
|
@ -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
|
||||
|
@ -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
|
||||
|
161
services/aitc/modules/main.js
Normal file
161
services/aitc/modules/main.js
Normal file
@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
};
|
573
services/aitc/modules/manager.js
Normal file
573
services/aitc/modules/manager.js
Normal file
@ -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
|
||||
});
|
||||
},
|
||||
|
||||
};
|
@ -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);
|
@ -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");
|
||||
|
@ -1,6 +1,8 @@
|
||||
const modules = [
|
||||
"client.js",
|
||||
"browserid.js",
|
||||
"main.js",
|
||||
"manager.js",
|
||||
"storage.js"
|
||||
];
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user