tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

sync.js (33016B)


      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/. */
      4 
      5 /* import-globals-from preferences.js */
      6 
      7 const { SCOPE_APP_SYNC } = ChromeUtils.importESModule(
      8  "resource://gre/modules/FxAccountsCommon.sys.mjs"
      9 );
     10 
     11 const FXA_PAGE_LOGGED_OUT = 0;
     12 const FXA_PAGE_LOGGED_IN = 1;
     13 
     14 // Indexes into the "login status" deck.
     15 // We are in a successful verified state - everything should work!
     16 const FXA_LOGIN_VERIFIED = 0;
     17 // We have logged in to an unverified account.
     18 const FXA_LOGIN_UNVERIFIED = 1;
     19 // We are logged in locally, but the server rejected our credentials.
     20 const FXA_LOGIN_FAILED = 2;
     21 
     22 // Indexes into the "sync status" deck.
     23 const SYNC_DISCONNECTED = 0;
     24 const SYNC_CONNECTED = 1;
     25 
     26 const BACKUP_ARCHIVE_ENABLED_PREF_NAME = "browser.backup.archive.enabled";
     27 const BACKUP_RESTORE_ENABLED_PREF_NAME = "browser.backup.restore.enabled";
     28 
     29 ChromeUtils.defineESModuleGetters(lazy, {
     30  BackupService: "resource:///modules/backup/BackupService.sys.mjs",
     31 });
     32 
     33 Preferences.addAll([
     34  // sync
     35  { id: "services.sync.engine.bookmarks", type: "bool" },
     36  { id: "services.sync.engine.history", type: "bool" },
     37  { id: "services.sync.engine.tabs", type: "bool" },
     38  { id: "services.sync.engine.passwords", type: "bool" },
     39  { id: "services.sync.engine.addresses", type: "bool" },
     40  { id: "services.sync.engine.creditcards", type: "bool" },
     41  { id: "services.sync.engine.addons", type: "bool" },
     42  { id: "services.sync.engine.prefs", type: "bool" },
     43 ]);
     44 
     45 /**
     46 * A helper class for managing sync related UI behavior.
     47 */
     48 var SyncHelpers = new (class SyncHelpers {
     49  /**
     50   * href for Connect another device link.
     51   *
     52   * @type {string}
     53   */
     54  connectAnotherDeviceHref = "";
     55 
     56  /**
     57   * Returns the current global UIState.
     58   *
     59   * @type {object}
     60   * @readonly
     61   */
     62  get uiState() {
     63    let state = UIState.get();
     64    return state;
     65  }
     66 
     67  /**
     68   * Retrieves the current UI state status from the global UIState.
     69   *
     70   * @type {string}
     71   * @readonly
     72   */
     73  get uiStateStatus() {
     74    return this.uiState.status;
     75  }
     76 
     77  /**
     78   * Whether Sync is currently enabled in the UIState.
     79   *
     80   * @type {boolean}
     81   * @readonly
     82   */
     83  get isSyncEnabled() {
     84    return this.uiState.syncEnabled;
     85  }
     86 
     87  /**
     88   * Extracts and sanitizes the `entrypoint` parameter from the current document URL.
     89   *
     90   * @returns {string} The sanitized entry point name.
     91   */
     92  getEntryPoint() {
     93    let params = URL.fromURI(document.documentURIObject).searchParams;
     94    let entryPoint = params.get("entrypoint") || "preferences";
     95    entryPoint = entryPoint.replace(/[^-.\w]/g, "");
     96    return entryPoint;
     97  }
     98 
     99  /**
    100   * Replace the current tab with the specified URL.
    101   *
    102   * @param {string} url
    103   */
    104  replaceTabWithUrl(url) {
    105    // Get the <browser> element hosting us.
    106    let browser = window.docShell.chromeEventHandler;
    107    // And tell it to load our URL.
    108    browser.loadURI(Services.io.newURI(url), {
    109      triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
    110        {}
    111      ),
    112    });
    113  }
    114 
    115  /**
    116   * Opens the "Choose What to Sync" dialog and handles user interaction.
    117   *
    118   * @param {boolean} isSyncConfigured
    119   *        Whether Sync is already configured for this profile.
    120   * @param {string|null} [why=null]
    121   *        Optional reason or event name indicating why the dialog was opened.
    122   * @returns {Promise<void>}
    123   *          Resolves when the dialog flow and any post-actions have completed.
    124   */
    125  async _chooseWhatToSync(isSyncConfigured, why = null) {
    126    // Record the user opening the choose what to sync menu.
    127    fxAccounts.telemetry.recordOpenCWTSMenu(why).catch(err => {
    128      console.error("Failed to record open CWTS menu event", err);
    129    });
    130 
    131    // Assuming another device is syncing and we're not,
    132    // we update the engines selection so the correct
    133    // checkboxes are pre-filed.
    134    if (!isSyncConfigured) {
    135      try {
    136        await Weave.Service.updateLocalEnginesState();
    137      } catch (err) {
    138        console.error("Error updating the local engines state", err);
    139      }
    140    }
    141    let params = {};
    142    if (isSyncConfigured) {
    143      // If we are already syncing then we also offer to disconnect.
    144      params.disconnectFun = () => this.disconnectSync();
    145    }
    146    gSubDialog.open(
    147      "chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.xhtml",
    148      {
    149        closingCallback: event => {
    150          if (event.detail.button == "accept") {
    151            // Sync wasn't previously configured, but the user has accepted
    152            // so we want to now start syncing!
    153            if (!isSyncConfigured) {
    154              fxAccounts.telemetry
    155                .recordConnection(["sync"], "ui")
    156                .then(() => {
    157                  return Weave.Service.configure();
    158                })
    159                .catch(err => {
    160                  console.error("Failed to enable sync", err);
    161                });
    162            } else {
    163              // User is already configured and have possibly changed the engines they want to
    164              // sync, so we should let the server know immediately
    165              // if the user is currently syncing, we queue another sync after
    166              // to ensure we caught their updates
    167              Services.tm.dispatchToMainThread(() => {
    168                Weave.Service.queueSync("cwts");
    169              });
    170            }
    171          }
    172          // When the modal closes we want to remove any query params
    173          // so it doesn't open on subsequent visits (and will reload)
    174          const browser = window.docShell.chromeEventHandler;
    175          browser.loadURI(Services.io.newURI("about:preferences#sync"), {
    176            triggeringPrincipal:
    177              Services.scriptSecurityManager.getSystemPrincipal(),
    178          });
    179        },
    180      },
    181      params /* aParams */
    182    );
    183  }
    184 
    185  // Disconnect sync, leaving the account connected.
    186  disconnectSync() {
    187    return window.browsingContext.topChromeWindow.gSync.disconnect({
    188      confirm: true,
    189      disconnectAccount: false,
    190    });
    191  }
    192 
    193  async setupSync() {
    194    try {
    195      const hasKeys = await fxAccounts.keys.hasKeysForScope(SCOPE_APP_SYNC);
    196      if (hasKeys) {
    197        // User has keys - open the choose what to sync dialog
    198        this._chooseWhatToSync(false, "setupSync");
    199      } else {
    200        // User signed in via third-party auth without sync keys.
    201        // Redirect to FxA to create a password and generate sync keys.
    202        // canConnectAccount() checks if the Primary Password is locked and
    203        // prompts the user to unlock it. Returns false if the user cancels.
    204        if (!(await FxAccounts.canConnectAccount())) {
    205          return;
    206        }
    207        const url = await FxAccounts.config.promiseConnectAccountURI(
    208          this.getEntryPoint()
    209        );
    210        this.replaceTabWithUrl(url);
    211      }
    212    } catch (err) {
    213      console.error("Failed to check for sync keys", err);
    214      // Fallback to opening CWTS dialog
    215      this._chooseWhatToSync(false, "setupSync");
    216    }
    217  }
    218 
    219  async signIn() {
    220    if (!(await FxAccounts.canConnectAccount())) {
    221      return;
    222    }
    223    const url = await FxAccounts.config.promiseConnectAccountURI(
    224      this.getEntryPoint()
    225    );
    226    this.replaceTabWithUrl(url);
    227  }
    228 
    229  /**
    230   * Attempts to take the user through the sign in flow by opening the web content
    231   * with the given entrypoint as a query parameter
    232   *
    233   * @param {string} entrypoint
    234   *        An string appended to the query parameters, used in telemetry to differentiate
    235   *        different entrypoints to accounts
    236   */
    237  async reSignIn(entrypoint) {
    238    const url = await FxAccounts.config.promiseConnectAccountURI(entrypoint);
    239    this.replaceTabWithUrl(url);
    240  }
    241 
    242  async verifyFirefoxAccount() {
    243    return this.reSignIn("preferences-reverify");
    244  }
    245 
    246  /**
    247   * Disconnect the account, including everything linked.
    248   *
    249   * @param {boolean} confirm
    250   *        Whether to show a confirmation dialog before disconnecting
    251   */
    252  unlinkFirefoxAccount(confirm) {
    253    window.browsingContext.topChromeWindow.gSync.disconnect({
    254      confirm,
    255    });
    256  }
    257 })();
    258 
    259 Preferences.addSetting({
    260  id: "uiStateUpdate",
    261  setup(emitChange) {
    262    Weave.Svc.Obs.add(UIState.ON_UPDATE, emitChange);
    263    return () => Weave.Svc.Obs.remove(UIState.ON_UPDATE, emitChange);
    264  },
    265 });
    266 
    267 // Mozilla accounts section
    268 
    269 // Logged out of Mozilla account
    270 Preferences.addSetting({
    271  id: "noFxaAccountGroup",
    272  deps: ["uiStateUpdate"],
    273  visible() {
    274    return SyncHelpers.uiStateStatus == UIState.STATUS_NOT_CONFIGURED;
    275  },
    276 });
    277 Preferences.addSetting({
    278  id: "noFxaAccount",
    279 });
    280 Preferences.addSetting({
    281  id: "noFxaSignIn",
    282  onUserClick: () => {
    283    SyncHelpers.signIn();
    284  },
    285 });
    286 
    287 // Logged in and verified and all is good
    288 Preferences.addSetting({
    289  id: "fxaSignedInGroup",
    290  deps: ["uiStateUpdate"],
    291  visible() {
    292    return SyncHelpers.uiStateStatus == UIState.STATUS_SIGNED_IN;
    293  },
    294 });
    295 Preferences.addSetting({
    296  id: "fxaLoginVerified",
    297  deps: ["uiStateUpdate"],
    298  _failedAvatarURLs: new Set(),
    299  getControlConfig(config, _, setting) {
    300    let state = SyncHelpers.uiState;
    301 
    302    if (state.displayName) {
    303      config.l10nId = "sync-account-signed-in-display-name";
    304      config.l10nArgs = {
    305        name: state.displayName,
    306        email: state.email || "",
    307      };
    308    } else {
    309      config.l10nId = "sync-account-signed-in";
    310      config.l10nArgs = {
    311        email: state.email || "",
    312      };
    313    }
    314 
    315    // Reset the image to default avatar if we encounter an error.
    316    if (this._failedAvatarURLs.has(state.avatarURL)) {
    317      config.iconSrc = "chrome://browser/skin/fxa/avatar-color.svg";
    318      return config;
    319    }
    320 
    321    if (state.avatarURL && !state.avatarIsDefault) {
    322      config.iconSrc = state.avatarURL;
    323      let img = new Image();
    324      img.onerror = () => {
    325        this._failedAvatarURLs.add(state.avatarURL);
    326        setting.onChange();
    327      };
    328      img.src = state.avatarURL;
    329    }
    330    return config;
    331  },
    332 });
    333 Preferences.addSetting(
    334  class extends Preferences.AsyncSetting {
    335    static id = "verifiedManage";
    336 
    337    setup() {
    338      Weave.Svc.Obs.add(UIState.ON_UPDATE, this.emitChange);
    339      return () => Weave.Svc.Obs.remove(UIState.ON_UPDATE, this.emitChange);
    340    }
    341 
    342    // The "manage account" link embeds the uid, so we need to update this
    343    // if the account state changes.
    344    async getControlConfig() {
    345      let href = await FxAccounts.config.promiseManageURI(
    346        SyncHelpers.getEntryPoint()
    347      );
    348      return {
    349        controlAttrs: {
    350          href: href ?? "https://accounts.firefox.com/settings",
    351        },
    352      };
    353    }
    354  }
    355 );
    356 
    357 Preferences.addSetting({
    358  id: "fxaUnlinkButton",
    359  onUserClick: () => {
    360    SyncHelpers.unlinkFirefoxAccount(true);
    361  },
    362 });
    363 
    364 // Logged in to an unverified account
    365 Preferences.addSetting({
    366  id: "fxaUnverifiedGroup",
    367  deps: ["uiStateUpdate"],
    368  visible() {
    369    return SyncHelpers.uiStateStatus == UIState.STATUS_NOT_VERIFIED;
    370  },
    371 });
    372 Preferences.addSetting({
    373  id: "fxaLoginUnverified",
    374  deps: ["uiStateUpdate"],
    375  getControlConfig(config) {
    376    let state = SyncHelpers.uiState;
    377    config.l10nArgs = {
    378      email: state.email || "",
    379    };
    380    return config;
    381  },
    382 });
    383 Preferences.addSetting({
    384  id: "verifyFxaAccount",
    385  onUserClick: () => {
    386    SyncHelpers.verifyFirefoxAccount();
    387  },
    388 });
    389 Preferences.addSetting({
    390  id: "unverifiedUnlinkFxaAccount",
    391  onUserClick: () => {
    392    /* no warning as account can't have previously synced */
    393    SyncHelpers.unlinkFirefoxAccount(false);
    394  },
    395 });
    396 
    397 // Logged in locally but server rejected credentials
    398 Preferences.addSetting({
    399  id: "fxaLoginRejectedGroup",
    400  deps: ["uiStateUpdate"],
    401  visible() {
    402    return SyncHelpers.uiStateStatus == UIState.STATUS_LOGIN_FAILED;
    403  },
    404 });
    405 Preferences.addSetting({
    406  id: "fxaLoginRejected",
    407  deps: ["uiStateUpdate"],
    408  getControlConfig(config) {
    409    let state = SyncHelpers.uiState;
    410    config.l10nArgs = {
    411      email: state.email || "",
    412    };
    413    return config;
    414  },
    415 });
    416 Preferences.addSetting({
    417  id: "rejectReSignIn",
    418  onUserClick: () => {
    419    SyncHelpers.reSignIn(SyncHelpers.getEntryPoint());
    420  },
    421 });
    422 Preferences.addSetting({
    423  id: "rejectUnlinkFxaAccount",
    424  onUserClick: () => {
    425    SyncHelpers.unlinkFirefoxAccount(true);
    426  },
    427 });
    428 
    429 //Sync section
    430 
    431 //Sync section - no Firefox account
    432 Preferences.addSetting({
    433  id: "syncNoFxaSignIn",
    434  deps: ["uiStateUpdate"],
    435  visible() {
    436    return SyncHelpers.uiStateStatus === UIState.STATUS_NOT_CONFIGURED;
    437  },
    438  onUserClick: () => {
    439    SyncHelpers.signIn();
    440  },
    441 });
    442 
    443 // Sync section - Syncing is OFF
    444 Preferences.addSetting({
    445  id: "syncNotConfigured",
    446  deps: ["uiStateUpdate"],
    447  visible() {
    448    return (
    449      SyncHelpers.uiStateStatus === UIState.STATUS_SIGNED_IN &&
    450      !SyncHelpers.isSyncEnabled
    451    );
    452  },
    453 });
    454 Preferences.addSetting({
    455  id: "syncSetup",
    456  onUserClick: () => SyncHelpers.setupSync(),
    457 });
    458 
    459 // Sync section - Syncing is ON
    460 Preferences.addSetting({
    461  id: "syncConfigured",
    462  deps: ["uiStateUpdate"],
    463  visible() {
    464    return (
    465      SyncHelpers.uiStateStatus === UIState.STATUS_SIGNED_IN &&
    466      SyncHelpers.isSyncEnabled
    467    );
    468  },
    469 });
    470 
    471 Preferences.addSetting({
    472  id: "syncStatus",
    473 });
    474 Preferences.addSetting({
    475  id: "syncNow",
    476  deps: ["uiStateUpdate"],
    477  onUserClick() {
    478    Weave.Service.sync({ why: "aboutprefs" });
    479  },
    480  visible: () => !SyncHelpers.uiState.syncing,
    481  // Bug 2004864 - add tooltip
    482 });
    483 Preferences.addSetting({
    484  id: "syncing",
    485  deps: ["uiStateUpdate"],
    486  disabled: () => SyncHelpers.uiState.syncing,
    487  visible: () => SyncHelpers.uiState.syncing,
    488 });
    489 
    490 const SYNC_ENGINE_SETTINGS = [
    491  {
    492    id: "syncBookmarks",
    493    pref: "services.sync.engine.bookmarks",
    494    type: "bookmarks",
    495  },
    496  { id: "syncHistory", pref: "services.sync.engine.history", type: "history" },
    497  { id: "syncTabs", pref: "services.sync.engine.tabs", type: "tabs" },
    498  {
    499    id: "syncPasswords",
    500    pref: "services.sync.engine.passwords",
    501    type: "passwords",
    502  },
    503  {
    504    id: "syncAddresses",
    505    pref: "services.sync.engine.addresses",
    506    type: "addresses",
    507  },
    508  {
    509    id: "syncPayments",
    510    pref: "services.sync.engine.creditcards",
    511    type: "payments",
    512  },
    513  { id: "syncAddons", pref: "services.sync.engine.addons", type: "addons" },
    514  { id: "syncSettings", pref: "services.sync.engine.prefs", type: "settings" },
    515 ];
    516 
    517 SYNC_ENGINE_SETTINGS.forEach(({ id, pref }) => {
    518  Preferences.addSetting({ id, pref });
    519 });
    520 
    521 Preferences.addSetting({
    522  id: "syncEnginesList",
    523  deps: SYNC_ENGINE_SETTINGS.map(({ id }) => id),
    524  getControlConfig(config, deps) {
    525    const engines = SYNC_ENGINE_SETTINGS.filter(
    526      ({ id }) => deps[id]?.value
    527    ).map(({ type }) => type);
    528 
    529    return {
    530      ...config,
    531      controlAttrs: {
    532        ...config.controlAttrs,
    533        ".engines": engines,
    534      },
    535    };
    536  },
    537 });
    538 
    539 Preferences.addSetting({
    540  id: "syncChangeOptions",
    541  onUserClick: () => {
    542    SyncHelpers._chooseWhatToSync(true, "manageSyncSettings");
    543  },
    544 });
    545 
    546 // Sync section - Device name
    547 Preferences.addSetting({
    548  id: "fxaDeviceNameSection",
    549  deps: ["uiStateUpdate"],
    550  visible() {
    551    return SyncHelpers.uiStateStatus !== UIState.STATUS_NOT_CONFIGURED;
    552  },
    553 });
    554 Preferences.addSetting({
    555  id: "fxaDeviceNameGroup",
    556 });
    557 Preferences.addSetting({
    558  id: "fxaDeviceName",
    559  deps: ["uiStateUpdate"],
    560  get: () => Weave.Service.clientsEngine.localName,
    561  set(val) {
    562    Weave.Service.clientsEngine.localName = val;
    563  },
    564  disabled() {
    565    return SyncHelpers.uiStateStatus !== UIState.STATUS_SIGNED_IN;
    566  },
    567  getControlConfig(config) {
    568    if (config.controlAttrs?.defaultvalue) {
    569      return config;
    570    }
    571    const deviceDefaultLocalName = fxAccounts?.device?.getDefaultLocalName();
    572    if (deviceDefaultLocalName) {
    573      return {
    574        ...config,
    575        controlAttrs: {
    576          ...config.controlAttrs,
    577          defaultvalue: deviceDefaultLocalName,
    578        },
    579      };
    580    }
    581    return config;
    582  },
    583 });
    584 Preferences.addSetting({
    585  id: "fxaConnectAnotherDevice",
    586  getControlConfig(config) {
    587    if (SyncHelpers.connectAnotherDeviceHref) {
    588      return {
    589        ...config,
    590        controlAttrs: {
    591          ...config.controlAttrs,
    592          href: SyncHelpers.connectAnotherDeviceHref,
    593        },
    594      };
    595    }
    596    return config;
    597  },
    598  setup(emitChange) {
    599    FxAccounts.config
    600      .promiseConnectDeviceURI(SyncHelpers.getEntryPoint())
    601      .then(connectURI => {
    602        SyncHelpers.connectAnotherDeviceHref = connectURI;
    603        emitChange();
    604      });
    605  },
    606 });
    607 
    608 var gSyncPane = {
    609  get page() {
    610    return document.getElementById("weavePrefsDeck").selectedIndex;
    611  },
    612 
    613  set page(val) {
    614    document.getElementById("weavePrefsDeck").selectedIndex = val;
    615  },
    616 
    617  init() {
    618    this._setupEventListeners();
    619    this.setupEnginesUI();
    620    this.updateSyncUI();
    621 
    622    document
    623      .getElementById("weavePrefsDeck")
    624      .removeAttribute("data-hidden-from-search");
    625 
    626    // If the Service hasn't finished initializing, wait for it.
    627    let xps = Cc["@mozilla.org/weave/service;1"].getService(
    628      Ci.nsISupports
    629    ).wrappedJSObject;
    630 
    631    if (xps.ready) {
    632      this._init();
    633      return;
    634    }
    635 
    636    // it may take some time before all the promises we care about resolve, so
    637    // pre-load what we can from synchronous sources.
    638    this._showLoadPage(xps);
    639 
    640    let onUnload = function () {
    641      window.removeEventListener("unload", onUnload);
    642      try {
    643        Services.obs.removeObserver(onReady, "weave:service:ready");
    644      } catch (e) {}
    645    };
    646 
    647    let onReady = () => {
    648      Services.obs.removeObserver(onReady, "weave:service:ready");
    649      window.removeEventListener("unload", onUnload);
    650      this._init();
    651    };
    652 
    653    Services.obs.addObserver(onReady, "weave:service:ready");
    654    window.addEventListener("unload", onUnload);
    655 
    656    xps.ensureLoaded();
    657  },
    658 
    659  /**
    660   * This method allows us to override any hidden states that were set
    661   * during preferences.js init(). Currently, this is used to hide the
    662   * backup section if backup is disabled.
    663   *
    664   * Take caution when trying to flip the hidden state to true since the
    665   * element might show up unexpectedly on different pages in about:preferences
    666   * since this function will run at the end of preferences.js init().
    667   *
    668   * See Bug 1999032 to remove this in favor of config-based prefs.
    669   */
    670  handlePrefControlledSection() {
    671    let bs = lazy.BackupService.init();
    672 
    673    if (!bs.archiveEnabledStatus.enabled && !bs.restoreEnabledStatus.enabled) {
    674      document.getElementById("backupCategory").hidden = true;
    675      document.getElementById("dataBackupGroup").hidden = true;
    676    }
    677  },
    678 
    679  _showLoadPage() {
    680    let maybeAcct = false;
    681    let username = Services.prefs.getCharPref("services.sync.username", "");
    682    if (username) {
    683      document.getElementById("fxaEmailAddress").textContent = username;
    684      maybeAcct = true;
    685    }
    686 
    687    let cachedComputerName = Services.prefs.getStringPref(
    688      "identity.fxaccounts.account.device.name",
    689      ""
    690    );
    691    if (cachedComputerName) {
    692      maybeAcct = true;
    693      this._populateComputerName(cachedComputerName);
    694    }
    695    this.page = maybeAcct ? FXA_PAGE_LOGGED_IN : FXA_PAGE_LOGGED_OUT;
    696  },
    697 
    698  _init() {
    699    initSettingGroup("sync");
    700    initSettingGroup("account");
    701 
    702    Weave.Svc.Obs.add(UIState.ON_UPDATE, this.updateWeavePrefs, this);
    703 
    704    window.addEventListener("unload", () => {
    705      Weave.Svc.Obs.remove(UIState.ON_UPDATE, this.updateWeavePrefs, this);
    706    });
    707 
    708    FxAccounts.config
    709      .promiseConnectDeviceURI(SyncHelpers.getEntryPoint())
    710      .then(connectURI => {
    711        document
    712          .getElementById("connect-another-device")
    713          .setAttribute("href", connectURI);
    714      });
    715 
    716    // Links for mobile devices.
    717    for (let platform of ["android", "ios"]) {
    718      let url =
    719        Services.prefs.getCharPref(`identity.mobilepromo.${platform}`) +
    720        "sync-preferences";
    721      for (let elt of document.querySelectorAll(
    722        `.fxaMobilePromo-${platform}`
    723      )) {
    724        elt.setAttribute("href", url);
    725      }
    726    }
    727 
    728    this.updateWeavePrefs();
    729 
    730    // Notify observers that the UI is now ready
    731    Services.obs.notifyObservers(window, "sync-pane-loaded");
    732 
    733    this._maybeShowSyncAction();
    734  },
    735 
    736  // Check if the user is coming from a call to action
    737  // and show them the correct additional panel
    738  _maybeShowSyncAction() {
    739    if (
    740      location.hash == "#sync" &&
    741      UIState.get().status == UIState.STATUS_SIGNED_IN
    742    ) {
    743      if (location.href.includes("action=pair")) {
    744        gSyncPane.pairAnotherDevice();
    745      } else if (location.href.includes("action=choose-what-to-sync")) {
    746        SyncHelpers._chooseWhatToSync(false, "callToAction");
    747      }
    748    }
    749  },
    750 
    751  _toggleComputerNameControls(editMode) {
    752    let textbox = document.getElementById("fxaSyncComputerName");
    753    textbox.disabled = !editMode;
    754    document.getElementById("fxaChangeDeviceName").hidden = editMode;
    755    document.getElementById("fxaCancelChangeDeviceName").hidden = !editMode;
    756    document.getElementById("fxaSaveChangeDeviceName").hidden = !editMode;
    757  },
    758 
    759  _focusComputerNameTextbox() {
    760    let textbox = document.getElementById("fxaSyncComputerName");
    761    let valLength = textbox.value.length;
    762    textbox.focus();
    763    textbox.setSelectionRange(valLength, valLength);
    764  },
    765 
    766  _blurComputerNameTextbox() {
    767    document.getElementById("fxaSyncComputerName").blur();
    768  },
    769 
    770  _focusAfterComputerNameTextbox() {
    771    // Focus the most appropriate element that's *not* the "computer name" box.
    772    Services.focus.moveFocus(
    773      window,
    774      document.getElementById("fxaSyncComputerName"),
    775      Services.focus.MOVEFOCUS_FORWARD,
    776      0
    777    );
    778  },
    779 
    780  _updateComputerNameValue(save) {
    781    if (save) {
    782      let textbox = document.getElementById("fxaSyncComputerName");
    783      Weave.Service.clientsEngine.localName = textbox.value;
    784    }
    785    this._populateComputerName(Weave.Service.clientsEngine.localName);
    786  },
    787 
    788  _setupEventListeners() {
    789    function setEventListener(aId, aEventType, aCallback) {
    790      document
    791        .getElementById(aId)
    792        .addEventListener(aEventType, aCallback.bind(gSyncPane));
    793    }
    794 
    795    setEventListener("openChangeProfileImage", "click", function (event) {
    796      gSyncPane.openChangeProfileImage(event);
    797    });
    798    setEventListener("openChangeProfileImage", "keypress", function (event) {
    799      gSyncPane.openChangeProfileImage(event);
    800    });
    801    setEventListener("fxaChangeDeviceName", "command", function () {
    802      this._toggleComputerNameControls(true);
    803      this._focusComputerNameTextbox();
    804    });
    805    setEventListener("fxaCancelChangeDeviceName", "command", function () {
    806      // We explicitly blur the textbox because of bug 75324, then after
    807      // changing the state of the buttons, force focus to whatever the focus
    808      // manager thinks should be next (which on the mac, depends on an OSX
    809      // keyboard access preference)
    810      this._blurComputerNameTextbox();
    811      this._toggleComputerNameControls(false);
    812      this._updateComputerNameValue(false);
    813      this._focusAfterComputerNameTextbox();
    814    });
    815    setEventListener("fxaSaveChangeDeviceName", "command", function () {
    816      // Work around bug 75324 - see above.
    817      this._blurComputerNameTextbox();
    818      this._toggleComputerNameControls(false);
    819      this._updateComputerNameValue(true);
    820      this._focusAfterComputerNameTextbox();
    821    });
    822    setEventListener("noFxaSignIn", "command", function () {
    823      SyncHelpers.signIn();
    824      return false;
    825    });
    826    setEventListener("fxaUnlinkButton", "command", function () {
    827      SyncHelpers.unlinkFirefoxAccount(true);
    828    });
    829    setEventListener("verifyFxaAccount", "command", () =>
    830      SyncHelpers.verifyFirefoxAccount()
    831    );
    832    setEventListener("unverifiedUnlinkFxaAccount", "command", function () {
    833      /* no warning as account can't have previously synced */
    834      SyncHelpers.unlinkFirefoxAccount(false);
    835    });
    836    setEventListener("rejectReSignIn", "command", function () {
    837      SyncHelpers.reSignIn(SyncHelpers.getEntryPoint());
    838    });
    839    setEventListener("rejectUnlinkFxaAccount", "command", function () {
    840      SyncHelpers.unlinkFirefoxAccount(true);
    841    });
    842    setEventListener("fxaSyncComputerName", "keypress", function (e) {
    843      if (e.keyCode == KeyEvent.DOM_VK_RETURN) {
    844        document.getElementById("fxaSaveChangeDeviceName").click();
    845      } else if (e.keyCode == KeyEvent.DOM_VK_ESCAPE) {
    846        document.getElementById("fxaCancelChangeDeviceName").click();
    847      }
    848    });
    849    setEventListener("syncSetup", "command", () => SyncHelpers.setupSync());
    850    setEventListener("syncChangeOptions", "command", function () {
    851      SyncHelpers._chooseWhatToSync(true, "manageSyncSettings");
    852    });
    853    setEventListener("syncNow", "command", function () {
    854      // syncing can take a little time to send the "started" notification, so
    855      // pretend we already got it.
    856      this._updateSyncNow(true);
    857      Weave.Service.sync({ why: "aboutprefs" });
    858    });
    859    setEventListener("syncNow", "mouseover", function () {
    860      const state = UIState.get();
    861      // If we are currently syncing, just set the tooltip to the same as the
    862      // button label (ie, "Syncing...")
    863      let tooltiptext = state.syncing
    864        ? document.getElementById("syncNow").getAttribute("label")
    865        : window.browsingContext.topChromeWindow.gSync.formatLastSyncDate(
    866            state.lastSync
    867          );
    868      document
    869        .getElementById("syncNow")
    870        .setAttribute("tooltiptext", tooltiptext);
    871    });
    872  },
    873 
    874  updateSyncUI() {
    875    let syncStatusTitle = document.getElementById("syncStatusTitle");
    876    let syncNowButton = document.getElementById("syncNow");
    877    let syncNotConfiguredEl = document.getElementById("syncNotConfigured");
    878    let syncConfiguredEl = document.getElementById("syncConfigured");
    879 
    880    if (SyncHelpers.isSyncEnabled) {
    881      syncStatusTitle.setAttribute("data-l10n-id", "prefs-syncing-on");
    882      syncNowButton.hidden = false;
    883      syncConfiguredEl.hidden = false;
    884      syncNotConfiguredEl.hidden = true;
    885    } else {
    886      syncStatusTitle.setAttribute("data-l10n-id", "prefs-syncing-off");
    887      syncNowButton.hidden = true;
    888      syncConfiguredEl.hidden = true;
    889      syncNotConfiguredEl.hidden = false;
    890    }
    891  },
    892 
    893  _updateSyncNow(syncing) {
    894    let butSyncNow = document.getElementById("syncNow");
    895    let fluentID = syncing ? "prefs-syncing-button" : "prefs-sync-now-button";
    896    if (document.l10n.getAttributes(butSyncNow).id != fluentID) {
    897      // Only one of the two strings has an accesskey, and fluent won't
    898      // remove it if we switch to the string that doesn't, so just force
    899      // removal here.
    900      butSyncNow.removeAttribute("accesskey");
    901      document.l10n.setAttributes(butSyncNow, fluentID);
    902    }
    903    butSyncNow.disabled = syncing;
    904  },
    905 
    906  updateWeavePrefs() {
    907    let service = Cc["@mozilla.org/weave/service;1"].getService(
    908      Ci.nsISupports
    909    ).wrappedJSObject;
    910 
    911    let displayNameLabel = document.getElementById("fxaDisplayName");
    912    let fxaEmailAddressLabels = document.querySelectorAll(
    913      ".l10nArgsEmailAddress"
    914    );
    915    displayNameLabel.hidden = true;
    916 
    917    // while we determine the fxa status pre-load what we can.
    918    this._showLoadPage(service);
    919 
    920    let state = UIState.get();
    921    if (state.status == UIState.STATUS_NOT_CONFIGURED) {
    922      this.page = FXA_PAGE_LOGGED_OUT;
    923      return;
    924    }
    925    this.page = FXA_PAGE_LOGGED_IN;
    926    // We are logged in locally, but maybe we are in a state where the
    927    // server rejected our credentials (eg, password changed on the server)
    928    let fxaLoginStatus = document.getElementById("fxaLoginStatus");
    929    let syncReady = false; // Is sync able to actually sync?
    930    // We need to check error states that need a re-authenticate to resolve
    931    // themselves first.
    932    if (state.status == UIState.STATUS_LOGIN_FAILED) {
    933      fxaLoginStatus.selectedIndex = FXA_LOGIN_FAILED;
    934    } else if (state.status == UIState.STATUS_NOT_VERIFIED) {
    935      fxaLoginStatus.selectedIndex = FXA_LOGIN_UNVERIFIED;
    936    } else {
    937      // We must be golden (or in an error state we expect to magically
    938      // resolve itself)
    939      fxaLoginStatus.selectedIndex = FXA_LOGIN_VERIFIED;
    940      syncReady = true;
    941    }
    942    fxaEmailAddressLabels.forEach(label => {
    943      let l10nAttrs = document.l10n.getAttributes(label);
    944      document.l10n.setAttributes(label, l10nAttrs.id, { email: state.email });
    945    });
    946    document.getElementById("fxaEmailAddress").textContent = state.email;
    947 
    948    this._populateComputerName(Weave.Service.clientsEngine.localName);
    949    for (let elt of document.querySelectorAll(".needs-account-ready")) {
    950      elt.disabled = !syncReady;
    951    }
    952 
    953    // Clear the profile image (if any) of the previously logged in account.
    954    document
    955      .querySelector("#fxaLoginVerified > .fxaProfileImage")
    956      .style.removeProperty("list-style-image");
    957 
    958    if (state.displayName) {
    959      fxaLoginStatus.setAttribute("hasName", true);
    960      displayNameLabel.hidden = false;
    961      document.getElementById("fxaDisplayNameHeading").textContent =
    962        state.displayName;
    963    } else {
    964      fxaLoginStatus.removeAttribute("hasName");
    965    }
    966    if (state.avatarURL && !state.avatarIsDefault) {
    967      let bgImage = 'url("' + state.avatarURL + '")';
    968      let profileImageElement = document.querySelector(
    969        "#fxaLoginVerified > .fxaProfileImage"
    970      );
    971      profileImageElement.style.listStyleImage = bgImage;
    972 
    973      let img = new Image();
    974      img.onerror = () => {
    975        // Clear the image if it has trouble loading. Since this callback is asynchronous
    976        // we check to make sure the image is still the same before we clear it.
    977        if (profileImageElement.style.listStyleImage === bgImage) {
    978          profileImageElement.style.removeProperty("list-style-image");
    979        }
    980      };
    981      img.src = state.avatarURL;
    982    }
    983    // The "manage account" link embeds the uid, so we need to update this
    984    // if the account state changes.
    985    FxAccounts.config
    986      .promiseManageURI(SyncHelpers.getEntryPoint())
    987      .then(accountsManageURI => {
    988        document
    989          .getElementById("verifiedManage")
    990          .setAttribute("href", accountsManageURI);
    991      });
    992    // and the actual sync state.
    993    let eltSyncStatus = document.getElementById("syncStatusContainer");
    994    eltSyncStatus.hidden = !syncReady;
    995    this._updateSyncNow(state.syncing);
    996    this.updateSyncUI();
    997  },
    998 
    999  openContentInBrowser(url, options) {
   1000    let win = Services.wm.getMostRecentWindow("navigator:browser");
   1001    if (!win) {
   1002      openTrustedLinkIn(url, "tab");
   1003      return;
   1004    }
   1005    win.switchToTabHavingURI(url, true, options);
   1006  },
   1007 
   1008  // Replace the current tab with the specified URL.
   1009  replaceTabWithUrl(url) {
   1010    // Get the <browser> element hosting us.
   1011    let browser = window.docShell.chromeEventHandler;
   1012    // And tell it to load our URL.
   1013    browser.loadURI(Services.io.newURI(url), {
   1014      triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
   1015        {}
   1016      ),
   1017    });
   1018  },
   1019 
   1020  clickOrSpaceOrEnterPressed(event) {
   1021    // Note: charCode is deprecated, but 'char' not yet implemented.
   1022    // Replace charCode with char when implemented, see Bug 680830
   1023    return (
   1024      (event.type == "click" && event.button == 0) ||
   1025      (event.type == "keypress" &&
   1026        (event.charCode == KeyEvent.DOM_VK_SPACE ||
   1027          event.keyCode == KeyEvent.DOM_VK_RETURN))
   1028    );
   1029  },
   1030 
   1031  openChangeProfileImage(event) {
   1032    if (this.clickOrSpaceOrEnterPressed(event)) {
   1033      FxAccounts.config
   1034        .promiseChangeAvatarURI(SyncHelpers.getEntryPoint())
   1035        .then(url => {
   1036          this.openContentInBrowser(url, {
   1037            replaceQueryString: true,
   1038            triggeringPrincipal:
   1039              Services.scriptSecurityManager.getSystemPrincipal(),
   1040          });
   1041        });
   1042      // Prevent page from scrolling on the space key.
   1043      event.preventDefault();
   1044    }
   1045  },
   1046 
   1047  pairAnotherDevice() {
   1048    gSubDialog.open(
   1049      "chrome://browser/content/preferences/fxaPairDevice.xhtml",
   1050      { features: "resizable=no" }
   1051    );
   1052  },
   1053 
   1054  _populateComputerName(value) {
   1055    let textbox = document.getElementById("fxaSyncComputerName");
   1056    if (!textbox.hasAttribute("placeholder")) {
   1057      textbox.setAttribute(
   1058        "placeholder",
   1059        fxAccounts.device.getDefaultLocalName()
   1060      );
   1061    }
   1062    textbox.value = value;
   1063  },
   1064 
   1065  // arranges to dynamically show or hide sync engine name elements based on the
   1066  // preferences used for this engines.
   1067  setupEnginesUI() {
   1068    let observe = (elt, prefName) => {
   1069      elt.hidden = !Services.prefs.getBoolPref(prefName, false);
   1070    };
   1071 
   1072    for (let elt of document.querySelectorAll("[engine_preference]")) {
   1073      let prefName = elt.getAttribute("engine_preference");
   1074      let obs = observe.bind(null, elt, prefName);
   1075      obs();
   1076      Services.prefs.addObserver(prefName, obs);
   1077      window.addEventListener("unload", () => {
   1078        Services.prefs.removeObserver(prefName, obs);
   1079      });
   1080    }
   1081  },
   1082 };