tor-browser

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

AboutLoginsParent.sys.mjs (26786B)


      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/. */
      4 
      5 // _AboutLogins is only exported for testing
      6 import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";
      7 
      8 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      9 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
     10 import { E10SUtils } from "resource://gre/modules/E10SUtils.sys.mjs";
     11 
     12 const lazy = {};
     13 
     14 ChromeUtils.defineESModuleGetters(lazy, {
     15  LoginBreaches: "resource:///modules/LoginBreaches.sys.mjs",
     16  LoginCSVImport: "resource://gre/modules/LoginCSVImport.sys.mjs",
     17  LoginExport: "resource://gre/modules/LoginExport.sys.mjs",
     18  LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
     19  MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs",
     20  UIState: "resource://services-sync/UIState.sys.mjs",
     21  FxAccounts: "resource://gre/modules/FxAccounts.sys.mjs",
     22 });
     23 
     24 ChromeUtils.defineLazyGetter(lazy, "log", () => {
     25  return lazy.LoginHelper.createLogger("AboutLoginsParent");
     26 });
     27 XPCOMUtils.defineLazyPreferenceGetter(
     28  lazy,
     29  "BREACH_ALERTS_ENABLED",
     30  "signon.management.page.breach-alerts.enabled",
     31  false
     32 );
     33 XPCOMUtils.defineLazyPreferenceGetter(
     34  lazy,
     35  "FXA_ENABLED",
     36  "identity.fxaccounts.enabled",
     37  false
     38 );
     39 XPCOMUtils.defineLazyPreferenceGetter(
     40  lazy,
     41  "VULNERABLE_PASSWORDS_ENABLED",
     42  "signon.management.page.vulnerable-passwords.enabled",
     43  false
     44 );
     45 ChromeUtils.defineLazyGetter(lazy, "AboutLoginsL10n", () => {
     46  return new Localization(["branding/brand.ftl", "browser/aboutLogins.ftl"]);
     47 });
     48 
     49 const ABOUT_LOGINS_ORIGIN = "about:logins";
     50 const AUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
     51 const PRIMARY_PASSWORD_NOTIFICATION_ID = "primary-password-login-required";
     52 const NOCERTDB_PREF = "security.nocertdb";
     53 
     54 // about:logins will always use the privileged content process,
     55 // even if it is disabled for other consumers such as about:newtab.
     56 const EXPECTED_ABOUTLOGINS_REMOTE_TYPE = E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE;
     57 let _gPasswordRemaskTimeout = null;
     58 const convertSubjectToLogin = subject => {
     59  subject.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo);
     60  const login = lazy.LoginHelper.loginToVanillaObject(subject);
     61  if (!lazy.LoginHelper.isUserFacingLogin(login)) {
     62    return null;
     63  }
     64  return augmentVanillaLoginObject(login);
     65 };
     66 
     67 const SUBDOMAIN_REGEX = new RegExp(/^www\d*\./);
     68 const augmentVanillaLoginObject = login => {
     69  // Note that `displayOrigin` can also include a httpRealm.
     70  let title = login.displayOrigin.replace(SUBDOMAIN_REGEX, "");
     71  return Object.assign({}, login, {
     72    title,
     73  });
     74 };
     75 
     76 const EXPORT_PASSWORD_OS_AUTH_DIALOG_MESSAGE_IDS = {
     77  win: "about-logins-export-password-os-auth-dialog-message2-win",
     78  macosx: "about-logins-export-password-os-auth-dialog-message2-macosx",
     79 };
     80 
     81 export class AboutLoginsParent extends JSWindowActorParent {
     82  async receiveMessage(message) {
     83    if (!this.browsingContext.embedderElement) {
     84      return;
     85    }
     86 
     87    // Only respond to messages sent from a privlegedabout process. Ideally
     88    // we would also check the contentPrincipal.originNoSuffix but this
     89    // check has been removed due to bug 1576722.
     90    if (
     91      this.browsingContext.embedderElement.remoteType !=
     92      EXPECTED_ABOUTLOGINS_REMOTE_TYPE
     93    ) {
     94      throw new Error(
     95        `AboutLoginsParent: Received ${message.name} message the remote type didn't match expectations: ${this.browsingContext.embedderElement.remoteType} == ${EXPECTED_ABOUTLOGINS_REMOTE_TYPE}`
     96      );
     97    }
     98 
     99    AboutLogins.subscribers.add(this.browsingContext);
    100 
    101    switch (message.name) {
    102      case "AboutLogins:CreateLogin": {
    103        await this.#createLogin(message.data.login);
    104        break;
    105      }
    106      case "AboutLogins:DeleteLogin": {
    107        this.#deleteLogin(message.data.login);
    108        break;
    109      }
    110      case "AboutLogins:SortChanged": {
    111        this.#sortChanged(message.data);
    112        break;
    113      }
    114      case "AboutLogins:SyncEnable": {
    115        this.#syncEnable();
    116        break;
    117      }
    118      case "AboutLogins:ImportFromBrowser": {
    119        this.#importFromBrowser();
    120        break;
    121      }
    122      case "AboutLogins:ImportReportInit": {
    123        this.#importReportInit();
    124        break;
    125      }
    126      case "AboutLogins:GetHelp": {
    127        this.#getHelp();
    128        break;
    129      }
    130      case "AboutLogins:OpenPreferences": {
    131        this.#openPreferences();
    132        break;
    133      }
    134      case "AboutLogins:PrimaryPasswordRequest": {
    135        await this.#primaryPasswordRequest(
    136          message.data.messageId,
    137          message.data.reason
    138        );
    139        break;
    140      }
    141      case "AboutLogins:Subscribe": {
    142        await this.#subscribe();
    143        break;
    144      }
    145      case "AboutLogins:UpdateLogin": {
    146        await this.#updateLogin(message.data.login);
    147        break;
    148      }
    149      case "AboutLogins:ExportPasswords": {
    150        await this.#exportPasswords();
    151        break;
    152      }
    153      case "AboutLogins:ImportFromFile": {
    154        await this.#importFromFile();
    155        break;
    156      }
    157      case "AboutLogins:RemoveAllLogins": {
    158        this.#removeAllLogins();
    159        break;
    160      }
    161    }
    162  }
    163 
    164  get #ownerGlobal() {
    165    return this.browsingContext.embedderElement?.ownerGlobal;
    166  }
    167 
    168  async #createLogin(newLogin) {
    169    if (!Services.policies.isAllowed("removeMasterPassword")) {
    170      if (!lazy.LoginHelper.isPrimaryPasswordSet()) {
    171        this.#ownerGlobal.openDialog(
    172          "chrome://mozapps/content/preferences/changemp.xhtml",
    173          "",
    174          "centerscreen,chrome,modal,titlebar"
    175        );
    176        if (!lazy.LoginHelper.isPrimaryPasswordSet()) {
    177          return;
    178        }
    179      }
    180    }
    181    // Remove the path from the origin, if it was provided.
    182    let origin = lazy.LoginHelper.getLoginOrigin(newLogin.origin);
    183    if (!origin) {
    184      console.error(
    185        "AboutLogins:CreateLogin: Unable to get an origin from the login details."
    186      );
    187      return;
    188    }
    189    newLogin.origin = origin;
    190    Object.assign(newLogin, {
    191      formActionOrigin: "",
    192      usernameField: "",
    193      passwordField: "",
    194    });
    195    newLogin = lazy.LoginHelper.vanillaObjectToLogin(newLogin);
    196    try {
    197      await Services.logins.addLoginAsync(newLogin);
    198    } catch (error) {
    199      this.#handleLoginStorageErrors(newLogin, error);
    200    }
    201  }
    202 
    203  get preselectedLogin() {
    204    const preselectedLogin =
    205      this.#ownerGlobal?.gBrowser.selectedTab.getAttribute("preselect-login") ||
    206      this.browsingContext.currentURI?.ref;
    207    this.#ownerGlobal?.gBrowser.selectedTab.removeAttribute("preselect-login");
    208    return preselectedLogin || null;
    209  }
    210 
    211  #deleteLogin(loginObject) {
    212    let login = lazy.LoginHelper.vanillaObjectToLogin(loginObject);
    213    Services.logins.removeLogin(login);
    214  }
    215 
    216  #sortChanged(sort) {
    217    Services.prefs.setCharPref("signon.management.page.sort", sort);
    218  }
    219 
    220  #syncEnable() {
    221    this.#ownerGlobal.gSync.openFxAEmailFirstPage("password-manager");
    222  }
    223 
    224  #importFromBrowser() {
    225    try {
    226      lazy.MigrationUtils.showMigrationWizard(this.#ownerGlobal, {
    227        entrypoint: lazy.MigrationUtils.MIGRATION_ENTRYPOINTS.PASSWORDS,
    228      });
    229    } catch (ex) {
    230      console.error(ex);
    231    }
    232  }
    233 
    234  #importReportInit() {
    235    let reportData = lazy.LoginCSVImport.lastImportReport;
    236    this.sendAsyncMessage("AboutLogins:ImportReportData", reportData);
    237  }
    238 
    239  #getHelp() {
    240    const SUPPORT_URL =
    241      Services.urlFormatter.formatURLPref("app.support.baseURL") +
    242      "password-manager-remember-delete-edit-logins";
    243    this.#ownerGlobal.openWebLinkIn(SUPPORT_URL, "tab", {
    244      relatedToCurrent: true,
    245    });
    246  }
    247 
    248  #openPreferences() {
    249    this.#ownerGlobal.openPreferences("privacy-logins");
    250  }
    251 
    252  async #primaryPasswordRequest(messageId, reason) {
    253    if (!messageId) {
    254      throw new Error("AboutLogins:PrimaryPasswordRequest: no messageId.");
    255    }
    256    let messageText = { value: "NOT SUPPORTED" };
    257    let captionText = { value: "" };
    258 
    259    const isOSAuthEnabled = lazy.LoginHelper.getOSAuthEnabled();
    260 
    261    // This feature is only supported on Windows and macOS
    262    // but we still call in to OSKeyStore on Linux to get
    263    // the proper auth_details for Telemetry.
    264    // See bug 1614874 for Linux support.
    265    if (isOSAuthEnabled) {
    266      messageId += "-" + AppConstants.platform;
    267      [messageText, captionText] = await lazy.AboutLoginsL10n.formatMessages([
    268        {
    269          id: messageId,
    270        },
    271        {
    272          id: "about-logins-os-auth-dialog-caption",
    273        },
    274      ]);
    275    }
    276 
    277    let { isAuthorized, telemetryEvent } = await lazy.LoginHelper.requestReauth(
    278      this.browsingContext.embedderElement,
    279      isOSAuthEnabled,
    280      AboutLogins._authExpirationTime,
    281      messageText.value,
    282      captionText.value,
    283      reason
    284    );
    285    this.sendAsyncMessage("AboutLogins:PrimaryPasswordResponse", {
    286      result: isAuthorized,
    287      telemetryEvent,
    288    });
    289    if (isAuthorized) {
    290      AboutLogins._authExpirationTime = Date.now() + AUTH_TIMEOUT_MS;
    291      const remaskPasswords = () => {
    292        this.sendAsyncMessage("AboutLogins:RemaskPassword");
    293      };
    294      clearTimeout(_gPasswordRemaskTimeout);
    295      _gPasswordRemaskTimeout = setTimeout(remaskPasswords, AUTH_TIMEOUT_MS);
    296    }
    297  }
    298 
    299  async #subscribe() {
    300    AboutLogins._authExpirationTime = Number.NEGATIVE_INFINITY;
    301    AboutLogins.addObservers();
    302 
    303    const logins = await AboutLogins.getAllLogins();
    304    try {
    305      let syncState = await AboutLogins.getSyncState();
    306 
    307      let selectedSort = Services.prefs.getCharPref(
    308        "signon.management.page.sort",
    309        "name"
    310      );
    311      if (selectedSort == "breached") {
    312        // The "breached" value was used since Firefox 70 and
    313        // replaced with "alerts" in Firefox 76.
    314        selectedSort = "alerts";
    315      }
    316      this.sendAsyncMessage("AboutLogins:Setup", {
    317        logins,
    318        selectedSort,
    319        syncState,
    320        primaryPasswordEnabled: lazy.LoginHelper.isPrimaryPasswordSet(),
    321        passwordRevealVisible: Services.policies.isAllowed("passwordReveal"),
    322        importVisible:
    323          Services.policies.isAllowed("profileImport") &&
    324          AppConstants.platform != "linux",
    325        preselectedLogin: this.preselectedLogin,
    326        canCreateLogins: !Services.prefs.getBoolPref(NOCERTDB_PREF, false),
    327      });
    328 
    329      await AboutLogins.sendAllLoginRelatedObjects(
    330        logins,
    331        this.browsingContext
    332      );
    333    } catch (ex) {
    334      if (ex.result != Cr.NS_ERROR_NOT_INITIALIZED) {
    335        throw ex;
    336      }
    337 
    338      // The message manager may be destroyed before the replies can be sent.
    339      lazy.log.debug(
    340        "AboutLogins:Subscribe: exception when replying with logins",
    341        ex
    342      );
    343    }
    344  }
    345 
    346  async #updateLogin(loginUpdates) {
    347    let logins = await Services.logins.searchLoginsAsync({
    348      guid: loginUpdates.guid,
    349    });
    350    if (logins.length != 1) {
    351      lazy.log.warn(
    352        `AboutLogins:UpdateLogin: expected to find a login for guid: ${loginUpdates.guid} but found ${logins.length}`
    353      );
    354      return;
    355    }
    356 
    357    let modifiedLogin = logins[0].clone();
    358    if (loginUpdates.hasOwnProperty("username")) {
    359      modifiedLogin.username = loginUpdates.username;
    360    }
    361    if (loginUpdates.hasOwnProperty("password")) {
    362      modifiedLogin.password = loginUpdates.password;
    363    }
    364    try {
    365      await Services.logins.modifyLoginAsync(logins[0], modifiedLogin);
    366    } catch (error) {
    367      this.#handleLoginStorageErrors(modifiedLogin, error);
    368    }
    369  }
    370 
    371  async #exportPasswords() {
    372    let messageText = { value: "NOT SUPPORTED" };
    373    let captionText = { value: "" };
    374 
    375    const isOSAuthEnabled = lazy.LoginHelper.getOSAuthEnabled();
    376 
    377    // This feature is only supported on Windows and macOS
    378    // but we still call in to OSKeyStore on Linux to get
    379    // the proper auth_details for Telemetry.
    380    // See bug 1614874 for Linux support.
    381    if (isOSAuthEnabled) {
    382      const messageId =
    383        EXPORT_PASSWORD_OS_AUTH_DIALOG_MESSAGE_IDS[AppConstants.platform];
    384      if (!messageId) {
    385        throw new Error(
    386          `AboutLoginsParent: Cannot find l10n id for platform ${AppConstants.platform} for export passwords os auth dialog message`
    387        );
    388      }
    389      [messageText, captionText] = await lazy.AboutLoginsL10n.formatMessages([
    390        {
    391          id: messageId,
    392        },
    393        {
    394          id: "about-logins-os-auth-dialog-caption",
    395        },
    396      ]);
    397    }
    398 
    399    let reason = "export_logins";
    400    let { isAuthorized, telemetryEvent } = await lazy.LoginHelper.requestReauth(
    401      this.browsingContext.embedderElement,
    402      true,
    403      null, // Prompt regardless of a recent prompt
    404      messageText.value,
    405      captionText.value,
    406      reason
    407    );
    408 
    409    let { name, extra = {}, value = null } = telemetryEvent;
    410    if (value) {
    411      extra.value = value;
    412    }
    413    Glean.pwmgr[name].record(extra);
    414 
    415    if (!isAuthorized) {
    416      return;
    417    }
    418 
    419    if (!this.browsingContext.canOpenModalPicker) {
    420      // Prompting for os auth removed the focus from about:logins.
    421      // Waiting for about:logins window to re-gain the focus, because only
    422      // active browsing contexts are allowed to open the file picker.
    423      await this.sendQuery("AboutLogins:WaitForFocus");
    424    }
    425 
    426    let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
    427    function fpCallback(aResult) {
    428      if (aResult != Ci.nsIFilePicker.returnCancel) {
    429        lazy.LoginExport.exportAsCSV(fp.file.path);
    430        Glean.pwmgr.mgmtMenuItemUsedExportComplete.record();
    431      }
    432    }
    433    let [title, defaultFilename, okButtonLabel, csvFilterTitle] =
    434      await lazy.AboutLoginsL10n.formatValues([
    435        {
    436          id: "about-logins-export-file-picker-title2",
    437        },
    438        {
    439          id: "about-logins-export-file-picker-default-filename2",
    440        },
    441        {
    442          id: "about-logins-export-file-picker-export-button",
    443        },
    444        {
    445          id: "about-logins-export-file-picker-csv-filter-title",
    446        },
    447      ]);
    448 
    449    fp.init(this.browsingContext, title, Ci.nsIFilePicker.modeSave);
    450    fp.appendFilter(csvFilterTitle, "*.csv");
    451    fp.appendFilters(Ci.nsIFilePicker.filterAll);
    452    fp.defaultString = defaultFilename;
    453    fp.defaultExtension = "csv";
    454    fp.okButtonLabel = okButtonLabel;
    455    fp.open(fpCallback);
    456  }
    457 
    458  async #importFromFile() {
    459    let [title, okButtonLabel, csvFilterTitle, tsvFilterTitle] =
    460      await lazy.AboutLoginsL10n.formatValues([
    461        {
    462          id: "about-logins-import-file-picker-title2",
    463        },
    464        {
    465          id: "about-logins-import-file-picker-import-button",
    466        },
    467        {
    468          id: "about-logins-import-file-picker-csv-filter-title",
    469        },
    470        {
    471          id: "about-logins-import-file-picker-tsv-filter-title",
    472        },
    473      ]);
    474    let { result, path } = await this.openFilePickerDialog(
    475      title,
    476      okButtonLabel,
    477      [
    478        {
    479          title: csvFilterTitle,
    480          extensionPattern: "*.csv",
    481        },
    482        {
    483          title: tsvFilterTitle,
    484          extensionPattern: "*.tsv",
    485        },
    486      ]
    487    );
    488 
    489    if (result != Ci.nsIFilePicker.returnCancel) {
    490      let summary;
    491      try {
    492        summary = await lazy.LoginCSVImport.importFromCSV(path);
    493      } catch (e) {
    494        console.error(e);
    495        this.sendAsyncMessage(
    496          "AboutLogins:ImportPasswordsErrorDialog",
    497          e.errorType
    498        );
    499      }
    500      if (summary) {
    501        this.sendAsyncMessage("AboutLogins:ImportPasswordsDialog", summary);
    502        Glean.pwmgr.mgmtMenuItemUsedImportCsvComplete.record();
    503      }
    504    }
    505  }
    506 
    507  #removeAllLogins() {
    508    Services.logins.removeAllUserFacingLogins();
    509  }
    510 
    511  #handleLoginStorageErrors(login, error) {
    512    let messageObject = {
    513      login: augmentVanillaLoginObject(
    514        lazy.LoginHelper.loginToVanillaObject(login)
    515      ),
    516      errorMessage: error.message,
    517    };
    518 
    519    if (error.message.includes("This login already exists")) {
    520      // See comment in LoginHelper.createLoginAlreadyExistsError as to
    521      // why we need to call .toString() on the nsISupportsString.
    522      messageObject.existingLoginGuid = error.data.toString();
    523    }
    524 
    525    this.sendAsyncMessage("AboutLogins:ShowLoginItemError", messageObject);
    526  }
    527 
    528  async openFilePickerDialog(title, okButtonLabel, appendFilters) {
    529    return new Promise(resolve => {
    530      let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
    531      fp.init(this.browsingContext, title, Ci.nsIFilePicker.modeOpen);
    532      for (const appendFilter of appendFilters) {
    533        fp.appendFilter(appendFilter.title, appendFilter.extensionPattern);
    534      }
    535      fp.appendFilters(Ci.nsIFilePicker.filterAll);
    536      fp.okButtonLabel = okButtonLabel;
    537      fp.open(async result => {
    538        resolve({ result, path: fp.file.path });
    539      });
    540    });
    541  }
    542 }
    543 
    544 class AboutLoginsInternal {
    545  subscribers = new WeakSet();
    546  #observersAdded = false;
    547  authExpirationTime = Number.NEGATIVE_INFINITY;
    548 
    549  async observe(subject, topic, type) {
    550    if (!ChromeUtils.nondeterministicGetWeakSetKeys(this.subscribers).length) {
    551      this.#removeObservers();
    552      return;
    553    }
    554 
    555    switch (topic) {
    556      case "passwordmgr-reload-all": {
    557        await this.#reloadAllLogins();
    558        break;
    559      }
    560      case "passwordmgr-crypto-login": {
    561        this.#removeNotifications(PRIMARY_PASSWORD_NOTIFICATION_ID);
    562        await this.#reloadAllLogins();
    563        break;
    564      }
    565      case "passwordmgr-crypto-loginCanceled": {
    566        this.#showPrimaryPasswordLoginNotifications();
    567        break;
    568      }
    569      case lazy.UIState.ON_UPDATE: {
    570        this.#messageSubscribers(
    571          "AboutLogins:SyncState",
    572          await this.getSyncState()
    573        );
    574        break;
    575      }
    576      case "passwordmgr-storage-changed": {
    577        switch (type) {
    578          case "addLogin": {
    579            await this.#addLogin(subject);
    580            break;
    581          }
    582          case "modifyLogin": {
    583            this.#modifyLogin(subject);
    584            break;
    585          }
    586          case "removeLogin": {
    587            this.#removeLogin(subject);
    588            break;
    589          }
    590          case "removeAllLogins": {
    591            this.#removeAllLogins();
    592            break;
    593          }
    594        }
    595      }
    596    }
    597  }
    598 
    599  async #addLogin(subject) {
    600    const login = convertSubjectToLogin(subject);
    601    if (!login) {
    602      return;
    603    }
    604 
    605    if (lazy.BREACH_ALERTS_ENABLED) {
    606      this.#messageSubscribers(
    607        "AboutLogins:UpdateBreaches",
    608        await lazy.LoginBreaches.getPotentialBreachesByLoginGUID([login])
    609      );
    610      if (lazy.VULNERABLE_PASSWORDS_ENABLED) {
    611        this.#messageSubscribers(
    612          "AboutLogins:UpdateVulnerableLogins",
    613          await lazy.LoginBreaches.getPotentiallyVulnerablePasswordsByLoginGUID(
    614            [login]
    615          )
    616        );
    617      }
    618    }
    619 
    620    this.#messageSubscribers("AboutLogins:LoginAdded", login);
    621  }
    622 
    623  async #modifyLogin(subject) {
    624    subject.QueryInterface(Ci.nsIArrayExtensions);
    625    const login = convertSubjectToLogin(subject.GetElementAt(1));
    626    if (!login) {
    627      return;
    628    }
    629 
    630    if (lazy.BREACH_ALERTS_ENABLED) {
    631      let breachesForThisLogin =
    632        await lazy.LoginBreaches.getPotentialBreachesByLoginGUID([login]);
    633      let breachData = breachesForThisLogin.size
    634        ? breachesForThisLogin.get(login.guid)
    635        : false;
    636      this.#messageSubscribers(
    637        "AboutLogins:UpdateBreaches",
    638        new Map([[login.guid, breachData]])
    639      );
    640      if (lazy.VULNERABLE_PASSWORDS_ENABLED) {
    641        let vulnerablePasswordsForThisLogin =
    642          await lazy.LoginBreaches.getPotentiallyVulnerablePasswordsByLoginGUID(
    643            [login]
    644          );
    645        let isLoginVulnerable = !!vulnerablePasswordsForThisLogin.size;
    646        this.#messageSubscribers(
    647          "AboutLogins:UpdateVulnerableLogins",
    648          new Map([[login.guid, isLoginVulnerable]])
    649        );
    650      }
    651    }
    652 
    653    this.#messageSubscribers("AboutLogins:LoginModified", login);
    654  }
    655 
    656  #removeLogin(subject) {
    657    const login = convertSubjectToLogin(subject);
    658    if (!login) {
    659      return;
    660    }
    661    this.#messageSubscribers("AboutLogins:LoginRemoved", login);
    662  }
    663 
    664  #removeAllLogins() {
    665    this.#messageSubscribers("AboutLogins:RemoveAllLogins", []);
    666  }
    667 
    668  async #reloadAllLogins() {
    669    let logins = await this.getAllLogins();
    670    this.#messageSubscribers("AboutLogins:AllLogins", logins);
    671    await this.sendAllLoginRelatedObjects(logins);
    672  }
    673 
    674  #showPrimaryPasswordLoginNotifications() {
    675    this.#showNotifications({
    676      id: PRIMARY_PASSWORD_NOTIFICATION_ID,
    677      priority: "PRIORITY_WARNING_MEDIUM",
    678      iconURL: "chrome://browser/skin/login.svg",
    679      messageId: "about-logins-primary-password-notification-message",
    680      buttonIds: ["master-password-reload-button"],
    681      onClicks: [
    682        function onReloadClick(browser) {
    683          browser.reload();
    684        },
    685      ],
    686    });
    687    this.#messageSubscribers("AboutLogins:PrimaryPasswordAuthRequired");
    688  }
    689 
    690  #showNotifications({
    691    id,
    692    priority,
    693    iconURL,
    694    messageId,
    695    buttonIds,
    696    onClicks,
    697    extraFtl = [],
    698  } = {}) {
    699    for (let subscriber of this.#subscriberIterator()) {
    700      let browser = subscriber.embedderElement;
    701      let MozXULElement = browser.ownerGlobal.MozXULElement;
    702      MozXULElement.insertFTLIfNeeded("browser/aboutLogins.ftl");
    703      for (let ftl of extraFtl) {
    704        MozXULElement.insertFTLIfNeeded(ftl);
    705      }
    706 
    707      // If there's already an existing notification bar, don't do anything.
    708      let { gBrowser } = browser.ownerGlobal;
    709      let notificationBox = gBrowser.getNotificationBox(browser);
    710      let notification = notificationBox.getNotificationWithValue(id);
    711      if (notification) {
    712        continue;
    713      }
    714 
    715      let buttons = [];
    716      for (let i = 0; i < buttonIds.length; i++) {
    717        buttons[i] = {
    718          "l10n-id": buttonIds[i],
    719          popup: null,
    720          callback: () => {
    721            onClicks[i](browser);
    722          },
    723        };
    724      }
    725 
    726      notification = notificationBox.appendNotification(
    727        id,
    728        {
    729          label: { "l10n-id": messageId },
    730          image: iconURL,
    731          priority: notificationBox[priority],
    732        },
    733        buttons
    734      );
    735    }
    736  }
    737 
    738  #removeNotifications(notificationId) {
    739    for (let subscriber of this.#subscriberIterator()) {
    740      let browser = subscriber.embedderElement;
    741      let { gBrowser } = browser.ownerGlobal;
    742      let notificationBox = gBrowser.getNotificationBox(browser);
    743      let notification =
    744        notificationBox.getNotificationWithValue(notificationId);
    745      if (!notification) {
    746        continue;
    747      }
    748      notificationBox.removeNotification(notification);
    749    }
    750  }
    751 
    752  *#subscriberIterator() {
    753    let subscribers = ChromeUtils.nondeterministicGetWeakSetKeys(
    754      this.subscribers
    755    );
    756    for (let subscriber of subscribers) {
    757      let browser = subscriber.embedderElement;
    758      if (
    759        browser?.remoteType != EXPECTED_ABOUTLOGINS_REMOTE_TYPE ||
    760        browser?.contentPrincipal?.originNoSuffix != ABOUT_LOGINS_ORIGIN
    761      ) {
    762        this.subscribers.delete(subscriber);
    763        continue;
    764      }
    765      yield subscriber;
    766    }
    767  }
    768 
    769  #messageSubscribers(name, details) {
    770    for (let subscriber of this.#subscriberIterator()) {
    771      try {
    772        if (subscriber.currentWindowGlobal) {
    773          let actor = subscriber.currentWindowGlobal.getActor("AboutLogins");
    774          actor.sendAsyncMessage(name, details);
    775        }
    776      } catch (ex) {
    777        if (ex.result == Cr.NS_ERROR_NOT_INITIALIZED) {
    778          // The actor may be destroyed before the message is sent.
    779          lazy.log.debug(
    780            "messageSubscribers: exception when calling sendAsyncMessage",
    781            ex
    782          );
    783        } else {
    784          throw ex;
    785        }
    786      }
    787    }
    788  }
    789 
    790  async getAllLogins() {
    791    try {
    792      let logins = await lazy.LoginHelper.getAllUserFacingLogins();
    793      return logins
    794        .map(lazy.LoginHelper.loginToVanillaObject)
    795        .map(augmentVanillaLoginObject);
    796    } catch (e) {
    797      if (e.result == Cr.NS_ERROR_ABORT) {
    798        // If the user cancels the MP prompt then return no logins.
    799        return [];
    800      }
    801      throw e;
    802    }
    803  }
    804 
    805  async sendAllLoginRelatedObjects(logins, browsingContext) {
    806    let sendMessageFn = (name, details) => {
    807      if (browsingContext?.currentWindowGlobal) {
    808        let actor = browsingContext.currentWindowGlobal.getActor("AboutLogins");
    809        actor.sendAsyncMessage(name, details);
    810      } else {
    811        this.#messageSubscribers(name, details);
    812      }
    813    };
    814 
    815    if (lazy.BREACH_ALERTS_ENABLED) {
    816      sendMessageFn(
    817        "AboutLogins:SetBreaches",
    818        await lazy.LoginBreaches.getPotentialBreachesByLoginGUID(logins)
    819      );
    820      if (lazy.VULNERABLE_PASSWORDS_ENABLED) {
    821        sendMessageFn(
    822          "AboutLogins:SetVulnerableLogins",
    823          await lazy.LoginBreaches.getPotentiallyVulnerablePasswordsByLoginGUID(
    824            logins
    825          )
    826        );
    827      }
    828    }
    829  }
    830 
    831  async getSyncState() {
    832    const state = lazy.UIState.get();
    833    // As long as Sync is configured, about:logins will treat it as
    834    // authenticated. More diagnostics and error states can be handled
    835    // by other more Sync-specific pages.
    836    const loggedIn = state.status != lazy.UIState.STATUS_NOT_CONFIGURED;
    837    const passwordSyncEnabled = state.syncEnabled && lazy.PASSWORD_SYNC_ENABLED;
    838    const accountURL =
    839      await lazy.FxAccounts.config.promiseManageURI("password-manager");
    840 
    841    return {
    842      loggedIn,
    843      email: state.email,
    844      avatarURL: state.avatarURL,
    845      fxAccountsEnabled: lazy.FXA_ENABLED,
    846      passwordSyncEnabled,
    847      accountURL,
    848    };
    849  }
    850 
    851  async onPasswordSyncEnabledPreferenceChange(_data, _previous, _latest) {
    852    this.#messageSubscribers(
    853      "AboutLogins:SyncState",
    854      await this.getSyncState()
    855    );
    856  }
    857 
    858  #observedTopics = [
    859    "passwordmgr-crypto-login",
    860    "passwordmgr-crypto-loginCanceled",
    861    "passwordmgr-storage-changed",
    862    "passwordmgr-reload-all",
    863    lazy.UIState.ON_UPDATE,
    864  ];
    865 
    866  addObservers() {
    867    if (!this.#observersAdded) {
    868      for (const topic of this.#observedTopics) {
    869        Services.obs.addObserver(this, topic);
    870      }
    871      this.#observersAdded = true;
    872    }
    873  }
    874 
    875  #removeObservers() {
    876    for (const topic of this.#observedTopics) {
    877      Services.obs.removeObserver(this, topic);
    878    }
    879    this.#observersAdded = false;
    880  }
    881 }
    882 
    883 let AboutLogins = new AboutLoginsInternal();
    884 export var _AboutLogins = AboutLogins;
    885 
    886 XPCOMUtils.defineLazyPreferenceGetter(
    887  lazy,
    888  "PASSWORD_SYNC_ENABLED",
    889  "services.sync.engine.passwords",
    890  false,
    891  AboutLogins.onPasswordSyncEnabledPreferenceChange.bind(AboutLogins)
    892 );