diff options
Diffstat (limited to 'toolkit/identity/FirefoxAccounts.jsm')
-rw-r--r-- | toolkit/identity/FirefoxAccounts.jsm | 320 |
1 files changed, 320 insertions, 0 deletions
diff --git a/toolkit/identity/FirefoxAccounts.jsm b/toolkit/identity/FirefoxAccounts.jsm new file mode 100644 index 000000000..54efaf3b4 --- /dev/null +++ b/toolkit/identity/FirefoxAccounts.jsm @@ -0,0 +1,320 @@ +/* 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"; + +this.EXPORTED_SYMBOLS = ["FirefoxAccounts"]; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/identity/LogUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "objectCopy", + "resource://gre/modules/identity/IdentityUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "makeMessageObject", + "resource://gre/modules/identity/IdentityUtils.jsm"); + +// loglevel preference should be one of: "FATAL", "ERROR", "WARN", "INFO", +// "CONFIG", "DEBUG", "TRACE" or "ALL". We will be logging error messages by +// default. +const PREF_LOG_LEVEL = "identity.fxaccounts.loglevel"; +try { + this.LOG_LEVEL = + Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING + && Services.prefs.getCharPref(PREF_LOG_LEVEL); +} catch (e) { + this.LOG_LEVEL = Log.Level.Error; +} + +var log = Log.repository.getLogger("Identity.FxAccounts"); +log.level = LOG_LEVEL; +log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter())); + +#ifdef MOZ_B2G +XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsManager", + "resource://gre/modules/FxAccountsManager.jsm", + "FxAccountsManager"); +Cu.import("resource://gre/modules/FxAccountsCommon.js"); +#else +log.warn("The FxAccountsManager is only functional in B2G at this time."); +var FxAccountsManager = null; +var ONVERIFIED_NOTIFICATION = null; +var ONLOGIN_NOTIFICATION = null; +var ONLOGOUT_NOTIFICATION = null; +#endif + +function FxAccountsService() { + Services.obs.addObserver(this, "quit-application-granted", false); + if (ONVERIFIED_NOTIFICATION) { + Services.obs.addObserver(this, ONVERIFIED_NOTIFICATION, false); + Services.obs.addObserver(this, ONLOGIN_NOTIFICATION, false); + Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false); + } + + // Maintain interface parity with Identity.jsm and MinimalIdentity.jsm + this.RP = this; + + this._rpFlows = new Map(); + + // Enable us to mock FxAccountsManager service in testing + this.fxAccountsManager = FxAccountsManager; +} + +FxAccountsService.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]), + + observe: function observe(aSubject, aTopic, aData) { + switch (aTopic) { + case null: + // Guard against matching null ON*_NOTIFICATION + break; + case ONVERIFIED_NOTIFICATION: + log.debug("Received " + ONVERIFIED_NOTIFICATION + "; firing request()s"); + for (let [rpId,] of this._rpFlows) { + this.request(rpId); + } + break; + case ONLOGIN_NOTIFICATION: + log.debug("Received " + ONLOGIN_NOTIFICATION + "; doLogin()s fired"); + for (let [rpId,] of this._rpFlows) { + this.request(rpId); + } + break; + case ONLOGOUT_NOTIFICATION: + log.debug("Received " + ONLOGOUT_NOTIFICATION + "; doLogout()s fired"); + for (let [rpId,] of this._rpFlows) { + this.doLogout(rpId); + } + break; + case "quit-application-granted": + Services.obs.removeObserver(this, "quit-application-granted"); + if (ONVERIFIED_NOTIFICATION) { + Services.obs.removeObserver(this, ONVERIFIED_NOTIFICATION); + Services.obs.removeObserver(this, ONLOGIN_NOTIFICATION); + Services.obs.removeObserver(this, ONLOGOUT_NOTIFICATION); + } + break; + } + }, + + cleanupRPRequest: function(aRp) { + aRp.pendingRequest = false; + this._rpFlows.set(aRp.id, aRp); + }, + + /** + * Register a listener for a given windowID as a result of a call to + * navigator.id.watch(). + * + * @param aRPCaller + * (Object) an object that represents the caller document, and + * is expected to have properties: + * - id (unique, e.g. uuid) + * - origin (string) + * + * and a bunch of callbacks + * - doReady() + * - doLogin() + * - doLogout() + * - doError() + * - doCancel() + * + */ + watch: function watch(aRpCaller) { + this._rpFlows.set(aRpCaller.id, aRpCaller); + log.debug("watch: " + aRpCaller.id); + log.debug("Current rp flows: " + this._rpFlows.size); + + // Log the user in, if possible, and then call ready(). + let runnable = { + run: () => { + this.fxAccountsManager.getAssertion(aRpCaller.audience, + aRpCaller.principal, + { silent:true }).then( + data => { + if (data) { + this.doLogin(aRpCaller.id, data); + } else { + this.doLogout(aRpCaller.id); + } + this.doReady(aRpCaller.id); + }, + error => { + log.error("get silent assertion failed: " + JSON.stringify(error)); + this.doError(aRpCaller.id, error); + } + ); + } + }; + Services.tm.currentThread.dispatch(runnable, + Ci.nsIThread.DISPATCH_NORMAL); + }, + + /** + * Delete the flow when the screen is unloaded + */ + unwatch: function(aRpCallerId, aTargetMM) { + log.debug("unwatching: " + aRpCallerId); + this._rpFlows.delete(aRpCallerId); + }, + + /** + * Initiate a login with user interaction as a result of a call to + * navigator.id.request(). + * + * @param aRPId + * (integer) the id of the doc object obtained in .watch() + * + * @param aOptions + * (Object) options including privacyPolicy, termsOfService + */ + request: function request(aRPId, aOptions) { + aOptions = aOptions || {}; + let rp = this._rpFlows.get(aRPId); + if (!rp) { + log.error("request() called before watch()"); + return; + } + + // We check if we already have a pending request for this RP and in that + // case we just bail out. We don't want duplicated onlogin or oncancel + // events. + if (rp.pendingRequest) { + log.debug("request() already called"); + return; + } + + // Otherwise, we set the RP flow with the pending request flag. + rp.pendingRequest = true; + this._rpFlows.set(rp.id, rp); + + let options = makeMessageObject(rp); + objectCopy(aOptions, options); + + log.debug("get assertion for " + rp.audience); + + this.fxAccountsManager.getAssertion(rp.audience, rp.principal, options) + .then( + data => { + log.debug("got assertion for " + rp.audience + ": " + data); + this.doLogin(aRPId, data); + }, + error => { + log.debug("get assertion failed: " + JSON.stringify(error)); + // Cancellation is passed through an error channel; here we reroute. + if ((error.error && (error.error.details == "DIALOG_CLOSED_BY_USER")) || + (error.details == "DIALOG_CLOSED_BY_USER")) { + return this.doCancel(aRPId); + } + this.doError(aRPId, error); + } + ) + .then( + () => { + this.cleanupRPRequest(rp); + } + ) + .catch( + () => { + this.cleanupRPRequest(rp); + } + ); + }, + + /** + * Invoked when a user wishes to logout of a site (for instance, when clicking + * on an in-content logout button). + * + * @param aRpCallerId + * (integer) the id of the doc object obtained in .watch() + * + */ + logout: function logout(aRpCallerId) { + // XXX Bug 945363 - Resolve the SSO story for FXA and implement + // logout accordingly. + // + // For now, it makes no sense to logout from a specific RP in + // Firefox Accounts, so just directly call the logout callback. + if (!this._rpFlows.has(aRpCallerId)) { + log.error("logout() called before watch()"); + return; + } + + // Call logout() on the next tick + let runnable = { + run: () => { + this.fxAccountsManager.signOut().then(() => { + this.doLogout(aRpCallerId); + }); + } + }; + Services.tm.currentThread.dispatch(runnable, + Ci.nsIThread.DISPATCH_NORMAL); + }, + + childProcessShutdown: function childProcessShutdown(messageManager) { + for (let [key,] of this._rpFlows) { + if (this._rpFlows.get(key)._mm === messageManager) { + this._rpFlows.delete(key); + } + } + }, + + doLogin: function doLogin(aRpCallerId, aAssertion) { + let rp = this._rpFlows.get(aRpCallerId); + if (!rp) { + log.warn("doLogin found no rp to go with callerId " + aRpCallerId); + return; + } + + rp.doLogin(aAssertion); + }, + + doLogout: function doLogout(aRpCallerId) { + let rp = this._rpFlows.get(aRpCallerId); + if (!rp) { + log.warn("doLogout found no rp to go with callerId " + aRpCallerId); + return; + } + + rp.doLogout(); + }, + + doReady: function doReady(aRpCallerId) { + let rp = this._rpFlows.get(aRpCallerId); + if (!rp) { + log.warn("doReady found no rp to go with callerId " + aRpCallerId); + return; + } + + rp.doReady(); + }, + + doCancel: function doCancel(aRpCallerId) { + let rp = this._rpFlows.get(aRpCallerId); + if (!rp) { + log.warn("doCancel found no rp to go with callerId " + aRpCallerId); + return; + } + + rp.doCancel(); + }, + + doError: function doError(aRpCallerId, aError) { + let rp = this._rpFlows.get(aRpCallerId); + if (!rp) { + log.warn("doError found no rp to go with callerId " + aRpCallerId); + return; + } + + rp.doError(aError); + } +}; + +this.FirefoxAccounts = new FxAccountsService(); + |