Bumping manifests a=b2g-bump
[gecko.git] / toolkit / identity / FirefoxAccounts.jsm
blob33f872fc6c5c0ccccaa966b707495de137f12bc7
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 this.EXPORTED_SYMBOLS = ["FirefoxAccounts"];
9 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
11 Cu.import("resource://gre/modules/Log.jsm");
12 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
13 Cu.import("resource://gre/modules/Services.jsm");
14 Cu.import("resource://gre/modules/identity/LogUtils.jsm");
16 XPCOMUtils.defineLazyModuleGetter(this, "objectCopy",
17                                   "resource://gre/modules/identity/IdentityUtils.jsm");
19 XPCOMUtils.defineLazyModuleGetter(this, "makeMessageObject",
20                                   "resource://gre/modules/identity/IdentityUtils.jsm");
22 // loglevel preference should be one of: "FATAL", "ERROR", "WARN", "INFO",
23 // "CONFIG", "DEBUG", "TRACE" or "ALL". We will be logging error messages by
24 // default.
25 const PREF_LOG_LEVEL = "identity.fxaccounts.loglevel";
26 try {
27   this.LOG_LEVEL =
28     Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
29     && Services.prefs.getCharPref(PREF_LOG_LEVEL);
30 } catch (e) {
31   this.LOG_LEVEL = Log.Level.Error;
34 let log = Log.repository.getLogger("Identity.FxAccounts");
35 log.level = LOG_LEVEL;
36 log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
38 #ifdef MOZ_B2G
39 XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsManager",
40                                   "resource://gre/modules/FxAccountsManager.jsm",
41                                   "FxAccountsManager");
42 Cu.import("resource://gre/modules/FxAccountsCommon.js");
43 #else
44 log.warn("The FxAccountsManager is only functional in B2G at this time.");
45 var FxAccountsManager = null;
46 var ONVERIFIED_NOTIFICATION = null;
47 var ONLOGIN_NOTIFICATION = null;
48 var ONLOGOUT_NOTIFICATION = null;
49 #endif
51 function FxAccountsService() {
52   Services.obs.addObserver(this, "quit-application-granted", false);
53   if (ONVERIFIED_NOTIFICATION) {
54     Services.obs.addObserver(this, ONVERIFIED_NOTIFICATION, false);
55     Services.obs.addObserver(this, ONLOGIN_NOTIFICATION, false);
56     Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false);
57   }
59   // Maintain interface parity with Identity.jsm and MinimalIdentity.jsm
60   this.RP = this;
62   this._rpFlows = new Map();
64   // Enable us to mock FxAccountsManager service in testing
65   this.fxAccountsManager = FxAccountsManager;
68 FxAccountsService.prototype = {
69   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
71   observe: function observe(aSubject, aTopic, aData) {
72     switch (aTopic) {
73       case null:
74         // Guard against matching null ON*_NOTIFICATION
75         break;
76       case ONVERIFIED_NOTIFICATION:
77         log.debug("Received " + ONVERIFIED_NOTIFICATION + "; firing request()s");
78         for (let [rpId,] of this._rpFlows) {
79           this.request(rpId);
80         }
81         break;
82       case ONLOGIN_NOTIFICATION:
83         log.debug("Received " + ONLOGIN_NOTIFICATION + "; doLogin()s fired");
84         for (let [rpId,] of this._rpFlows) {
85           this.request(rpId);
86         }
87         break;
88       case ONLOGOUT_NOTIFICATION:
89         log.debug("Received " + ONLOGOUT_NOTIFICATION + "; doLogout()s fired");
90         for (let [rpId,] of this._rpFlows) {
91           this.doLogout(rpId);
92         }
93         break;
94       case "quit-application-granted":
95         Services.obs.removeObserver(this, "quit-application-granted");
96         if (ONVERIFIED_NOTIFICATION) {
97           Services.obs.removeObserver(this, ONVERIFIED_NOTIFICATION);
98           Services.obs.removeObserver(this, ONLOGIN_NOTIFICATION);
99           Services.obs.removeObserver(this, ONLOGOUT_NOTIFICATION);
100         }
101         break;
102     }
103   },
105   cleanupRPRequest: function(aRp) {
106     aRp.pendingRequest = false;
107     this._rpFlows.set(aRp.id, aRp);
108   },
110   /**
111    * Register a listener for a given windowID as a result of a call to
112    * navigator.id.watch().
113    *
114    * @param aRPCaller
115    *        (Object)  an object that represents the caller document, and
116    *                  is expected to have properties:
117    *                  - id (unique, e.g. uuid)
118    *                  - origin (string)
119    *
120    *                  and a bunch of callbacks
121    *                  - doReady()
122    *                  - doLogin()
123    *                  - doLogout()
124    *                  - doError()
125    *                  - doCancel()
126    *
127    */
128   watch: function watch(aRpCaller) {
129     this._rpFlows.set(aRpCaller.id, aRpCaller);
130     log.debug("watch: " + aRpCaller.id);
131     log.debug("Current rp flows: " + this._rpFlows.size);
133     // Log the user in, if possible, and then call ready().
134     let runnable = {
135       run: () => {
136         this.fxAccountsManager.getAssertion(aRpCaller.audience,
137                                             aRpCaller.principal,
138                                             { silent:true }).then(
139           data => {
140             if (data) {
141               this.doLogin(aRpCaller.id, data);
142             } else {
143               this.doLogout(aRpCaller.id);
144             }
145             this.doReady(aRpCaller.id);
146           },
147           error => {
148             log.error("get silent assertion failed: " + JSON.stringify(error));
149             this.doError(aRpCaller.id, error);
150           }
151         );
152       }
153     };
154     Services.tm.currentThread.dispatch(runnable,
155                                        Ci.nsIThread.DISPATCH_NORMAL);
156   },
158   /**
159    * Delete the flow when the screen is unloaded
160    */
161   unwatch: function(aRpCallerId, aTargetMM) {
162     log.debug("unwatching: " + aRpCallerId);
163     this._rpFlows.delete(aRpCallerId);
164   },
166   /**
167    * Initiate a login with user interaction as a result of a call to
168    * navigator.id.request().
169    *
170    * @param aRPId
171    *        (integer) the id of the doc object obtained in .watch()
172    *
173    * @param aOptions
174    *        (Object) options including privacyPolicy, termsOfService
175    */
176   request: function request(aRPId, aOptions) {
177     aOptions = aOptions || {};
178     let rp = this._rpFlows.get(aRPId);
179     if (!rp) {
180       log.error("request() called before watch()");
181       return;
182     }
184     // We check if we already have a pending request for this RP and in that
185     // case we just bail out. We don't want duplicated onlogin or oncancel
186     // events.
187     if (rp.pendingRequest) {
188       log.debug("request() already called");
189       return;
190     }
192     // Otherwise, we set the RP flow with the pending request flag.
193     rp.pendingRequest = true;
194     this._rpFlows.set(rp.id, rp);
196     let options = makeMessageObject(rp);
197     objectCopy(aOptions, options);
199     log.debug("get assertion for " + rp.audience);
201     this.fxAccountsManager.getAssertion(rp.audience, rp.principal, options)
202     .then(
203       data => {
204         log.debug("got assertion for " + rp.audience + ": " + data);
205         this.doLogin(aRPId, data);
206       },
207       error => {
208         log.debug("get assertion failed: " + JSON.stringify(error));
209         // Cancellation is passed through an error channel; here we reroute.
210         if ((error.error && (error.error.details == "DIALOG_CLOSED_BY_USER")) ||
211             (error.details == "DIALOG_CLOSED_BY_USER")) {
212           return this.doCancel(aRPId);
213         }
214         this.doError(aRPId, error);
215       }
216     )
217     .then(
218       () => {
219         this.cleanupRPRequest(rp);
220       }
221     )
222     .catch(
223       () => {
224         this.cleanupRPRequest(rp);
225       }
226     );
227   },
229   /**
230    * Invoked when a user wishes to logout of a site (for instance, when clicking
231    * on an in-content logout button).
232    *
233    * @param aRpCallerId
234    *        (integer)  the id of the doc object obtained in .watch()
235    *
236    */
237   logout: function logout(aRpCallerId) {
238     // XXX Bug 945363 - Resolve the SSO story for FXA and implement
239     // logout accordingly.
240     //
241     // For now, it makes no sense to logout from a specific RP in
242     // Firefox Accounts, so just directly call the logout callback.
243     if (!this._rpFlows.has(aRpCallerId)) {
244       log.error("logout() called before watch()");
245       return;
246     }
248     // Call logout() on the next tick
249     let runnable = {
250       run: () => {
251         this.fxAccountsManager.signOut().then(() => {
252           this.doLogout(aRpCallerId);
253         });
254       }
255     };
256     Services.tm.currentThread.dispatch(runnable,
257                                        Ci.nsIThread.DISPATCH_NORMAL);
258   },
260   childProcessShutdown: function childProcessShutdown(messageManager) {
261     for (let [key,] of this._rpFlows) {
262       if (this._rpFlows.get(key)._mm === messageManager) {
263         this._rpFlows.delete(key);
264       }
265     }
266   },
268   doLogin: function doLogin(aRpCallerId, aAssertion) {
269     let rp = this._rpFlows.get(aRpCallerId);
270     if (!rp) {
271       log.warn("doLogin found no rp to go with callerId " + aRpCallerId);
272       return;
273     }
275     rp.doLogin(aAssertion);
276   },
278   doLogout: function doLogout(aRpCallerId) {
279     let rp = this._rpFlows.get(aRpCallerId);
280     if (!rp) {
281       log.warn("doLogout found no rp to go with callerId " + aRpCallerId);
282       return;
283     }
285     rp.doLogout();
286   },
288   doReady: function doReady(aRpCallerId) {
289     let rp = this._rpFlows.get(aRpCallerId);
290     if (!rp) {
291       log.warn("doReady found no rp to go with callerId " + aRpCallerId);
292       return;
293     }
295     rp.doReady();
296   },
298   doCancel: function doCancel(aRpCallerId) {
299     let rp = this._rpFlows.get(aRpCallerId);
300     if (!rp) {
301       log.warn("doCancel found no rp to go with callerId " + aRpCallerId);
302       return;
303     }
305     rp.doCancel();
306   },
308   doError: function doError(aRpCallerId, aError) {
309     let rp = this._rpFlows.get(aRpCallerId);
310     if (!rp) {
311       log.warn("doError found no rp to go with callerId " + aRpCallerId);
312       return;
313     }
315     rp.doError(aError);
316   }
319 this.FirefoxAccounts = new FxAccountsService();