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 ImportableLearnMoreAutocompleteItem extends AutocompleteItem {
237  constructor() {
238    super("importableLearnMore");
239  }
240}
241
242class ImportableLoginsAutocompleteItem extends AutocompleteItem {
243  constructor(browserId, hostname, actor) {
244    super("importableLogins");
245    this.label = browserId;
246    this.comment = hostname;
247    this._actor = actor;
248
249    // This is sent for every item (re)shown, but the parent will debounce to
250    // reduce the count by 1 total.
251    this._actor.sendAsyncMessage(
252      "PasswordManager:decreaseSuggestImportCount",
253      1
254    );
255  }
256
257  removeFromStorage() {
258    this._actor.sendAsyncMessage(
259      "PasswordManager:decreaseSuggestImportCount",
260      100
261    );
262  }
263}
264
265class LoginsFooterAutocompleteItem extends AutocompleteItem {
266  constructor(formHostname, telemetryEventData) {
267    super("loginsFooter");
268    XPCOMUtils.defineLazyGetter(this, "comment", () => {
269      // The comment field of `loginsFooter` results have many additional pieces of
270      // information for telemetry purposes. After bug 1555209, this information
271      // can be passed to the parent process outside of nsIAutoCompleteResult APIs
272      // so we won't need this hack.
273      return JSON.stringify({
274        ...telemetryEventData,
275        formHostname,
276      });
277    });
278
279    XPCOMUtils.defineLazyGetter(this, "label", () => {
280      return getLocalizedString("viewSavedLogins.label");
281    });
282  }
283}
284
285// nsIAutoCompleteResult implementation
286function LoginAutoCompleteResult(
287  aSearchString,
288  matchingLogins,
289  formOrigin,
290  {
291    generatedPassword,
292    willAutoSaveGeneratedPassword,
293    importable,
294    isSecure,
295    actor,
296    hasBeenTypePassword,
297    hostname,
298    telemetryEventData,
299  }
300) {
301  let hidingFooterOnPWFieldAutoOpened = false;
302  const importableBrowsers =
303    importable?.state === "import" && importable?.browsers;
304  function isFooterEnabled() {
305    // We need to check LoginHelper.enabled here since the insecure warning should
306    // appear even if pwmgr is disabled but the footer should never appear in that case.
307    if (!LoginHelper.showAutoCompleteFooter || !LoginHelper.enabled) {
308      return false;
309    }
310
311    // Don't show the footer on non-empty password fields as it's not providing
312    // value and only adding noise since a password was already filled.
313    if (hasBeenTypePassword && aSearchString && !generatedPassword) {
314      log.debug("Hiding footer: non-empty password field");
315      return false;
316    }
317
318    if (
319      !importableBrowsers &&
320      !matchingLogins.length &&
321      !generatedPassword &&
322      hasBeenTypePassword &&
323      formFillController.passwordPopupAutomaticallyOpened
324    ) {
325      hidingFooterOnPWFieldAutoOpened = true;
326      log.debug(
327        "Hiding footer: no logins and the popup was opened upon focus of the pw. field"
328      );
329      return false;
330    }
331
332    return true;
333  }
334
335  this.searchString = aSearchString;
336
337  // Build up the array of autocomplete rows to display.
338  this._rows = [];
339
340  // Insecure field warning comes first if it applies and is enabled.
341  if (!isSecure && LoginHelper.showInsecureFieldWarning) {
342    this._rows.push(new InsecureLoginFormAutocompleteItem());
343  }
344
345  // Saved login items
346  let formHostPort = LoginHelper.maybeGetHostPortForURL(formOrigin);
347  let logins = matchingLogins.sort(loginSort.bind(null, formHostPort));
348  let duplicateUsernames = findDuplicates(matchingLogins);
349
350  for (let login of logins) {
351    let item = new LoginAutocompleteItem(
352      login,
353      hasBeenTypePassword,
354      duplicateUsernames,
355      actor,
356      LoginHelper.isOriginMatching(login.origin, formOrigin, {
357        schemeUpgrades: LoginHelper.schemeUpgrades,
358      })
359    );
360    this._rows.push(item);
361  }
362
363  // The footer comes last if it's enabled
364  if (isFooterEnabled()) {
365    if (generatedPassword) {
366      this._rows.push(
367        new GeneratedPasswordAutocompleteItem(
368          generatedPassword,
369          willAutoSaveGeneratedPassword
370        )
371      );
372    }
373
374    // Suggest importing logins if there are none found.
375    if (!logins.length && importableBrowsers) {
376      this._rows.push(
377        ...importableBrowsers.map(
378          browserId =>
379            new ImportableLoginsAutocompleteItem(browserId, hostname, actor)
380        )
381      );
382      this._rows.push(new ImportableLearnMoreAutocompleteItem());
383    }
384
385    this._rows.push(
386      new LoginsFooterAutocompleteItem(hostname, telemetryEventData)
387    );
388  }
389
390  // Determine the result code and default index.
391  if (this.matchCount > 0) {
392    this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
393    this.defaultIndex = 0;
394  } else if (hidingFooterOnPWFieldAutoOpened) {
395    // We use a failure result so that the empty results aren't re-used for when
396    // the user tries to manually open the popup (we want the footer in that case).
397    this.searchResult = Ci.nsIAutoCompleteResult.RESULT_FAILURE;
398    this.defaultIndex = -1;
399  }
400}
401
402LoginAutoCompleteResult.prototype = {
403  QueryInterface: ChromeUtils.generateQI([
404    "nsIAutoCompleteResult",
405    "nsISupportsWeakReference",
406  ]),
407
408  /**
409   * Accessed via .wrappedJSObject
410   * @private
411   */
412  get logins() {
413    return this._rows
414      .filter(item => {
415        return item.constructor === LoginAutocompleteItem;
416      })
417      .map(item => item._login);
418  },
419
420  // Allow autoCompleteSearch to get at the JS object so it can
421  // modify some readonly properties for internal use.
422  get wrappedJSObject() {
423    return this;
424  },
425
426  // Interfaces from idl...
427  searchString: null,
428  searchResult: Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
429  defaultIndex: -1,
430  errorDescription: "",
431  get matchCount() {
432    return this._rows.length;
433  },
434
435  getValueAt(index) {
436    if (index < 0 || index >= this.matchCount) {
437      throw new Error("Index out of range.");
438    }
439    return this._rows[index].value;
440  },
441
442  getLabelAt(index) {
443    if (index < 0 || index >= this.matchCount) {
444      throw new Error("Index out of range.");
445    }
446    return this._rows[index].label;
447  },
448
449  getCommentAt(index) {
450    if (index < 0 || index >= this.matchCount) {
451      throw new Error("Index out of range.");
452    }
453    return this._rows[index].comment;
454  },
455
456  getStyleAt(index) {
457    return this._rows[index].style;
458  },
459
460  getImageAt(index) {
461    return "";
462  },
463
464  getFinalCompleteValueAt(index) {
465    return this.getValueAt(index);
466  },
467
468  removeValueAt(index) {
469    if (index < 0 || index >= this.matchCount) {
470      throw new Error("Index out of range.");
471    }
472
473    let [removedItem] = this._rows.splice(index, 1);
474
475    if (this.defaultIndex > this._rows.length) {
476      this.defaultIndex--;
477    }
478
479    removedItem.removeFromStorage();
480  },
481};
482
483function LoginAutoComplete() {
484  // HTMLInputElement to number, the element's new-password heuristic confidence score
485  this._cachedNewPasswordScore = new WeakMap();
486}
487LoginAutoComplete.prototype = {
488  classID: Components.ID("{2bdac17c-53f1-4896-a521-682ccdeef3a8}"),
489  QueryInterface: ChromeUtils.generateQI(["nsILoginAutoCompleteSearch"]),
490
491  _autoCompleteLookupPromise: null,
492  _cachedNewPasswordScore: null,
493
494  /**
495   * Yuck. This is called directly by satchel:
496   * nsFormFillController::StartSearch()
497   * [toolkit/components/satchel/nsFormFillController.cpp]
498   *
499   * We really ought to have a simple way for code to register an
500   * auto-complete provider, and not have satchel calling pwmgr directly.
501   *
502   * @param {string} aSearchString The value typed in the field.
503   * @param {nsIAutoCompleteResult} aPreviousResult
504   * @param {HTMLInputElement} aElement
505   * @param {nsIFormAutoCompleteObserver} aCallback
506   */
507  startSearch(aSearchString, aPreviousResult, aElement, aCallback) {
508    let { isNullPrincipal } = aElement.nodePrincipal;
509    if (aElement.nodePrincipal.schemeIs("about")) {
510      // Don't show autocomplete results for about: pages.
511      // XXX: Don't we need to call the callback here?
512      return;
513    }
514
515    let searchStartTimeMS = Services.telemetry.msSystemNow();
516
517    // Show the insecure login warning in the passwords field on null principal documents.
518    let isSecure = !isNullPrincipal;
519    // Avoid loading InsecurePasswordUtils.jsm in a sandboxed document (e.g. an ad. frame) if we
520    // already know it has a null principal and will therefore get the insecure autocomplete
521    // treatment.
522    // InsecurePasswordUtils doesn't handle the null principal case as not secure because we don't
523    // want the same treatment:
524    // * The web console warnings will be confusing (as they're primarily about http:) and not very
525    //   useful if the developer intentionally sandboxed the document.
526    // * The site identity insecure field warning would require LoginManagerChild being loaded and
527    //   listening to some of the DOM events we're ignoring in null principal documents. For memory
528    //   reasons it's better to not load LMC at all for these sandboxed frames. Also, if the top-
529    //   document is sandboxing a document, it probably doesn't want that sandboxed document to be
530    //   able to affect the identity icon in the address bar by adding a password field.
531    let form = LoginFormFactory.createFromField(aElement);
532    if (isSecure) {
533      isSecure = InsecurePasswordUtils.isFormSecure(form);
534    }
535    let { hasBeenTypePassword } = aElement;
536    let hostname = aElement.ownerDocument.documentURIObject.host;
537    let formOrigin = LoginHelper.getLoginOrigin(
538      aElement.ownerDocument.documentURI
539    );
540
541    let loginManagerActor = LoginManagerChild.forWindow(aElement.ownerGlobal);
542
543    let completeSearch = async autoCompleteLookupPromise => {
544      // Assign to the member synchronously before awaiting the Promise.
545      this._autoCompleteLookupPromise = autoCompleteLookupPromise;
546
547      let {
548        generatedPassword,
549        importable,
550        logins,
551        willAutoSaveGeneratedPassword,
552      } = await autoCompleteLookupPromise;
553
554      // If the search was canceled before we got our
555      // results, don't bother reporting them.
556      // N.B. This check must occur after the `await` above for it to be
557      // effective.
558      if (this._autoCompleteLookupPromise !== autoCompleteLookupPromise) {
559        log.debug("ignoring result from previous search");
560        return;
561      }
562
563      let telemetryEventData = {
564        acFieldName: aElement.getAutocompleteInfo().fieldName,
565        hadPrevious: !!aPreviousResult,
566        typeWasPassword: aElement.hasBeenTypePassword,
567        fieldType: aElement.type,
568        searchStartTimeMS,
569        stringLength: aSearchString.length,
570      };
571
572      this._autoCompleteLookupPromise = null;
573      let results = new LoginAutoCompleteResult(
574        aSearchString,
575        logins,
576        formOrigin,
577        {
578          generatedPassword,
579          willAutoSaveGeneratedPassword,
580          importable,
581          actor: loginManagerActor,
582          isSecure,
583          hasBeenTypePassword,
584          hostname,
585          telemetryEventData,
586        }
587      );
588      aCallback.onSearchCompletion(results);
589    };
590
591    if (isNullPrincipal) {
592      // Don't search login storage when the field has a null principal as we don't want to fill
593      // logins for the `location` in this case.
594      completeSearch(Promise.resolve({ logins: [] }));
595      return;
596    }
597
598    if (
599      hasBeenTypePassword &&
600      aSearchString &&
601      !loginManagerActor.isPasswordGenerationForcedOn(aElement)
602    ) {
603      // Return empty result on password fields with password already filled,
604      // unless password generation was forced.
605      completeSearch(Promise.resolve({ logins: [] }));
606      return;
607    }
608
609    if (!LoginHelper.enabled) {
610      completeSearch(Promise.resolve({ logins: [] }));
611      return;
612    }
613
614    let previousResult;
615    if (aPreviousResult) {
616      previousResult = {
617        searchString: aPreviousResult.searchString,
618        logins: LoginHelper.loginsToVanillaObjects(
619          aPreviousResult.wrappedJSObject.logins
620        ),
621      };
622    } else {
623      previousResult = null;
624    }
625
626    let acLookupPromise = this._requestAutoCompleteResultsFromParent({
627      searchString: aSearchString,
628      previousResult,
629      inputElement: aElement,
630      form,
631      hasBeenTypePassword,
632    });
633    completeSearch(acLookupPromise).catch(log.error.bind(log));
634  },
635
636  stopSearch() {
637    this._autoCompleteLookupPromise = null;
638  },
639
640  async _requestAutoCompleteResultsFromParent({
641    searchString,
642    previousResult,
643    inputElement,
644    form,
645    hasBeenTypePassword,
646  }) {
647    let actionOrigin = LoginHelper.getFormActionOrigin(form);
648    let autocompleteInfo = inputElement.getAutocompleteInfo();
649
650    let loginManagerActor = LoginManagerChild.forWindow(
651      inputElement.ownerGlobal
652    );
653    let forcePasswordGeneration = false;
654    let isProbablyANewPasswordField = false;
655    if (hasBeenTypePassword) {
656      forcePasswordGeneration = loginManagerActor.isPasswordGenerationForcedOn(
657        inputElement
658      );
659      // Run the Fathom model only if the password field does not have the
660      // autocomplete="new-password" attribute.
661      isProbablyANewPasswordField =
662        autocompleteInfo.fieldName == "new-password" ||
663        this._isProbablyANewPasswordField(inputElement);
664    }
665
666    let messageData = {
667      actionOrigin,
668      searchString,
669      previousResult,
670      forcePasswordGeneration,
671      hasBeenTypePassword,
672      isSecure: InsecurePasswordUtils.isFormSecure(form),
673      isProbablyANewPasswordField,
674    };
675
676    if (LoginHelper.showAutoCompleteFooter) {
677      gAutoCompleteListener.init();
678    }
679
680    log.debug("LoginAutoComplete search:", {
681      forcePasswordGeneration,
682      isSecure: messageData.isSecure,
683      hasBeenTypePassword,
684      isProbablyANewPasswordField,
685      searchString: hasBeenTypePassword
686        ? "*".repeat(searchString.length)
687        : searchString,
688    });
689
690    let result = await loginManagerActor.sendQuery(
691      "PasswordManager:autoCompleteLogins",
692      messageData
693    );
694
695    return {
696      generatedPassword: result.generatedPassword,
697      importable: result.importable,
698      logins: LoginHelper.vanillaObjectsToLogins(result.logins),
699      willAutoSaveGeneratedPassword: result.willAutoSaveGeneratedPassword,
700    };
701  },
702
703  _isProbablyANewPasswordField(inputElement) {
704    const threshold = LoginHelper.generationConfidenceThreshold;
705    if (threshold == -1) {
706      // Fathom is disabled
707      return false;
708    }
709
710    let score = this._cachedNewPasswordScore.get(inputElement);
711    if (score) {
712      return score >= threshold;
713    }
714
715    const { rules, type } = NewPasswordModel;
716    const results = rules.against(inputElement);
717    score = results.get(inputElement).scoreFor(type);
718    this._cachedNewPasswordScore.set(inputElement, score);
719    return score >= threshold;
720  },
721};
722
723let gAutoCompleteListener = {
724  // Input element on which enter keydown event was fired.
725  keyDownEnterForInput: null,
726
727  added: false,
728
729  init() {
730    if (!this.added) {
731      AutoCompleteChild.addPopupStateListener(this);
732      this.added = true;
733    }
734  },
735
736  popupStateChanged(messageName, data, target) {
737    switch (messageName) {
738      case "FormAutoComplete:PopupOpened": {
739        let { chromeEventHandler } = target.docShell;
740        chromeEventHandler.addEventListener("keydown", this, true);
741        break;
742      }
743
744      case "FormAutoComplete:PopupClosed": {
745        this.onPopupClosed(data, target);
746        let { chromeEventHandler } = target.docShell;
747        chromeEventHandler.removeEventListener("keydown", this, true);
748        break;
749      }
750    }
751  },
752
753  handleEvent(event) {
754    if (event.type != "keydown") {
755      return;
756    }
757
758    let focusedElement = formFillController.focusedInput;
759    if (
760      event.keyCode != event.DOM_VK_RETURN ||
761      focusedElement != event.target
762    ) {
763      this.keyDownEnterForInput = null;
764      return;
765    }
766    this.keyDownEnterForInput = focusedElement;
767  },
768
769  onPopupClosed({ selectedRowComment, selectedRowStyle }, window) {
770    let focusedElement = formFillController.focusedInput;
771    let eventTarget = this.keyDownEnterForInput;
772    this.keyDownEnterForInput = null;
773    if (!eventTarget || eventTarget !== focusedElement) {
774      return;
775    }
776
777    let loginManager = window.windowGlobalChild.getActor("LoginManager");
778    switch (selectedRowStyle) {
779      case "importableLearnMore":
780        loginManager.sendAsyncMessage(
781          "PasswordManager:OpenImportableLearnMore",
782          {}
783        );
784        break;
785      case "importableLogins":
786        loginManager.sendAsyncMessage("PasswordManager:HandleImportable", {
787          browserId: selectedRowComment,
788        });
789        break;
790      case "loginsFooter":
791        loginManager.sendAsyncMessage("PasswordManager:OpenPreferences", {
792          entryPoint: "autocomplete",
793        });
794        break;
795    }
796  },
797};
798