/* 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/. */ "use strict"; this.EXPORTED_SYMBOLS = ["LoginManagerContextMenu"]; const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerParent", "resource://gre/modules/LoginManagerParent.jsm"); /* * Password manager object for the browser contextual menu. */ var LoginManagerContextMenu = { dateAndTimeFormatter: new Intl.DateTimeFormat(undefined, { day: "numeric", month: "short", year: "numeric" }), /** * Look for login items and add them to the contextual menu. * * @param {HTMLInputElement} inputElement * The target input element of the context menu click. * @param {xul:browser} browser * The browser for the document the context menu was open on. * @param {nsIURI} documentURI * The URI of the document that the context menu was activated from. * This isn't the same as the browser's top-level document URI * when subframes are involved. * @returns {DocumentFragment} a document fragment with all the login items. */ addLoginsToMenu(inputElement, browser, documentURI) { let foundLogins = this._findLogins(documentURI); if (!foundLogins.length) { return null; } let fragment = browser.ownerDocument.createDocumentFragment(); let duplicateUsernames = this._findDuplicates(foundLogins); for (let login of foundLogins) { let item = fragment.ownerDocument.createElement("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, inputElement, browser, documentURI); }.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(); } }, /** * Find logins for the current URI. * * @param {nsIURI} documentURI * URI object with the hostname of the logins we want to find. * This isn't the same as the browser's top-level document URI * when subframes are involved. * * @returns {nsILoginInfo[]} a login list */ _findLogins(documentURI) { let logins = Services.logins.findLogins({}, documentURI.prePath, "", ""); // 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 {Element} inputElement * The target input element we want to fill. * @param {xul:browser} browser * The target tab browser. * @param {nsIURI} documentURI * URI of the document owning the form we want to fill. * This isn't the same as the browser's top-level * document URI when subframes are involved. */ _fillTargetField(login, inputElement, browser, documentURI) { LoginManagerParent.fillForm({ browser: browser, loginFormOrigin: documentURI.prePath, login: login, inputElement: inputElement, }).catch(Cu.reportError); }, /** * @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, formatArgs.length); } return this._stringBundle.GetStringFromName(key); }, }; XPCOMUtils.defineLazyGetter(LoginManagerContextMenu, "_stringBundle", function() { return Services.strings. createBundle("chrome://passwordmgr/locale/passwordmgr.properties"); });