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/**
6 * nsIAutoCompleteResult and nsILoginAutoCompleteSearch implementations for saved logins.
7 */
8
9"use strict";
10
11const EXPORTED_SYMBOLS = ["LoginAutoComplete", "LoginAutoCompleteResult"];
12
13const { XPCOMUtils } = ChromeUtils.import(
14  "resource://gre/modules/XPCOMUtils.jsm"
15);
16const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
17
18ChromeUtils.defineModuleGetter(
19  this,
20  "AutoCompleteChild",
21  "resource://gre/actors/AutoCompleteChild.jsm"
22);
23ChromeUtils.defineModuleGetter(
24  this,
25  "BrowserUtils",
26  "resource://gre/modules/BrowserUtils.jsm"
27);
28ChromeUtils.defineModuleGetter(
29  this,
30  "InsecurePasswordUtils",
31  "resource://gre/modules/InsecurePasswordUtils.jsm"
32);
33ChromeUtils.defineModuleGetter(
34  this,
35  "LoginFormFactory",
36  "resource://gre/modules/LoginFormFactory.jsm"
37);
38ChromeUtils.defineModuleGetter(
39  this,
40  "LoginHelper",
41  "resource://gre/modules/LoginHelper.jsm"
42);
43ChromeUtils.defineModuleGetter(
44  this,
45  "LoginManagerChild",
46  "resource://gre/modules/LoginManagerChild.jsm"
47);
48
49ChromeUtils.defineModuleGetter(
50  this,
51  "NewPasswordModel",
52  "resource://gre/modules/NewPasswordModel.jsm"
53);
54
55XPCOMUtils.defineLazyServiceGetter(
56  this,
57  "formFillController",
58  "@mozilla.org/satchel/form-fill-controller;1",
59  Ci.nsIFormFillController
60);
61XPCOMUtils.defineLazyPreferenceGetter(
62  this,
63  "SHOULD_SHOW_ORIGIN",
64  "signon.showAutoCompleteOrigins"
65);
66
67XPCOMUtils.defineLazyGetter(this, "log", () => {
68  return LoginHelper.createLogger("LoginAutoComplete");
69});
70XPCOMUtils.defineLazyGetter(this, "passwordMgrBundle", () => {
71  return Services.strings.createBundle(
72    "chrome://passwordmgr/locale/passwordmgr.properties"
73  );
74});
75XPCOMUtils.defineLazyGetter(this, "dateAndTimeFormatter", () => {
76  return new Services.intl.DateTimeFormat(undefined, {
77    dateStyle: "medium",
78  });
79});
80
81function loginSort(formHostPort, a, b) {
82  let maybeHostPortA = LoginHelper.maybeGetHostPortForURL(a.origin);
83  let maybeHostPortB = LoginHelper.maybeGetHostPortForURL(b.origin);
84  if (formHostPort == maybeHostPortA && formHostPort != maybeHostPortB) {
85    return -1;
86  }
87  if (formHostPort != maybeHostPortA && formHostPort == maybeHostPortB) {
88    return 1;
89  }
90
91  if (a.httpRealm !== b.httpRealm) {
92    // Sort HTTP auth. logins after form logins for the same origin.
93    if (b.httpRealm === null) {
94      return 1;
95    }
96    if (a.httpRealm === null) {
97      return -1;
98    }
99  }
100
101  let userA = a.username.toLowerCase();
102  let userB = b.username.toLowerCase();
103
104  if (userA < userB) {
105    return -1;
106  }
107
108  if (userA > userB) {
109    return 1;
110  }
111
112  return 0;
113}
114
115function findDuplicates(loginList) {
116  let seen = new Set();
117  let duplicates = new Set();
118  for (let login of loginList) {
119    if (seen.has(login.username)) {
120      duplicates.add(login.username);
121    }
122    seen.add(login.username);
123  }
124  return duplicates;
125}
126
127function getLocalizedString(key, formatArgs = null) {
128  if (formatArgs) {
129    return passwordMgrBundle.formatStringFromName(key, formatArgs);
130  }
131  return passwordMgrBundle.GetStringFromName(key);
132}
133
134class AutocompleteItem {
135  constructor(style) {
136    this.comment = "";
137    this.style = style;
138    this.value = "";
139  }
140
141  removeFromStorage() {
142    /* Do nothing by default */
143  }
144}
145
146class InsecureLoginFormAutocompleteItem extends AutocompleteItem {
147  constructor() {
148    super("insecureWarning");
149
150    XPCOMUtils.defineLazyGetter(this, "label", () => {
151      let learnMoreString = getLocalizedString("insecureFieldWarningLearnMore");
152      return getLocalizedString("insecureFieldWarningDescription2", [
153        learnMoreString,
154      ]);
155    });
156  }
157}
158
159class LoginAutocompleteItem extends AutocompleteItem {
160  constructor(
161    login,
162    hasBeenTypePassword,
163    duplicateUsernames,
164    actor,
165    isOriginMatched
166  ) {
167    super(SHOULD_SHOW_ORIGIN ? "loginWithOrigin" : "login");
168    this._login = login.QueryInterface(Ci.nsILoginMetaInfo);
169    this._actor = actor;
170
171    this._isDuplicateUsername =
172      login.username && duplicateUsernames.has(login.username);
173
174    XPCOMUtils.defineLazyGetter(this, "label", () => {
175      let username = login.username;
176      // If login is empty or duplicated we want to append a modification date to it.
177      if (!username || this._isDuplicateUsername) {
178        if (!username) {
179          username = getLocalizedString("noUsername");
180        }
181        let time = dateAndTimeFormatter.format(
182          new Date(login.timePasswordChanged)
183        );
184        username = getLocalizedString("loginHostAge", [username, time]);
185      }
186      return username;
187    });
188
189    XPCOMUtils.defineLazyGetter(this, "value", () => {
190      return hasBeenTypePassword ? login.password : login.username;
191    });
192
193    XPCOMUtils.defineLazyGetter(this, "comment", () => {
194      return JSON.stringify({
195        guid: login.guid,
196        login,
197        isDuplicateUsername: this._isDuplicateUsername,
198        isOriginMatched,
199        comment:
200          isOriginMatched && login.httpRealm === null
201            ? getLocalizedString("displaySameOrigin")
202            : login.displayOrigin,
203      });
204    });
205  }
206
207  removeFromStorage() {
208    if (this._actor) {
209      let vanilla = LoginHelper.loginToVanillaObject(this._login);
210      this._actor.sendAsyncMessage("PasswordManager:removeLogin", {
211        login: vanilla,
212      });
213    } else {
214      Services.logins.removeLogin(this._login);
215    }
216  }
217}
218
219class GeneratedPasswordAutocompleteItem extends AutocompleteItem {
220  constructor(generatedPassword, willAutoSaveGeneratedPassword) {
221    super("generatedPassword");
222    XPCOMUtils.defineLazyGetter(this, "comment", () => {
223      return JSON.stringify({
224        generatedPassword,
225        willAutoSaveGeneratedPassword,
226      });
227    });
228    this.value = generatedPassword;
229
230    XPCOMUtils.defineLazyGetter(this, "label", () => {
231      return getLocalizedString("useASecurelyGeneratedPassword");
232    });
233  }
234}
235
236class ImportableLoginsAutocompleteItem extends AutocompleteItem {
237  constructor(browserId, hostname) {
238    super("importableLogins");
239    this.label = browserId;
240    this.comment = hostname;
241  }
242
243  removeFromStorage() {
244    Services.telemetry.recordEvent("exp_import", "event", "delete", this.label);
245  }
246}
247
248class LoginsFooterAutocompleteItem extends AutocompleteItem {
249  constructor(formHostname, telemetryEventData) {
250    super("loginsFooter");
251    XPCOMUtils.defineLazyGetter(this, "comment", () => {
252      // The comment field of `loginsFooter` results have many additional pieces of
253      // information for telemetry purposes. After bug 1555209, this information
254      // can be passed to the parent process outside of nsIAutoCompleteResult APIs
255      // so we won't need this hack.
256      return JSON.stringify({
257        ...telemetryEventData,
258        formHostname,
259      });
260    });
261
262    XPCOMUtils.defineLazyGetter(this, "label", () => {
263      return getLocalizedString("viewSavedLogins.label");
264    });
265  }
266}
267
268// nsIAutoCompleteResult implementation
269function LoginAutoCompleteResult(
270  aSearchString,
271  matchingLogins,
272  formOrigin,
273  {
274    generatedPassword,
275    willAutoSaveGeneratedPassword,
276    importable,
277    isSecure,
278    actor,
279    hasBeenTypePassword,
280    hostname,
281    telemetryEventData,
282  }
283) {
284  let hidingFooterOnPWFieldAutoOpened = false;
285  const importableBrowsers =
286    importable?.state === "import" && importable?.browsers;
287  function isFooterEnabled() {
288    // We need to check LoginHelper.enabled here since the insecure warning should
289    // appear even if pwmgr is disabled but the footer should never appear in that case.
290    if (!LoginHelper.showAutoCompleteFooter || !LoginHelper.enabled) {
291      return false;
292    }
293
294    // Don't show the footer on non-empty password fields as it's not providing
295    // value and only adding noise since a password was already filled.
296    if (hasBeenTypePassword && aSearchString && !generatedPassword) {
297      log.debug("Hiding footer: non-empty password field");
298      return false;
299    }
300
301    if (
302      !importableBrowsers &&
303      !matchingLogins.length &&
304      !generatedPassword &&
305      hasBeenTypePassword &&
306      formFillController.passwordPopupAutomaticallyOpened
307    ) {
308      hidingFooterOnPWFieldAutoOpened = true;
309      log.debug(
310        "Hiding footer: no logins and the popup was opened upon focus of the pw. field"
311      );
312      return false;
313    }
314
315    return true;
316  }
317
318  this.searchString = aSearchString;
319
320  // Build up the array of autocomplete rows to display.
321  this._rows = [];
322
323  // Insecure field warning comes first if it applies and is enabled.
324  if (!isSecure && LoginHelper.showInsecureFieldWarning) {
325    this._rows.push(new InsecureLoginFormAutocompleteItem());
326  }
327
328  // Saved login items
329  let formHostPort = LoginHelper.maybeGetHostPortForURL(formOrigin);
330  let logins = matchingLogins.sort(loginSort.bind(null, formHostPort));
331  let duplicateUsernames = findDuplicates(matchingLogins);
332
333  for (let login of logins) {
334    let item = new LoginAutocompleteItem(
335      login,
336      hasBeenTypePassword,
337      duplicateUsernames,
338      actor,
339      LoginHelper.isOriginMatching(login.origin, formOrigin, {
340        schemeUpgrades: LoginHelper.schemeUpgrades,
341      })
342    );
343    this._rows.push(item);
344  }
345
346  // The footer comes last if it's enabled
347  if (isFooterEnabled()) {
348    if (generatedPassword) {
349      this._rows.push(
350        new GeneratedPasswordAutocompleteItem(
351          generatedPassword,
352          willAutoSaveGeneratedPassword
353        )
354      );
355    }
356
357    // Suggest importing logins if there are none found.
358    if (!logins.length && importableBrowsers) {
359      this._rows.push(
360        ...importableBrowsers.map(
361          browserId => new ImportableLoginsAutocompleteItem(browserId, hostname)
362        )
363      );
364    }
365
366    this._rows.push(
367      new LoginsFooterAutocompleteItem(hostname, telemetryEventData)
368    );
369  }
370
371  // Determine the result code and default index.
372  if (this.matchCount > 0) {
373    this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
374    this.defaultIndex = 0;
375    // For experiment telemetry, record how many importable logins were
376    // available when showing the popup and some extra data.
377    Services.telemetry.recordEvent(
378      "exp_import",
379      "impression",
380      "popup",
381      (importable?.browsers?.length ?? 0) + "",
382      {
383        loginsCount: logins.length + "",
384        searchLength: aSearchString.length + "",
385      }
386    );
387  } else if (hidingFooterOnPWFieldAutoOpened) {
388    // We use a failure result so that the empty results aren't re-used for when
389    // the user tries to manually open the popup (we want the footer in that case).
390    this.searchResult = Ci.nsIAutoCompleteResult.RESULT_FAILURE;
391    this.defaultIndex = -1;
392  }
393}
394
395LoginAutoCompleteResult.prototype = {
396  QueryInterface: ChromeUtils.generateQI([
397    Ci.nsIAutoCompleteResult,
398    Ci.nsISupportsWeakReference,
399  ]),
400
401  /**
402   * Accessed via .wrappedJSObject
403   * @private
404   */
405  get logins() {
406    return this._rows
407      .filter(item => {
408        return item.constructor === LoginAutocompleteItem;
409      })
410      .map(item => item._login);
411  },
412
413  // Allow autoCompleteSearch to get at the JS object so it can
414  // modify some readonly properties for internal use.
415  get wrappedJSObject() {
416    return this;
417  },
418
419  // Interfaces from idl...
420  searchString: null,
421  searchResult: Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
422  defaultIndex: -1,
423  errorDescription: "",
424  get matchCount() {
425    return this._rows.length;
426  },
427
428  getValueAt(index) {
429    if (index < 0 || index >= this.matchCount) {
430      throw new Error("Index out of range.");
431    }
432    return this._rows[index].value;
433  },
434
435  getLabelAt(index) {
436    if (index < 0 || index >= this.matchCount) {
437      throw new Error("Index out of range.");
438    }
439    return this._rows[index].label;
440  },
441
442  getCommentAt(index) {
443    if (index < 0 || index >= this.matchCount) {
444      throw new Error("Index out of range.");
445    }
446    return this._rows[index].comment;
447  },
448
449  getStyleAt(index) {
450    return this._rows[index].style;
451  },
452
453  getImageAt(index) {
454    return "";
455  },
456
457  getFinalCompleteValueAt(index) {
458    return this.getValueAt(index);
459  },
460
461  removeValueAt(index) {
462    if (index < 0 || index >= this.matchCount) {
463      throw new Error("Index out of range.");
464    }
465
466    let [removedItem] = this._rows.splice(index, 1);
467
468    if (this.defaultIndex > this._rows.length) {
469      this.defaultIndex--;
470    }
471
472    removedItem.removeFromStorage();
473  },
474};
475
476function LoginAutoComplete() {
477  // HTMLInputElement to number, the element's new-password heuristic confidence score
478  this._cachedNewPasswordScore = new WeakMap();
479}
480LoginAutoComplete.prototype = {
481  classID: Components.ID("{2bdac17c-53f1-4896-a521-682ccdeef3a8}"),
482  QueryInterface: ChromeUtils.generateQI([Ci.nsILoginAutoCompleteSearch]),
483
484  _autoCompleteLookupPromise: null,
485  _cachedNewPasswordScore: null,
486
487  /**
488   * Yuck. This is called directly by satchel:
489   * nsFormFillController::StartSearch()
490   * [toolkit/components/satchel/nsFormFillController.cpp]
491   *
492   * We really ought to have a simple way for code to register an
493   * auto-complete provider, and not have satchel calling pwmgr directly.
494   *
495   * @param {string} aSearchString The value typed in the field.
496   * @param {nsIAutoCompleteResult} aPreviousResult
497   * @param {HTMLInputElement} aElement
498   * @param {nsIFormAutoCompleteObserver} aCallback
499   */
500  startSearch(aSearchString, aPreviousResult, aElement, aCallback) {
501    let { isNullPrincipal } = aElement.nodePrincipal;
502    if (aElement.nodePrincipal.schemeIs("about")) {
503      // Don't show autocomplete results for about: pages.
504      // XXX: Don't we need to call the callback here?
505      return;
506    }
507
508    let searchStartTimeMS = Services.telemetry.msSystemNow();
509
510    // Show the insecure login warning in the passwords field on null principal documents.
511    let isSecure = !isNullPrincipal;
512    // Avoid loading InsecurePasswordUtils.jsm in a sandboxed document (e.g. an ad. frame) if we
513    // already know it has a null principal and will therefore get the insecure autocomplete
514    // treatment.
515    // InsecurePasswordUtils doesn't handle the null principal case as not secure because we don't
516    // want the same treatment:
517    // * The web console warnings will be confusing (as they're primarily about http:) and not very
518    //   useful if the developer intentionally sandboxed the document.
519    // * The site identity insecure field warning would require LoginManagerChild being loaded and
520    //   listening to some of the DOM events we're ignoring in null principal documents. For memory
521    //   reasons it's better to not load LMC at all for these sandboxed frames. Also, if the top-
522    //   document is sandboxing a document, it probably doesn't want that sandboxed document to be
523    //   able to affect the identity icon in the address bar by adding a password field.
524    let form = LoginFormFactory.createFromField(aElement);
525    if (isSecure) {
526      isSecure = InsecurePasswordUtils.isFormSecure(form);
527    }
528    let { hasBeenTypePassword } = aElement;
529    let hostname = aElement.ownerDocument.documentURIObject.host;
530    let formOrigin = LoginHelper.getLoginOrigin(
531      aElement.ownerDocument.documentURI
532    );
533
534    let loginManagerActor = LoginManagerChild.forWindow(aElement.ownerGlobal);
535
536    let completeSearch = async autoCompleteLookupPromise => {
537      // Assign to the member synchronously before awaiting the Promise.
538      this._autoCompleteLookupPromise = autoCompleteLookupPromise;
539
540      let {
541        generatedPassword,
542        importable,
543        logins,
544        willAutoSaveGeneratedPassword,
545      } = await autoCompleteLookupPromise;
546
547      // If the search was canceled before we got our
548      // results, don't bother reporting them.
549      // N.B. This check must occur after the `await` above for it to be
550      // effective.
551      if (this._autoCompleteLookupPromise !== autoCompleteLookupPromise) {
552        log.debug("ignoring result from previous search");
553        return;
554      }
555
556      let telemetryEventData = {
557        acFieldName: aElement.getAutocompleteInfo().fieldName,
558        hadPrevious: !!aPreviousResult,
559        typeWasPassword: aElement.hasBeenTypePassword,
560        fieldType: aElement.type,
561        searchStartTimeMS,
562        stringLength: aSearchString.length,
563      };
564
565      this._autoCompleteLookupPromise = null;
566      let results = new LoginAutoCompleteResult(
567        aSearchString,
568        logins,
569        formOrigin,
570        {
571          generatedPassword,
572          willAutoSaveGeneratedPassword,
573          importable,
574          actor: loginManagerActor,
575          isSecure,
576          hasBeenTypePassword,
577          hostname,
578          telemetryEventData,
579        }
580      );
581      aCallback.onSearchCompletion(results);
582    };
583
584    if (isNullPrincipal) {
585      // Don't search login storage when the field has a null principal as we don't want to fill
586      // logins for the `location` in this case.
587      completeSearch(Promise.resolve({ logins: [] }));
588      return;
589    }
590
591    if (
592      hasBeenTypePassword &&
593      aSearchString &&
594      !loginManagerActor.isPasswordGenerationForcedOn(aElement)
595    ) {
596      // Return empty result on password fields with password already filled,
597      // unless password generation was forced.
598      completeSearch(Promise.resolve({ logins: [] }));
599      return;
600    }
601
602    if (!LoginHelper.enabled) {
603      completeSearch(Promise.resolve({ logins: [] }));
604      return;
605    }
606
607    let previousResult;
608    if (aPreviousResult) {
609      previousResult = {
610        searchString: aPreviousResult.searchString,
611        logins: LoginHelper.loginsToVanillaObjects(
612          aPreviousResult.wrappedJSObject.logins
613        ),
614      };
615    } else {
616      previousResult = null;
617    }
618
619    let acLookupPromise = this._requestAutoCompleteResultsFromParent({
620      searchString: aSearchString,
621      previousResult,
622      inputElement: aElement,
623      form,
624      formOrigin,
625      hasBeenTypePassword,
626    });
627    completeSearch(acLookupPromise).catch(log.error.bind(log));
628  },
629
630  stopSearch() {
631    this._autoCompleteLookupPromise = null;
632  },
633
634  async _requestAutoCompleteResultsFromParent({
635    searchString,
636    previousResult,
637    inputElement,
638    form,
639    formOrigin,
640    hasBeenTypePassword,
641  }) {
642    let actionOrigin = LoginHelper.getFormActionOrigin(form);
643    let autocompleteInfo = inputElement.getAutocompleteInfo();
644
645    let loginManagerActor = LoginManagerChild.forWindow(
646      inputElement.ownerGlobal
647    );
648    let forcePasswordGeneration = false;
649    let isProbablyANewPasswordField = false;
650    if (hasBeenTypePassword) {
651      forcePasswordGeneration = loginManagerActor.isPasswordGenerationForcedOn(
652        inputElement
653      );
654      // Run the Fathom model only if the password field does not have the
655      // autocomplete="new-password" attribute.
656      isProbablyANewPasswordField =
657        autocompleteInfo.fieldName == "new-password" ||
658        this._isProbablyANewPasswordField(inputElement);
659    }
660
661    let messageData = {
662      formOrigin,
663      actionOrigin,
664      searchString,
665      previousResult,
666      forcePasswordGeneration,
667      hasBeenTypePassword,
668      isSecure: InsecurePasswordUtils.isFormSecure(form),
669      isProbablyANewPasswordField,
670    };
671
672    if (LoginHelper.showAutoCompleteFooter) {
673      gAutoCompleteListener.init();
674    }
675
676    log.debug("LoginAutoComplete search:", {
677      forcePasswordGeneration,
678      isSecure: messageData.isSecure,
679      hasBeenTypePassword,
680      isProbablyANewPasswordField,
681      searchString: hasBeenTypePassword
682        ? "*".repeat(searchString.length)
683        : searchString,
684    });
685
686    let result = await loginManagerActor.sendQuery(
687      "PasswordManager:autoCompleteLogins",
688      messageData
689    );
690
691    return {
692      generatedPassword: result.generatedPassword,
693      importable: result.importable,
694      logins: LoginHelper.vanillaObjectsToLogins(result.logins),
695      willAutoSaveGeneratedPassword: result.willAutoSaveGeneratedPassword,
696    };
697  },
698
699  _isProbablyANewPasswordField(inputElement) {
700    const threshold = LoginHelper.generationConfidenceThreshold;
701    if (threshold == -1) {
702      // Fathom is disabled
703      return false;
704    }
705
706    let score = this._cachedNewPasswordScore.get(inputElement);
707    if (score) {
708      return score >= threshold;
709    }
710
711    const { rules, type } = NewPasswordModel;
712    const results = rules.against(inputElement);
713    score = results.get(inputElement).scoreFor(type);
714    this._cachedNewPasswordScore.set(inputElement, score);
715    return score >= threshold;
716  },
717};
718
719let gAutoCompleteListener = {
720  // Input element on which enter keydown event was fired.
721  keyDownEnterForInput: null,
722
723  added: false,
724
725  init() {
726    if (!this.added) {
727      AutoCompleteChild.addPopupStateListener(this);
728      this.added = true;
729    }
730  },
731
732  popupStateChanged(messageName, data, target) {
733    switch (messageName) {
734      case "FormAutoComplete:PopupOpened": {
735        let { chromeEventHandler } = target.docShell;
736        chromeEventHandler.addEventListener("keydown", this, true);
737        break;
738      }
739
740      case "FormAutoComplete:PopupClosed": {
741        this.onPopupClosed(data, target);
742        let { chromeEventHandler } = target.docShell;
743        chromeEventHandler.removeEventListener("keydown", this, true);
744        break;
745      }
746    }
747  },
748
749  handleEvent(event) {
750    if (event.type != "keydown") {
751      return;
752    }
753
754    let focusedElement = formFillController.focusedInput;
755    if (
756      event.keyCode != event.DOM_VK_RETURN ||
757      focusedElement != event.target
758    ) {
759      this.keyDownEnterForInput = null;
760      return;
761    }
762    this.keyDownEnterForInput = focusedElement;
763  },
764
765  onPopupClosed({ selectedRowComment, selectedRowStyle }, window) {
766    let focusedElement = formFillController.focusedInput;
767    let eventTarget = this.keyDownEnterForInput;
768    this.keyDownEnterForInput = null;
769    if (!eventTarget || eventTarget !== focusedElement) {
770      return;
771    }
772
773    let loginManager = window.windowGlobalChild.getActor("LoginManager");
774    switch (selectedRowStyle) {
775      case "importableLogins":
776        loginManager.sendAsyncMessage(
777          "PasswordManager:OpenMigrationWizard",
778          selectedRowComment
779        );
780        Services.telemetry.recordEvent(
781          "exp_import",
782          "event",
783          "enter",
784          selectedRowComment
785        );
786        break;
787      case "loginsFooter":
788        loginManager.sendAsyncMessage("PasswordManager:OpenPreferences", {
789          entryPoint: "autocomplete",
790        });
791        Services.telemetry.recordEvent(
792          "exp_import",
793          "event",
794          "enter",
795          "loginsFooter"
796        );
797        break;
798    }
799  },
800};
801