888 lines
26 KiB
JavaScript
888 lines
26 KiB
JavaScript
/* 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",
|
|
UIState: "resource://services-sync/UIState.sys.mjs",
|
|
FxAccounts: "resource://gre/modules/FxAccounts.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,
|
|
"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: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.messageId,
|
|
message.data.reason
|
|
);
|
|
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");
|
|
}
|
|
|
|
#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, reason) {
|
|
if (!messageId) {
|
|
throw new Error("AboutLogins:PrimaryPasswordRequest: no messageId.");
|
|
}
|
|
let messageText = { value: "NOT SUPPORTED" };
|
|
let captionText = { value: "" };
|
|
|
|
const isOSAuthEnabled = lazy.LoginHelper.getOSAuthEnabled(
|
|
lazy.LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF
|
|
);
|
|
|
|
// 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 (isOSAuthEnabled) {
|
|
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,
|
|
isOSAuthEnabled,
|
|
AboutLogins._authExpirationTime,
|
|
messageText.value,
|
|
captionText.value,
|
|
reason
|
|
);
|
|
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 = await 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: "" };
|
|
|
|
const isOSAuthEnabled = lazy.LoginHelper.getOSAuthEnabled(
|
|
lazy.LoginHelper.OS_AUTH_FOR_PASSWORDS_PREF
|
|
);
|
|
|
|
// 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 (isOSAuthEnabled) {
|
|
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 reason = "export_logins";
|
|
let { isAuthorized, telemetryEvent } = await lazy.LoginHelper.requestReauth(
|
|
this.browsingContext.embedderElement,
|
|
true,
|
|
null, // Prompt regardless of a recent prompt
|
|
messageText.value,
|
|
captionText.value,
|
|
reason
|
|
);
|
|
|
|
let { name, extra = {}, value = null } = telemetryEvent;
|
|
if (value) {
|
|
extra.value = value;
|
|
}
|
|
Glean.pwmgr[name].record(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);
|
|
Glean.pwmgr.mgmtMenuItemUsedExportComplete.record();
|
|
}
|
|
}
|
|
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.browsingContext, 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",
|
|
},
|
|
]
|
|
);
|
|
|
|
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);
|
|
Glean.pwmgr.mgmtMenuItemUsedImportCsvComplete.record();
|
|
}
|
|
}
|
|
}
|
|
|
|
#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) {
|
|
return new Promise(resolve => {
|
|
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
|
|
fp.init(this.browsingContext, 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",
|
|
await 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
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
async 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;
|
|
const accountURL =
|
|
await lazy.FxAccounts.config.promiseManageURI("password-manager");
|
|
|
|
return {
|
|
loggedIn,
|
|
email: state.email,
|
|
avatarURL: state.avatarURL,
|
|
fxAccountsEnabled: lazy.FXA_ENABLED,
|
|
passwordSyncEnabled,
|
|
accountURL,
|
|
};
|
|
}
|
|
|
|
async onPasswordSyncEnabledPreferenceChange(_data, _previous, _latest) {
|
|
this.#messageSubscribers(
|
|
"AboutLogins:SyncState",
|
|
await 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.bind(AboutLogins)
|
|
);
|