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