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 );