mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
574 lines
17 KiB
JavaScript
574 lines
17 KiB
JavaScript
/* 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.hidden = 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
|
|
});
|
|
},
|
|
|
|
};
|