summaryrefslogtreecommitdiffstats
path: root/browser/components/aboutlogins/AboutLoginsParent.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/aboutlogins/AboutLoginsParent.sys.mjs')
-rw-r--r--browser/components/aboutlogins/AboutLoginsParent.sys.mjs884
1 files changed, 884 insertions, 0 deletions
diff --git a/browser/components/aboutlogins/AboutLoginsParent.sys.mjs b/browser/components/aboutlogins/AboutLoginsParent.sys.mjs
new file mode 100644
index 0000000000..75b695cdd2
--- /dev/null
+++ b/browser/components/aboutlogins/AboutLoginsParent.sys.mjs
@@ -0,0 +1,884 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// _AboutLogins is only exported for testing
+import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+import { E10SUtils } from "resource://gre/modules/E10SUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ LoginBreaches: "resource:///modules/LoginBreaches.sys.mjs",
+ LoginCSVImport: "resource://gre/modules/LoginCSVImport.sys.mjs",
+ LoginExport: "resource://gre/modules/LoginExport.sys.mjs",
+ LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
+ MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs",
+ OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
+ UIState: "resource://services-sync/UIState.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "log", () => {
+ return lazy.LoginHelper.createLogger("AboutLoginsParent");
+});
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "BREACH_ALERTS_ENABLED",
+ "signon.management.page.breach-alerts.enabled",
+ false
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "FXA_ENABLED",
+ "identity.fxaccounts.enabled",
+ false
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "OS_AUTH_ENABLED",
+ "signon.management.page.os-auth.enabled",
+ true
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "VULNERABLE_PASSWORDS_ENABLED",
+ "signon.management.page.vulnerable-passwords.enabled",
+ false
+);
+ChromeUtils.defineLazyGetter(lazy, "AboutLoginsL10n", () => {
+ return new Localization(["branding/brand.ftl", "browser/aboutLogins.ftl"]);
+});
+
+const ABOUT_LOGINS_ORIGIN = "about:logins";
+const AUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
+const PRIMARY_PASSWORD_NOTIFICATION_ID = "primary-password-login-required";
+
+// about:logins will always use the privileged content process,
+// even if it is disabled for other consumers such as about:newtab.
+const EXPECTED_ABOUTLOGINS_REMOTE_TYPE = E10SUtils.PRIVILEGEDABOUT_REMOTE_TYPE;
+let _gPasswordRemaskTimeout = null;
+const convertSubjectToLogin = subject => {
+ subject.QueryInterface(Ci.nsILoginMetaInfo).QueryInterface(Ci.nsILoginInfo);
+ const login = lazy.LoginHelper.loginToVanillaObject(subject);
+ if (!lazy.LoginHelper.isUserFacingLogin(login)) {
+ return null;
+ }
+ return augmentVanillaLoginObject(login);
+};
+
+const SUBDOMAIN_REGEX = new RegExp(/^www\d*\./);
+const augmentVanillaLoginObject = login => {
+ // Note that `displayOrigin` can also include a httpRealm.
+ let title = login.displayOrigin.replace(SUBDOMAIN_REGEX, "");
+ return Object.assign({}, login, {
+ title,
+ });
+};
+
+const EXPORT_PASSWORD_OS_AUTH_DIALOG_MESSAGE_IDS = {
+ win: "about-logins-export-password-os-auth-dialog-message2-win",
+ macosx: "about-logins-export-password-os-auth-dialog-message2-macosx",
+};
+
+export class AboutLoginsParent extends JSWindowActorParent {
+ async receiveMessage(message) {
+ if (!this.browsingContext.embedderElement) {
+ return;
+ }
+
+ // Only respond to messages sent from a privlegedabout process. Ideally
+ // we would also check the contentPrincipal.originNoSuffix but this
+ // check has been removed due to bug 1576722.
+ if (
+ this.browsingContext.embedderElement.remoteType !=
+ EXPECTED_ABOUTLOGINS_REMOTE_TYPE
+ ) {
+ throw new Error(
+ `AboutLoginsParent: Received ${message.name} message the remote type didn't match expectations: ${this.browsingContext.embedderElement.remoteType} == ${EXPECTED_ABOUTLOGINS_REMOTE_TYPE}`
+ );
+ }
+
+ AboutLogins.subscribers.add(this.browsingContext);
+
+ switch (message.name) {
+ case "AboutLogins:CreateLogin": {
+ await this.#createLogin(message.data.login);
+ break;
+ }
+ case "AboutLogins:DeleteLogin": {
+ this.#deleteLogin(message.data.login);
+ break;
+ }
+ case "AboutLogins:SortChanged": {
+ this.#sortChanged(message.data);
+ break;
+ }
+ case "AboutLogins:SyncEnable": {
+ this.#syncEnable();
+ break;
+ }
+ case "AboutLogins:SyncOptions": {
+ this.#syncOptions();
+ break;
+ }
+ case "AboutLogins:ImportFromBrowser": {
+ this.#importFromBrowser();
+ break;
+ }
+ case "AboutLogins:ImportReportInit": {
+ this.#importReportInit();
+ break;
+ }
+ case "AboutLogins:GetHelp": {
+ this.#getHelp();
+ break;
+ }
+ case "AboutLogins:OpenPreferences": {
+ this.#openPreferences();
+ break;
+ }
+ case "AboutLogins:PrimaryPasswordRequest": {
+ await this.#primaryPasswordRequest(message.data);
+ break;
+ }
+ case "AboutLogins:Subscribe": {
+ await this.#subscribe();
+ break;
+ }
+ case "AboutLogins:UpdateLogin": {
+ this.#updateLogin(message.data.login);
+ break;
+ }
+ case "AboutLogins:ExportPasswords": {
+ await this.#exportPasswords();
+ break;
+ }
+ case "AboutLogins:ImportFromFile": {
+ await this.#importFromFile();
+ break;
+ }
+ case "AboutLogins:RemoveAllLogins": {
+ this.#removeAllLogins();
+ break;
+ }
+ }
+ }
+
+ get #ownerGlobal() {
+ return this.browsingContext.embedderElement?.ownerGlobal;
+ }
+
+ async #createLogin(newLogin) {
+ if (!Services.policies.isAllowed("removeMasterPassword")) {
+ if (!lazy.LoginHelper.isPrimaryPasswordSet()) {
+ this.#ownerGlobal.openDialog(
+ "chrome://mozapps/content/preferences/changemp.xhtml",
+ "",
+ "centerscreen,chrome,modal,titlebar"
+ );
+ if (!lazy.LoginHelper.isPrimaryPasswordSet()) {
+ return;
+ }
+ }
+ }
+ // Remove the path from the origin, if it was provided.
+ let origin = lazy.LoginHelper.getLoginOrigin(newLogin.origin);
+ if (!origin) {
+ console.error(
+ "AboutLogins:CreateLogin: Unable to get an origin from the login details."
+ );
+ return;
+ }
+ newLogin.origin = origin;
+ Object.assign(newLogin, {
+ formActionOrigin: "",
+ usernameField: "",
+ passwordField: "",
+ });
+ newLogin = lazy.LoginHelper.vanillaObjectToLogin(newLogin);
+ try {
+ await Services.logins.addLoginAsync(newLogin);
+ } catch (error) {
+ this.#handleLoginStorageErrors(newLogin, error);
+ }
+ }
+
+ get preselectedLogin() {
+ const preselectedLogin =
+ this.#ownerGlobal?.gBrowser.selectedTab.getAttribute("preselect-login") ||
+ this.browsingContext.currentURI?.ref;
+ this.#ownerGlobal?.gBrowser.selectedTab.removeAttribute("preselect-login");
+ return preselectedLogin || null;
+ }
+
+ #deleteLogin(loginObject) {
+ let login = lazy.LoginHelper.vanillaObjectToLogin(loginObject);
+ Services.logins.removeLogin(login);
+ }
+
+ #sortChanged(sort) {
+ Services.prefs.setCharPref("signon.management.page.sort", sort);
+ }
+
+ #syncEnable() {
+ this.#ownerGlobal.gSync.openFxAEmailFirstPage("password-manager");
+ }
+
+ #syncOptions() {
+ this.#ownerGlobal.gSync.openFxAManagePage("password-manager");
+ }
+
+ #importFromBrowser() {
+ try {
+ lazy.MigrationUtils.showMigrationWizard(this.#ownerGlobal, {
+ entrypoint: lazy.MigrationUtils.MIGRATION_ENTRYPOINTS.PASSWORDS,
+ });
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+
+ #importReportInit() {
+ let reportData = lazy.LoginCSVImport.lastImportReport;
+ this.sendAsyncMessage("AboutLogins:ImportReportData", reportData);
+ }
+
+ #getHelp() {
+ const SUPPORT_URL =
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "password-manager-remember-delete-edit-logins";
+ this.#ownerGlobal.openWebLinkIn(SUPPORT_URL, "tab", {
+ relatedToCurrent: true,
+ });
+ }
+
+ #openPreferences() {
+ this.#ownerGlobal.openPreferences("privacy-logins");
+ }
+
+ async #primaryPasswordRequest(messageId) {
+ if (!messageId) {
+ throw new Error("AboutLogins:PrimaryPasswordRequest: no messageId.");
+ }
+ let messageText = { value: "NOT SUPPORTED" };
+ let captionText = { value: "" };
+
+ // This feature is only supported on Windows and macOS
+ // but we still call in to OSKeyStore on Linux to get
+ // the proper auth_details for Telemetry.
+ // See bug 1614874 for Linux support.
+ if (lazy.OS_AUTH_ENABLED && lazy.OSKeyStore.canReauth()) {
+ messageId += "-" + AppConstants.platform;
+ [messageText, captionText] = await lazy.AboutLoginsL10n.formatMessages([
+ {
+ id: messageId,
+ },
+ {
+ id: "about-logins-os-auth-dialog-caption",
+ },
+ ]);
+ }
+
+ let { isAuthorized, telemetryEvent } = await lazy.LoginHelper.requestReauth(
+ this.browsingContext.embedderElement,
+ lazy.OS_AUTH_ENABLED,
+ AboutLogins._authExpirationTime,
+ messageText.value,
+ captionText.value
+ );
+ this.sendAsyncMessage("AboutLogins:PrimaryPasswordResponse", {
+ result: isAuthorized,
+ telemetryEvent,
+ });
+ if (isAuthorized) {
+ AboutLogins._authExpirationTime = Date.now() + AUTH_TIMEOUT_MS;
+ const remaskPasswords = () => {
+ this.sendAsyncMessage("AboutLogins:RemaskPassword");
+ };
+ clearTimeout(_gPasswordRemaskTimeout);
+ _gPasswordRemaskTimeout = setTimeout(remaskPasswords, AUTH_TIMEOUT_MS);
+ }
+ }
+
+ async #subscribe() {
+ AboutLogins._authExpirationTime = Number.NEGATIVE_INFINITY;
+ AboutLogins.addObservers();
+
+ const logins = await AboutLogins.getAllLogins();
+ try {
+ let syncState = AboutLogins.getSyncState();
+
+ let selectedSort = Services.prefs.getCharPref(
+ "signon.management.page.sort",
+ "name"
+ );
+ if (selectedSort == "breached") {
+ // The "breached" value was used since Firefox 70 and
+ // replaced with "alerts" in Firefox 76.
+ selectedSort = "alerts";
+ }
+ this.sendAsyncMessage("AboutLogins:Setup", {
+ logins,
+ selectedSort,
+ syncState,
+ primaryPasswordEnabled: lazy.LoginHelper.isPrimaryPasswordSet(),
+ passwordRevealVisible: Services.policies.isAllowed("passwordReveal"),
+ importVisible:
+ Services.policies.isAllowed("profileImport") &&
+ AppConstants.platform != "linux",
+ preselectedLogin: this.preselectedLogin,
+ });
+
+ await AboutLogins.sendAllLoginRelatedObjects(
+ logins,
+ this.browsingContext
+ );
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_NOT_INITIALIZED) {
+ throw ex;
+ }
+
+ // The message manager may be destroyed before the replies can be sent.
+ lazy.log.debug(
+ "AboutLogins:Subscribe: exception when replying with logins",
+ ex
+ );
+ }
+ }
+
+ #updateLogin(loginUpdates) {
+ let logins = lazy.LoginHelper.searchLoginsWithObject({
+ guid: loginUpdates.guid,
+ });
+ if (logins.length != 1) {
+ lazy.log.warn(
+ `AboutLogins:UpdateLogin: expected to find a login for guid: ${loginUpdates.guid} but found ${logins.length}`
+ );
+ return;
+ }
+
+ let modifiedLogin = logins[0].clone();
+ if (loginUpdates.hasOwnProperty("username")) {
+ modifiedLogin.username = loginUpdates.username;
+ }
+ if (loginUpdates.hasOwnProperty("password")) {
+ modifiedLogin.password = loginUpdates.password;
+ }
+ try {
+ Services.logins.modifyLogin(logins[0], modifiedLogin);
+ } catch (error) {
+ this.#handleLoginStorageErrors(modifiedLogin, error);
+ }
+ }
+
+ async #exportPasswords() {
+ let messageText = { value: "NOT SUPPORTED" };
+ let captionText = { value: "" };
+
+ // This feature is only supported on Windows and macOS
+ // but we still call in to OSKeyStore on Linux to get
+ // the proper auth_details for Telemetry.
+ // See bug 1614874 for Linux support.
+ if (lazy.OSKeyStore.canReauth()) {
+ const messageId =
+ EXPORT_PASSWORD_OS_AUTH_DIALOG_MESSAGE_IDS[AppConstants.platform];
+ if (!messageId) {
+ throw new Error(
+ `AboutLoginsParent: Cannot find l10n id for platform ${AppConstants.platform} for export passwords os auth dialog message`
+ );
+ }
+ [messageText, captionText] = await lazy.AboutLoginsL10n.formatMessages([
+ {
+ id: messageId,
+ },
+ {
+ id: "about-logins-os-auth-dialog-caption",
+ },
+ ]);
+ }
+
+ let { isAuthorized, telemetryEvent } = await lazy.LoginHelper.requestReauth(
+ this.browsingContext.embedderElement,
+ true,
+ null, // Prompt regardless of a recent prompt
+ messageText.value,
+ captionText.value
+ );
+
+ let { method, object, extra = {}, value = null } = telemetryEvent;
+ Services.telemetry.recordEvent("pwmgr", method, object, value, extra);
+
+ if (!isAuthorized) {
+ return;
+ }
+
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ function fpCallback(aResult) {
+ if (aResult != Ci.nsIFilePicker.returnCancel) {
+ lazy.LoginExport.exportAsCSV(fp.file.path);
+ Services.telemetry.recordEvent(
+ "pwmgr",
+ "mgmt_menu_item_used",
+ "export_complete"
+ );
+ }
+ }
+ let [title, defaultFilename, okButtonLabel, csvFilterTitle] =
+ await lazy.AboutLoginsL10n.formatValues([
+ {
+ id: "about-logins-export-file-picker-title2",
+ },
+ {
+ id: "about-logins-export-file-picker-default-filename2",
+ },
+ {
+ id: "about-logins-export-file-picker-export-button",
+ },
+ {
+ id: "about-logins-export-file-picker-csv-filter-title",
+ },
+ ]);
+
+ fp.init(this.#ownerGlobal, title, Ci.nsIFilePicker.modeSave);
+ fp.appendFilter(csvFilterTitle, "*.csv");
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+ fp.defaultString = defaultFilename;
+ fp.defaultExtension = "csv";
+ fp.okButtonLabel = okButtonLabel;
+ fp.open(fpCallback);
+ }
+
+ async #importFromFile() {
+ let [title, okButtonLabel, csvFilterTitle, tsvFilterTitle] =
+ await lazy.AboutLoginsL10n.formatValues([
+ {
+ id: "about-logins-import-file-picker-title2",
+ },
+ {
+ id: "about-logins-import-file-picker-import-button",
+ },
+ {
+ id: "about-logins-import-file-picker-csv-filter-title",
+ },
+ {
+ id: "about-logins-import-file-picker-tsv-filter-title",
+ },
+ ]);
+ let { result, path } = await this.openFilePickerDialog(
+ title,
+ okButtonLabel,
+ [
+ {
+ title: csvFilterTitle,
+ extensionPattern: "*.csv",
+ },
+ {
+ title: tsvFilterTitle,
+ extensionPattern: "*.tsv",
+ },
+ ],
+ this.#ownerGlobal
+ );
+
+ if (result != Ci.nsIFilePicker.returnCancel) {
+ let summary;
+ try {
+ summary = await lazy.LoginCSVImport.importFromCSV(path);
+ } catch (e) {
+ console.error(e);
+ this.sendAsyncMessage(
+ "AboutLogins:ImportPasswordsErrorDialog",
+ e.errorType
+ );
+ }
+ if (summary) {
+ this.sendAsyncMessage("AboutLogins:ImportPasswordsDialog", summary);
+ Services.telemetry.recordEvent(
+ "pwmgr",
+ "mgmt_menu_item_used",
+ "import_csv_complete"
+ );
+ }
+ }
+ }
+
+ #removeAllLogins() {
+ Services.logins.removeAllUserFacingLogins();
+ }
+
+ #handleLoginStorageErrors(login, error) {
+ let messageObject = {
+ login: augmentVanillaLoginObject(
+ lazy.LoginHelper.loginToVanillaObject(login)
+ ),
+ errorMessage: error.message,
+ };
+
+ if (error.message.includes("This login already exists")) {
+ // See comment in LoginHelper.createLoginAlreadyExistsError as to
+ // why we need to call .toString() on the nsISupportsString.
+ messageObject.existingLoginGuid = error.data.toString();
+ }
+
+ this.sendAsyncMessage("AboutLogins:ShowLoginItemError", messageObject);
+ }
+
+ async openFilePickerDialog(title, okButtonLabel, appendFilters, ownerGlobal) {
+ return new Promise(resolve => {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(ownerGlobal, title, Ci.nsIFilePicker.modeOpen);
+ for (const appendFilter of appendFilters) {
+ fp.appendFilter(appendFilter.title, appendFilter.extensionPattern);
+ }
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+ fp.okButtonLabel = okButtonLabel;
+ fp.open(async result => {
+ resolve({ result, path: fp.file.path });
+ });
+ });
+ }
+}
+
+class AboutLoginsInternal {
+ subscribers = new WeakSet();
+ #observersAdded = false;
+ authExpirationTime = Number.NEGATIVE_INFINITY;
+
+ async observe(subject, topic, type) {
+ if (!ChromeUtils.nondeterministicGetWeakSetKeys(this.subscribers).length) {
+ this.#removeObservers();
+ return;
+ }
+
+ switch (topic) {
+ case "passwordmgr-reload-all": {
+ await this.#reloadAllLogins();
+ break;
+ }
+ case "passwordmgr-crypto-login": {
+ this.#removeNotifications(PRIMARY_PASSWORD_NOTIFICATION_ID);
+ await this.#reloadAllLogins();
+ break;
+ }
+ case "passwordmgr-crypto-loginCanceled": {
+ this.#showPrimaryPasswordLoginNotifications();
+ break;
+ }
+ case lazy.UIState.ON_UPDATE: {
+ this.#messageSubscribers("AboutLogins:SyncState", this.getSyncState());
+ break;
+ }
+ case "passwordmgr-storage-changed": {
+ switch (type) {
+ case "addLogin": {
+ await this.#addLogin(subject);
+ break;
+ }
+ case "modifyLogin": {
+ this.#modifyLogin(subject);
+ break;
+ }
+ case "removeLogin": {
+ this.#removeLogin(subject);
+ break;
+ }
+ case "removeAllLogins": {
+ this.#removeAllLogins();
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ async #addLogin(subject) {
+ const login = convertSubjectToLogin(subject);
+ if (!login) {
+ return;
+ }
+
+ if (lazy.BREACH_ALERTS_ENABLED) {
+ this.#messageSubscribers(
+ "AboutLogins:UpdateBreaches",
+ await lazy.LoginBreaches.getPotentialBreachesByLoginGUID([login])
+ );
+ if (lazy.VULNERABLE_PASSWORDS_ENABLED) {
+ this.#messageSubscribers(
+ "AboutLogins:UpdateVulnerableLogins",
+ await lazy.LoginBreaches.getPotentiallyVulnerablePasswordsByLoginGUID(
+ [login]
+ )
+ );
+ }
+ }
+
+ this.#messageSubscribers("AboutLogins:LoginAdded", login);
+ }
+
+ async #modifyLogin(subject) {
+ subject.QueryInterface(Ci.nsIArrayExtensions);
+ const login = convertSubjectToLogin(subject.GetElementAt(1));
+ if (!login) {
+ return;
+ }
+
+ if (lazy.BREACH_ALERTS_ENABLED) {
+ let breachesForThisLogin =
+ await lazy.LoginBreaches.getPotentialBreachesByLoginGUID([login]);
+ let breachData = breachesForThisLogin.size
+ ? breachesForThisLogin.get(login.guid)
+ : false;
+ this.#messageSubscribers(
+ "AboutLogins:UpdateBreaches",
+ new Map([[login.guid, breachData]])
+ );
+ if (lazy.VULNERABLE_PASSWORDS_ENABLED) {
+ let vulnerablePasswordsForThisLogin =
+ await lazy.LoginBreaches.getPotentiallyVulnerablePasswordsByLoginGUID(
+ [login]
+ );
+ let isLoginVulnerable = !!vulnerablePasswordsForThisLogin.size;
+ this.#messageSubscribers(
+ "AboutLogins:UpdateVulnerableLogins",
+ new Map([[login.guid, isLoginVulnerable]])
+ );
+ }
+ }
+
+ this.#messageSubscribers("AboutLogins:LoginModified", login);
+ }
+
+ #removeLogin(subject) {
+ const login = convertSubjectToLogin(subject);
+ if (!login) {
+ return;
+ }
+ this.#messageSubscribers("AboutLogins:LoginRemoved", login);
+ }
+
+ #removeAllLogins() {
+ this.#messageSubscribers("AboutLogins:RemoveAllLogins", []);
+ }
+
+ async #reloadAllLogins() {
+ let logins = await this.getAllLogins();
+ this.#messageSubscribers("AboutLogins:AllLogins", logins);
+ await this.sendAllLoginRelatedObjects(logins);
+ }
+
+ #showPrimaryPasswordLoginNotifications() {
+ this.#showNotifications({
+ id: PRIMARY_PASSWORD_NOTIFICATION_ID,
+ priority: "PRIORITY_WARNING_MEDIUM",
+ iconURL: "chrome://browser/skin/login.svg",
+ messageId: "about-logins-primary-password-notification-message",
+ buttonIds: ["master-password-reload-button"],
+ onClicks: [
+ function onReloadClick(browser) {
+ browser.reload();
+ },
+ ],
+ });
+ this.#messageSubscribers("AboutLogins:PrimaryPasswordAuthRequired");
+ }
+
+ #showNotifications({
+ id,
+ priority,
+ iconURL,
+ messageId,
+ buttonIds,
+ onClicks,
+ extraFtl = [],
+ } = {}) {
+ for (let subscriber of this.#subscriberIterator()) {
+ let browser = subscriber.embedderElement;
+ let MozXULElement = browser.ownerGlobal.MozXULElement;
+ MozXULElement.insertFTLIfNeeded("browser/aboutLogins.ftl");
+ for (let ftl of extraFtl) {
+ MozXULElement.insertFTLIfNeeded(ftl);
+ }
+
+ // If there's already an existing notification bar, don't do anything.
+ let { gBrowser } = browser.ownerGlobal;
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ let notification = notificationBox.getNotificationWithValue(id);
+ if (notification) {
+ continue;
+ }
+
+ let buttons = [];
+ for (let i = 0; i < buttonIds.length; i++) {
+ buttons[i] = {
+ "l10n-id": buttonIds[i],
+ popup: null,
+ callback: () => {
+ onClicks[i](browser);
+ },
+ };
+ }
+
+ notification = notificationBox.appendNotification(
+ id,
+ {
+ label: { "l10n-id": messageId },
+ image: iconURL,
+ priority: notificationBox[priority],
+ },
+ buttons
+ );
+ }
+ }
+
+ #removeNotifications(notificationId) {
+ for (let subscriber of this.#subscriberIterator()) {
+ let browser = subscriber.embedderElement;
+ let { gBrowser } = browser.ownerGlobal;
+ let notificationBox = gBrowser.getNotificationBox(browser);
+ let notification =
+ notificationBox.getNotificationWithValue(notificationId);
+ if (!notification) {
+ continue;
+ }
+ notificationBox.removeNotification(notification);
+ }
+ }
+
+ *#subscriberIterator() {
+ let subscribers = ChromeUtils.nondeterministicGetWeakSetKeys(
+ this.subscribers
+ );
+ for (let subscriber of subscribers) {
+ let browser = subscriber.embedderElement;
+ if (
+ browser?.remoteType != EXPECTED_ABOUTLOGINS_REMOTE_TYPE ||
+ browser?.contentPrincipal?.originNoSuffix != ABOUT_LOGINS_ORIGIN
+ ) {
+ this.subscribers.delete(subscriber);
+ continue;
+ }
+ yield subscriber;
+ }
+ }
+
+ #messageSubscribers(name, details) {
+ for (let subscriber of this.#subscriberIterator()) {
+ try {
+ if (subscriber.currentWindowGlobal) {
+ let actor = subscriber.currentWindowGlobal.getActor("AboutLogins");
+ actor.sendAsyncMessage(name, details);
+ }
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_NOT_INITIALIZED) {
+ // The actor may be destroyed before the message is sent.
+ lazy.log.debug(
+ "messageSubscribers: exception when calling sendAsyncMessage",
+ ex
+ );
+ } else {
+ throw ex;
+ }
+ }
+ }
+ }
+
+ async getAllLogins() {
+ try {
+ let logins = await lazy.LoginHelper.getAllUserFacingLogins();
+ return logins
+ .map(lazy.LoginHelper.loginToVanillaObject)
+ .map(augmentVanillaLoginObject);
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_ABORT) {
+ // If the user cancels the MP prompt then return no logins.
+ return [];
+ }
+ throw e;
+ }
+ }
+
+ async sendAllLoginRelatedObjects(logins, browsingContext) {
+ let sendMessageFn = (name, details) => {
+ if (browsingContext?.currentWindowGlobal) {
+ let actor = browsingContext.currentWindowGlobal.getActor("AboutLogins");
+ actor.sendAsyncMessage(name, details);
+ } else {
+ this.#messageSubscribers(name, details);
+ }
+ };
+
+ if (lazy.BREACH_ALERTS_ENABLED) {
+ sendMessageFn(
+ "AboutLogins:SetBreaches",
+ await lazy.LoginBreaches.getPotentialBreachesByLoginGUID(logins)
+ );
+ if (lazy.VULNERABLE_PASSWORDS_ENABLED) {
+ sendMessageFn(
+ "AboutLogins:SetVulnerableLogins",
+ await lazy.LoginBreaches.getPotentiallyVulnerablePasswordsByLoginGUID(
+ logins
+ )
+ );
+ }
+ }
+ }
+
+ getSyncState() {
+ const state = lazy.UIState.get();
+ // As long as Sync is configured, about:logins will treat it as
+ // authenticated. More diagnostics and error states can be handled
+ // by other more Sync-specific pages.
+ const loggedIn = state.status != lazy.UIState.STATUS_NOT_CONFIGURED;
+ const passwordSyncEnabled = state.syncEnabled && lazy.PASSWORD_SYNC_ENABLED;
+
+ return {
+ loggedIn,
+ email: state.email,
+ avatarURL: state.avatarURL,
+ fxAccountsEnabled: lazy.FXA_ENABLED,
+ passwordSyncEnabled,
+ };
+ }
+
+ onPasswordSyncEnabledPreferenceChange(data, previous, latest) {
+ this.#messageSubscribers("AboutLogins:SyncState", this.getSyncState());
+ }
+
+ #observedTopics = [
+ "passwordmgr-crypto-login",
+ "passwordmgr-crypto-loginCanceled",
+ "passwordmgr-storage-changed",
+ "passwordmgr-reload-all",
+ lazy.UIState.ON_UPDATE,
+ ];
+
+ addObservers() {
+ if (!this.#observersAdded) {
+ for (const topic of this.#observedTopics) {
+ Services.obs.addObserver(this, topic);
+ }
+ this.#observersAdded = true;
+ }
+ }
+
+ #removeObservers() {
+ for (const topic of this.#observedTopics) {
+ Services.obs.removeObserver(this, topic);
+ }
+ this.#observersAdded = false;
+ }
+}
+
+let AboutLogins = new AboutLoginsInternal();
+export var _AboutLogins = AboutLogins;
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "PASSWORD_SYNC_ENABLED",
+ "services.sync.engine.passwords",
+ false,
+ AboutLogins.onPasswordSyncEnabledPreferenceChange
+);