no bug - Correct some typos in the comments. a=typo-fix
[gecko.git] / services / fxaccounts / FxAccountsWebChannel.sys.mjs
blobf8d7a3362d540d43a866209b4f7c0773b81d00b9
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
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /**
6  * Firefox Accounts Web Channel.
7  *
8  * Uses the WebChannel component to receive messages
9  * about account state changes.
10  */
12 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
14 import {
15   COMMAND_PROFILE_CHANGE,
16   COMMAND_LOGIN,
17   COMMAND_LOGOUT,
18   COMMAND_OAUTH,
19   COMMAND_DELETE,
20   COMMAND_CAN_LINK_ACCOUNT,
21   COMMAND_SYNC_PREFERENCES,
22   COMMAND_CHANGE_PASSWORD,
23   COMMAND_FXA_STATUS,
24   COMMAND_PAIR_HEARTBEAT,
25   COMMAND_PAIR_SUPP_METADATA,
26   COMMAND_PAIR_AUTHORIZE,
27   COMMAND_PAIR_DECLINE,
28   COMMAND_PAIR_COMPLETE,
29   COMMAND_PAIR_PREFERENCES,
30   COMMAND_FIREFOX_VIEW,
31   FX_OAUTH_CLIENT_ID,
32   ON_PROFILE_CHANGE_NOTIFICATION,
33   PREF_LAST_FXA_USER,
34   WEBCHANNEL_ID,
35   log,
36   logPII,
37 } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
39 const lazy = {};
41 ChromeUtils.defineESModuleGetters(lazy, {
42   CryptoUtils: "resource://services-crypto/utils.sys.mjs",
43   FxAccountsPairingFlow: "resource://gre/modules/FxAccountsPairing.sys.mjs",
44   FxAccountsStorageManagerCanStoreField:
45     "resource://gre/modules/FxAccountsStorage.sys.mjs",
46   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
47   Weave: "resource://services-sync/main.sys.mjs",
48   WebChannel: "resource://gre/modules/WebChannel.sys.mjs",
49 });
50 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
51   return ChromeUtils.importESModule(
52     "resource://gre/modules/FxAccounts.sys.mjs"
53   ).getFxAccountsSingleton();
54 });
55 XPCOMUtils.defineLazyPreferenceGetter(
56   lazy,
57   "pairingEnabled",
58   "identity.fxaccounts.pairing.enabled"
60 XPCOMUtils.defineLazyPreferenceGetter(
61   lazy,
62   "separatePrivilegedMozillaWebContentProcess",
63   "browser.tabs.remote.separatePrivilegedMozillaWebContentProcess",
64   false
66 XPCOMUtils.defineLazyPreferenceGetter(
67   lazy,
68   "separatedMozillaDomains",
69   "browser.tabs.remote.separatedMozillaDomains",
70   "",
71   false,
72   val => val.split(",")
74 XPCOMUtils.defineLazyPreferenceGetter(
75   lazy,
76   "accountServer",
77   "identity.fxaccounts.remote.root",
78   null,
79   false,
80   val => Services.io.newURI(val)
83 // These engines were added years after Sync had been introduced, they need
84 // special handling since they are system add-ons and are un-available on
85 // older versions of Firefox.
86 const EXTRA_ENGINES = ["addresses", "creditcards"];
88 // These engines will be displayed to the user to pick which they would like to
89 // use
90 const CHOOSE_WHAT_TO_SYNC = [
91   "addons",
92   "addresses",
93   "bookmarks",
94   "creditcards",
95   "history",
96   "passwords",
97   "preferences",
98   "tabs",
102  * A helper function that extracts the message and stack from an error object.
103  * Returns a `{ message, stack }` tuple. `stack` will be null if the error
104  * doesn't have a stack trace.
105  */
106 function getErrorDetails(error) {
107   // Replace anything that looks like it might be a filepath on Windows or Unix
108   let cleanMessage = String(error)
109     .replace(/\\.*\\/gm, "[REDACTED]")
110     .replace(/\/.*\//gm, "[REDACTED]");
111   let details = { message: cleanMessage, stack: null };
113   // Adapted from Console.sys.mjs.
114   if (error.stack) {
115     let frames = [];
116     for (let frame = error.stack; frame; frame = frame.caller) {
117       frames.push(String(frame).padStart(4));
118     }
119     details.stack = frames.join("\n");
120   }
122   return details;
126  * Create a new FxAccountsWebChannel to listen for account updates
128  * @param {Object} options Options
129  *   @param {Object} options
130  *     @param {String} options.content_uri
131  *     The FxA Content server uri
132  *     @param {String} options.channel_id
133  *     The ID of the WebChannel
134  *     @param {String} options.helpers
135  *     Helpers functions. Should only be passed in for testing.
136  * @constructor
137  */
138 export function FxAccountsWebChannel(options) {
139   if (!options) {
140     throw new Error("Missing configuration options");
141   }
142   if (!options.content_uri) {
143     throw new Error("Missing 'content_uri' option");
144   }
145   this._contentUri = options.content_uri;
147   if (!options.channel_id) {
148     throw new Error("Missing 'channel_id' option");
149   }
150   this._webChannelId = options.channel_id;
152   // options.helpers is only specified by tests.
153   ChromeUtils.defineLazyGetter(this, "_helpers", () => {
154     return options.helpers || new FxAccountsWebChannelHelpers(options);
155   });
157   this._setupChannel();
160 FxAccountsWebChannel.prototype = {
161   /**
162    * WebChannel that is used to communicate with content page
163    */
164   _channel: null,
166   /**
167    * Helpers interface that does the heavy lifting.
168    */
169   _helpers: null,
171   /**
172    * WebChannel ID.
173    */
174   _webChannelId: null,
175   /**
176    * WebChannel origin, used to validate origin of messages
177    */
178   _webChannelOrigin: null,
180   /**
181    * Release all resources that are in use.
182    */
183   tearDown() {
184     this._channel.stopListening();
185     this._channel = null;
186     this._channelCallback = null;
187   },
189   /**
190    * Configures and registers a new WebChannel
191    *
192    * @private
193    */
194   _setupChannel() {
195     // if this.contentUri is present but not a valid URI, then this will throw an error.
196     try {
197       this._webChannelOrigin = Services.io.newURI(this._contentUri);
198       this._registerChannel();
199     } catch (e) {
200       log.error(e);
201       throw e;
202     }
203   },
205   _receiveMessage(message, sendingContext) {
206     const { command, data } = message;
207     let shouldCheckRemoteType =
208       lazy.separatePrivilegedMozillaWebContentProcess &&
209       lazy.separatedMozillaDomains.some(function (val) {
210         return (
211           lazy.accountServer.asciiHost == val ||
212           lazy.accountServer.asciiHost.endsWith("." + val)
213         );
214       });
215     let { currentRemoteType } = sendingContext.browsingContext;
216     if (shouldCheckRemoteType && currentRemoteType != "privilegedmozilla") {
217       log.error(
218         `Rejected FxA webchannel message from remoteType = ${currentRemoteType}`
219       );
220       return;
221     }
223     let browser = sendingContext.browsingContext.top.embedderElement;
224     switch (command) {
225       case COMMAND_PROFILE_CHANGE:
226         Services.obs.notifyObservers(
227           null,
228           ON_PROFILE_CHANGE_NOTIFICATION,
229           data.uid
230         );
231         break;
232       case COMMAND_LOGIN:
233         this._helpers
234           .login(data)
235           .catch(error => this._sendError(error, message, sendingContext));
236         break;
237       case COMMAND_OAUTH:
238         this._helpers
239           .oauthLogin(data)
240           .catch(error => this._sendError(error, message, sendingContext));
241         break;
242       case COMMAND_LOGOUT:
243       case COMMAND_DELETE:
244         this._helpers
245           .logout(data.uid)
246           .catch(error => this._sendError(error, message, sendingContext));
247         break;
248       case COMMAND_CAN_LINK_ACCOUNT:
249         let canLinkAccount = this._helpers.shouldAllowRelink(data.email);
251         let response = {
252           command,
253           messageId: message.messageId,
254           data: { ok: canLinkAccount },
255         };
257         log.debug("FxAccountsWebChannel response", response);
258         this._channel.send(response, sendingContext);
259         break;
260       case COMMAND_SYNC_PREFERENCES:
261         this._helpers.openSyncPreferences(browser, data.entryPoint);
262         break;
263       case COMMAND_PAIR_PREFERENCES:
264         if (lazy.pairingEnabled) {
265           let window = browser.ownerGlobal;
266           // We should close the FxA tab after we open our pref page
267           let selectedTab = window.gBrowser.selectedTab;
268           window.switchToTabHavingURI(
269             "about:preferences?action=pair#sync",
270             true,
271             {
272               ignoreQueryString: true,
273               replaceQueryString: true,
274               adoptIntoActiveWindow: true,
275               ignoreFragment: "whenComparing",
276               triggeringPrincipal:
277                 Services.scriptSecurityManager.getSystemPrincipal(),
278             }
279           );
280           // close the tab
281           window.gBrowser.removeTab(selectedTab);
282         }
283         break;
284       case COMMAND_FIREFOX_VIEW:
285         this._helpers.openFirefoxView(browser, data.entryPoint);
286         break;
287       case COMMAND_CHANGE_PASSWORD:
288         this._helpers
289           .changePassword(data)
290           .catch(error => this._sendError(error, message, sendingContext));
291         break;
292       case COMMAND_FXA_STATUS:
293         log.debug("fxa_status received");
295         const service = data && data.service;
296         const isPairing = data && data.isPairing;
297         const context = data && data.context;
298         this._helpers
299           .getFxaStatus(service, sendingContext, isPairing, context)
300           .then(fxaStatus => {
301             let response = {
302               command,
303               messageId: message.messageId,
304               data: fxaStatus,
305             };
306             this._channel.send(response, sendingContext);
307           })
308           .catch(error => this._sendError(error, message, sendingContext));
309         break;
310       case COMMAND_PAIR_HEARTBEAT:
311       case COMMAND_PAIR_SUPP_METADATA:
312       case COMMAND_PAIR_AUTHORIZE:
313       case COMMAND_PAIR_DECLINE:
314       case COMMAND_PAIR_COMPLETE:
315         log.debug(`Pairing command ${command} received`);
316         const { channel_id: channelId } = data;
317         delete data.channel_id;
318         const flow = lazy.FxAccountsPairingFlow.get(channelId);
319         if (!flow) {
320           log.warn(`Could not find a pairing flow for ${channelId}`);
321           return;
322         }
323         flow.onWebChannelMessage(command, data).then(replyData => {
324           this._channel.send(
325             {
326               command,
327               messageId: message.messageId,
328               data: replyData,
329             },
330             sendingContext
331           );
332         });
333         break;
334       default:
335         log.warn("Unrecognized FxAccountsWebChannel command", command);
336         // As a safety measure we also terminate any pending FxA pairing flow.
337         lazy.FxAccountsPairingFlow.finalizeAll();
338         break;
339     }
340   },
342   _sendError(error, incomingMessage, sendingContext) {
343     log.error("Failed to handle FxAccountsWebChannel message", error);
344     this._channel.send(
345       {
346         command: incomingMessage.command,
347         messageId: incomingMessage.messageId,
348         data: {
349           error: getErrorDetails(error),
350         },
351       },
352       sendingContext
353     );
354   },
356   /**
357    * Create a new channel with the WebChannelBroker, setup a callback listener
358    * @private
359    */
360   _registerChannel() {
361     /**
362      * Processes messages that are called back from the FxAccountsChannel
363      *
364      * @param webChannelId {String}
365      *        Command webChannelId
366      * @param message {Object}
367      *        Command message
368      * @param sendingContext {Object}
369      *        Message sending context.
370      *        @param sendingContext.browsingContext {BrowsingContext}
371      *               The browsingcontext from which the
372      *               WebChannelMessageToChrome was sent.
373      *        @param sendingContext.eventTarget {EventTarget}
374      *               The <EventTarget> where the message was sent.
375      *        @param sendingContext.principal {Principal}
376      *               The <Principal> of the EventTarget where the message was sent.
377      * @private
378      *
379      */
380     let listener = (webChannelId, message, sendingContext) => {
381       if (message) {
382         log.debug("FxAccountsWebChannel message received", message.command);
383         if (logPII()) {
384           log.debug("FxAccountsWebChannel message details", message);
385         }
386         try {
387           this._receiveMessage(message, sendingContext);
388         } catch (error) {
389           this._sendError(error, message, sendingContext);
390         }
391       }
392     };
394     this._channelCallback = listener;
395     this._channel = new lazy.WebChannel(
396       this._webChannelId,
397       this._webChannelOrigin
398     );
399     this._channel.listen(listener);
400     log.debug(
401       "FxAccountsWebChannel registered: " +
402         this._webChannelId +
403         " with origin " +
404         this._webChannelOrigin.prePath
405     );
406   },
409 export function FxAccountsWebChannelHelpers(options) {
410   options = options || {};
412   this._fxAccounts = options.fxAccounts || lazy.fxAccounts;
413   this._weaveXPCOM = options.weaveXPCOM || null;
414   this._privateBrowsingUtils =
415     options.privateBrowsingUtils || lazy.PrivateBrowsingUtils;
418 FxAccountsWebChannelHelpers.prototype = {
419   // If the last fxa account used for sync isn't this account, we display
420   // a modal dialog checking they really really want to do this...
421   // (This is sync-specific, so ideally would be in sync's identity module,
422   // but it's a little more seamless to do here, and sync is currently the
423   // only fxa consumer, so...
424   shouldAllowRelink(acctName) {
425     return (
426       !this._needRelinkWarning(acctName) || this._promptForRelink(acctName)
427     );
428   },
430   async _initializeSync() {
431     // A sync-specific hack - we want to ensure sync has been initialized
432     // before we set the signed-in user.
433     // XXX - probably not true any more, especially now we have observerPreloads
434     // in FxAccounts.sys.mjs?
435     let xps =
436       this._weaveXPCOM ||
437       Cc["@mozilla.org/weave/service;1"].getService(Ci.nsISupports)
438         .wrappedJSObject;
439     await xps.whenLoaded();
440     return xps;
441   },
443   _setEnabledEngines(offeredEngines, declinedEngines) {
444     if (offeredEngines && declinedEngines) {
445       EXTRA_ENGINES.forEach(engine => {
446         if (
447           offeredEngines.includes(engine) &&
448           !declinedEngines.includes(engine)
449         ) {
450           // These extra engines are disabled by default.
451           Services.prefs.setBoolPref(`services.sync.engine.${engine}`, true);
452         }
453       });
454       log.debug("Received declined engines", declinedEngines);
455       lazy.Weave.Service.engineManager.setDeclined(declinedEngines);
456       declinedEngines.forEach(engine => {
457         Services.prefs.setBoolPref(`services.sync.engine.${engine}`, false);
458       });
459     }
460   },
461   /**
462    * stores sync login info it in the fxaccounts service
463    *
464    * @param accountData the user's account data and credentials
465    */
466   async login(accountData) {
467     // We don't act on customizeSync anymore, it used to open a dialog inside
468     // the browser to selecte the engines to sync but we do it on the web now.
469     log.debug("Webchannel is logging a user in.");
470     delete accountData.customizeSync;
472     // Save requested services for later.
473     const requestedServices = accountData.services;
474     delete accountData.services;
476     // the user has already been shown the "can link account"
477     // screen. No need to keep this data around.
478     delete accountData.verifiedCanLinkAccount;
480     // Remember who it was so we can log out next time.
481     if (accountData.verified) {
482       this.setPreviousAccountNameHashPref(accountData.email);
483     }
485     await this._fxAccounts.telemetry.recordConnection(
486       Object.keys(requestedServices || {}),
487       "webchannel"
488     );
490     const xps = await this._initializeSync();
491     await this._fxAccounts._internal.setSignedInUser(accountData);
493     if (requestedServices) {
494       // User has enabled Sync.
495       if (requestedServices.sync) {
496         const { offeredEngines, declinedEngines } = requestedServices.sync;
497         this._setEnabledEngines(offeredEngines, declinedEngines);
498         log.debug("Webchannel is enabling sync");
499         await xps.Weave.Service.configure();
500       }
501     }
502   },
504   /**
505    * Logins in to sync by completing an OAuth flow
506    * @param { Object } oauthData: The oauth code and state as returned by the server */
507   async oauthLogin(oauthData) {
508     log.debug("Webchannel is completing the oauth flow");
509     const xps = await this._initializeSync();
510     const { code, state, declinedSyncEngines, offeredSyncEngines } = oauthData;
511     const { sessionToken } =
512       await this._fxAccounts._internal.getUserAccountData(["sessionToken"]);
513     // First we finish the ongoing oauth flow
514     const { scopedKeys, refreshToken } =
515       await this._fxAccounts._internal.completeOAuthFlow(
516         sessionToken,
517         code,
518         state
519       );
521     // We don't currently use the refresh token in Firefox Desktop, lets be good citizens and revoke it.
522     await this._fxAccounts._internal.destroyOAuthToken({ token: refreshToken });
524     // Then, we persist the sync keys
525     await this._fxAccounts._internal.setScopedKeys(scopedKeys);
527     // Now that we have the scoped keys, we set our status to verified
528     await this._fxAccounts._internal.setUserVerified();
529     this._setEnabledEngines(offeredSyncEngines, declinedSyncEngines);
530     log.debug("Webchannel is enabling sync");
531     xps.Weave.Service.configure();
532   },
534   /**
535    * logout the fxaccounts service
536    *
537    * @param the uid of the account which have been logged out
538    */
539   async logout(uid) {
540     let fxa = this._fxAccounts;
541     let userData = await fxa._internal.getUserAccountData(["uid"]);
542     if (userData && userData.uid === uid) {
543       await fxa.telemetry.recordDisconnection(null, "webchannel");
544       // true argument is `localOnly`, because server-side stuff
545       // has already been taken care of by the content server
546       await fxa.signOut(true);
547     }
548   },
550   /**
551    * Check if `sendingContext` is in private browsing mode.
552    */
553   isPrivateBrowsingMode(sendingContext) {
554     if (!sendingContext) {
555       log.error("Unable to check for private browsing mode, assuming true");
556       return true;
557     }
559     let browser = sendingContext.browsingContext.top.embedderElement;
560     const isPrivateBrowsing =
561       this._privateBrowsingUtils.isBrowserPrivate(browser);
562     log.debug("is private browsing", isPrivateBrowsing);
563     return isPrivateBrowsing;
564   },
566   /**
567    * Check whether sending fxa_status data should be allowed.
568    */
569   shouldAllowFxaStatus(service, sendingContext, isPairing, context) {
570     // Return user data for any service in non-PB mode. In PB mode,
571     // only return user data if service==="sync" or is in pairing mode
572     // (as service will be equal to the OAuth client ID and not "sync").
573     //
574     // This behaviour allows users to click the "Manage Account"
575     // link from about:preferences#sync while in PB mode and things
576     // "just work". While in non-PB mode, users can sign into
577     // Pocket w/o entering their password a 2nd time, while in PB
578     // mode they *will* have to enter their email/password again.
579     //
580     // The difference in behaviour is to try to match user
581     // expectations as to what is and what isn't part of the browser.
582     // Sync is viewed as an integral part of the browser, interacting
583     // with FxA as part of a Sync flow should work all the time. If
584     // Sync is broken in PB mode, users will think Firefox is broken.
585     // See https://bugzilla.mozilla.org/show_bug.cgi?id=1323853
586     log.debug("service", service);
587     return (
588       !this.isPrivateBrowsingMode(sendingContext) ||
589       service === "sync" ||
590       context === "fx_desktop_v3" ||
591       isPairing
592     );
593   },
595   /**
596    * Get fxa_status information. Resolves to { signedInUser: <user_data> }.
597    * If returning status information is not allowed or no user is signed into
598    * Sync, `user_data` will be null.
599    */
600   async getFxaStatus(service, sendingContext, isPairing, context) {
601     let signedInUser = null;
603     if (
604       this.shouldAllowFxaStatus(service, sendingContext, isPairing, context)
605     ) {
606       const userData = await this._fxAccounts._internal.getUserAccountData([
607         "email",
608         "sessionToken",
609         "uid",
610         "verified",
611       ]);
612       if (userData) {
613         signedInUser = {
614           email: userData.email,
615           sessionToken: userData.sessionToken,
616           uid: userData.uid,
617           verified: userData.verified,
618         };
619       }
620     }
622     const capabilities = this._getCapabilities();
624     return {
625       signedInUser,
626       clientId: FX_OAUTH_CLIENT_ID,
627       capabilities,
628     };
629   },
630   _getCapabilities() {
631     if (
632       Services.prefs.getBoolPref("identity.fxaccounts.oauth.enabled", false)
633     ) {
634       return {
635         multiService: true,
636         pairing: lazy.pairingEnabled,
637         choose_what_to_sync: true,
638         engines: CHOOSE_WHAT_TO_SYNC,
639       };
640     }
641     return {
642       multiService: true,
643       pairing: lazy.pairingEnabled,
644       engines: this._getAvailableExtraEngines(),
645     };
646   },
648   _getAvailableExtraEngines() {
649     return EXTRA_ENGINES.filter(engineName => {
650       try {
651         return Services.prefs.getBoolPref(
652           `services.sync.engine.${engineName}.available`
653         );
654       } catch (e) {
655         return false;
656       }
657     });
658   },
660   async changePassword(credentials) {
661     // If |credentials| has fields that aren't handled by accounts storage,
662     // updateUserAccountData will throw - mainly to prevent errors in code
663     // that hard-codes field names.
664     // However, in this case the field names aren't really in our control.
665     // We *could* still insist the server know what fields names are valid,
666     // but that makes life difficult for the server when Firefox adds new
667     // features (ie, new fields) - forcing the server to track a map of
668     // versions to supported field names doesn't buy us much.
669     // So we just remove field names we know aren't handled.
670     let newCredentials = {
671       device: null, // Force a brand new device registration.
672       // We force the re-encryption of the send tab keys using the new sync key after the password change
673       encryptedSendTabKeys: null,
674     };
675     for (let name of Object.keys(credentials)) {
676       if (
677         name == "email" ||
678         name == "uid" ||
679         lazy.FxAccountsStorageManagerCanStoreField(name)
680       ) {
681         newCredentials[name] = credentials[name];
682       } else {
683         log.info("changePassword ignoring unsupported field", name);
684       }
685     }
686     await this._fxAccounts._internal.updateUserAccountData(newCredentials);
687     await this._fxAccounts._internal.updateDeviceRegistration();
688   },
690   /**
691    * Get the hash of account name of the previously signed in account
692    */
693   getPreviousAccountNameHashPref() {
694     try {
695       return Services.prefs.getStringPref(PREF_LAST_FXA_USER);
696     } catch (_) {
697       return "";
698     }
699   },
701   /**
702    * Given an account name, set the hash of the previously signed in account
703    *
704    * @param acctName the account name of the user's account.
705    */
706   setPreviousAccountNameHashPref(acctName) {
707     Services.prefs.setStringPref(
708       PREF_LAST_FXA_USER,
709       lazy.CryptoUtils.sha256Base64(acctName)
710     );
711   },
713   /**
714    * Open Sync Preferences in the current tab of the browser
715    *
716    * @param {Object} browser the browser in which to open preferences
717    * @param {String} [entryPoint] entryPoint to use for logging
718    */
719   openSyncPreferences(browser, entryPoint) {
720     let uri = "about:preferences";
721     if (entryPoint) {
722       uri += "?entrypoint=" + encodeURIComponent(entryPoint);
723     }
724     uri += "#sync";
726     browser.loadURI(Services.io.newURI(uri), {
727       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
728     });
729   },
731   /**
732    * Open Firefox View in the browser's window
733    *
734    * @param {Object} browser the browser in whose window we'll open Firefox View
735    */
736   openFirefoxView(browser) {
737     browser.ownerGlobal.FirefoxViewHandler.openTab("syncedtabs");
738   },
740   /**
741    * If a user signs in using a different account, the data from the
742    * previous account and the new account will be merged. Ask the user
743    * if they want to continue.
744    *
745    * @private
746    */
747   _needRelinkWarning(acctName) {
748     let prevAcctHash = this.getPreviousAccountNameHashPref();
749     return (
750       prevAcctHash && prevAcctHash != lazy.CryptoUtils.sha256Base64(acctName)
751     );
752   },
754   /**
755    * Show the user a warning dialog that the data from the previous account
756    * and the new account will be merged.
757    *
758    * @private
759    */
760   _promptForRelink(acctName) {
761     let sb = Services.strings.createBundle(
762       "chrome://browser/locale/syncSetup.properties"
763     );
764     let continueLabel = sb.GetStringFromName("continue.label");
765     let title = sb.GetStringFromName("relinkVerify.title");
766     let description = sb.formatStringFromName("relinkVerify.description", [
767       acctName,
768     ]);
769     let body =
770       sb.GetStringFromName("relinkVerify.heading") + "\n\n" + description;
771     let ps = Services.prompt;
772     let buttonFlags =
773       ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING +
774       ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL +
775       ps.BUTTON_POS_1_DEFAULT;
777     // If running in context of the browser chrome, window does not exist.
778     let pressed = Services.prompt.confirmEx(
779       null,
780       title,
781       body,
782       buttonFlags,
783       continueLabel,
784       null,
785       null,
786       null,
787       {}
788     );
789     return pressed === 0; // 0 is the "continue" button
790   },
793 var singleton;
795 // The entry-point for this module, which ensures only one of our channels is
796 // ever created - we require this because the WebChannel is global in scope
797 // (eg, it uses the observer service to tell interested parties of interesting
798 // things) and allowing multiple channels would cause such notifications to be
799 // sent multiple times.
800 export var EnsureFxAccountsWebChannel = () => {
801   let contentUri = Services.urlFormatter.formatURLPref(
802     "identity.fxaccounts.remote.root"
803   );
804   if (singleton && singleton._contentUri !== contentUri) {
805     singleton.tearDown();
806     singleton = null;
807   }
808   if (!singleton) {
809     try {
810       if (contentUri) {
811         // The FxAccountsWebChannel listens for events and updates
812         // the state machine accordingly.
813         singleton = new FxAccountsWebChannel({
814           content_uri: contentUri,
815           channel_id: WEBCHANNEL_ID,
816         });
817       } else {
818         log.warn("FxA WebChannel functionaly is disabled due to no URI pref.");
819       }
820     } catch (ex) {
821       log.error("Failed to create FxA WebChannel", ex);
822     }
823   }