Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
});
/**
* Password manager object for the browser contextual menu.
*/
export const LoginManagerContextMenu = {
/**
* Look for login items and add them to the contextual menu.
*
* @param {Object} inputElementIdentifier
* An identifier generated for the input element via ContentDOMReference.
* @param {xul:browser} browser
* The browser for the document the context menu was open on.
* @param {string} formOrigin
* The origin of the document that the context menu was activated from.
* This isn't the same as the browser's top-level document origin
* when subframes are involved.
* @returns {DocumentFragment} a document fragment with all the login items.
*/
addLoginsToMenu(inputElementIdentifier, browser, formOrigin) {
let foundLogins = this._findLogins(formOrigin);
if (!foundLogins.length) {
return null;
}
let fragment = browser.ownerDocument.createDocumentFragment();
let duplicateUsernames = this._findDuplicates(foundLogins);
for (let login of foundLogins) {
let item = fragment.ownerDocument.createXULElement("menuitem");
let username = login.username;
// If login is empty or duplicated we want to append a modification date to it.
if (!username || duplicateUsernames.has(username)) {
if (!username) {
username = this._getLocalizedString("noUsername");
}
let meta = login.QueryInterface(Ci.nsILoginMetaInfo);
let time = this.dateAndTimeFormatter.format(
new Date(meta.timePasswordChanged)
);
username = this._getLocalizedString("loginHostAge", [username, time]);
}
item.setAttribute("label", username);
item.setAttribute("class", "context-login-item");
// login is bound so we can keep the reference to each object.
item.addEventListener(
"command",
function (login, _event) {
this._fillTargetField(
login,
inputElementIdentifier,
browser,
formOrigin
);
}.bind(this, login)
);
fragment.appendChild(item);
}
return fragment;
},
/**
* Undoes the work of addLoginsToMenu for the same menu.
*
* @param {Document}
* The context menu owner document.
*/
clearLoginsFromMenu(document) {
let loginItems = document.getElementsByClassName("context-login-item");
while (loginItems.item(0)) {
loginItems.item(0).remove();
}
},
/**
* Show the password autocomplete UI with the generation option forced to appear.
*/
async useGeneratedPassword(inputElementIdentifier) {
let browsingContextId = inputElementIdentifier.browsingContextId;
let browsingContext = BrowsingContext.get(browsingContextId);
let actor = browsingContext.currentWindowGlobal.getActor("LoginManager");
actor.sendAsyncMessage("PasswordManager:useGeneratedPassword", {
inputElementIdentifier,
});
},
/**
* Find logins for the specified origin..
*
* @param {string} formOrigin
* Origin of the logins we want to find that has be sanitized by `getLoginOrigin`.
* This isn't the same as the browser's top-level document URI
* when subframes are involved.
*
* @returns {nsILoginInfo[]} a login list
*/
_findLogins(formOrigin) {
let searchParams = {
origin: formOrigin,
schemeUpgrades: lazy.LoginHelper.schemeUpgrades,
};
let logins = lazy.LoginHelper.searchLoginsWithObject(searchParams);
let resolveBy = ["scheme", "timePasswordChanged"];
logins = lazy.LoginHelper.dedupeLogins(
logins,
["username", "password"],
resolveBy,
formOrigin
);
// Sort logins in alphabetical order and by date.
logins.sort((loginA, loginB) => {
// Sort alphabetically
let result = loginA.username.localeCompare(loginB.username);
if (result) {
// Forces empty logins to be at the end
if (!loginA.username) {
return 1;
}
if (!loginB.username) {
return -1;
}
return result;
}
// Same username logins are sorted by last change date
let metaA = loginA.QueryInterface(Ci.nsILoginMetaInfo);
let metaB = loginB.QueryInterface(Ci.nsILoginMetaInfo);
return metaB.timePasswordChanged - metaA.timePasswordChanged;
});
return logins;
},
/**
* Find duplicate usernames in a login list.
*
* @param {nsILoginInfo[]} loginList
* A list of logins we want to look for duplicate usernames.
*
* @returns {Set} a set with the duplicate usernames.
*/
_findDuplicates(loginList) {
let seen = new Set();
let duplicates = new Set();
for (let login of loginList) {
if (seen.has(login.username)) {
duplicates.add(login.username);
}
seen.add(login.username);
}
return duplicates;
},
/**
* @param {nsILoginInfo} login
* The login we want to fill the form with.
* @param {Object} inputElementIdentifier
* An identifier generated for the input element via ContentDOMReference.
* @param {xul:browser} browser
* The target tab browser.
* @param {string} formOrigin
* Origin of the document we're filling after sanitization via
* `getLoginOrigin`.
* This isn't the same as the browser's top-level
* origin when subframes are involved.
*/
_fillTargetField(login, inputElementIdentifier, browser, formOrigin) {
let browsingContextId = inputElementIdentifier.browsingContextId;
let browsingContext = BrowsingContext.get(browsingContextId);
if (!browsingContext) {
return;
}
let actor = browsingContext.currentWindowGlobal.getActor("LoginManager");
if (!actor) {
return;
}
actor
.fillForm({
browser,
inputElementIdentifier,
loginFormOrigin: formOrigin,
login,
})
.catch(console.error);
},
/**
* @param {string} key
* The localized string key
* @param {string[]} formatArgs
* An array of formatting argument string
*
* @returns {string} the localized string for the specified key,
* formatted with arguments if required.
*/
_getLocalizedString(key, formatArgs) {
if (formatArgs) {
return this._stringBundle.formatStringFromName(key, formatArgs);
}
return this._stringBundle.GetStringFromName(key);
},
};
ChromeUtils.defineLazyGetter(
LoginManagerContextMenu,
"_stringBundle",
function () {
return Services.strings.createBundle(
"chrome://passwordmgr/locale/passwordmgr.properties"
);
}
);
ChromeUtils.defineLazyGetter(
LoginManagerContextMenu,
"dateAndTimeFormatter",
function () {
return new Services.intl.DateTimeFormat(undefined, {
dateStyle: "medium",
});
}
);