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