summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/LoginManagerContextMenu.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/LoginManagerContextMenu.sys.mjs')
-rw-r--r--toolkit/components/passwordmgr/LoginManagerContextMenu.sys.mjs238
1 files changed, 238 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/LoginManagerContextMenu.sys.mjs b/toolkit/components/passwordmgr/LoginManagerContextMenu.sys.mjs
new file mode 100644
index 0000000000..fc87f5cf58
--- /dev/null
+++ b/toolkit/components/passwordmgr/LoginManagerContextMenu.sys.mjs
@@ -0,0 +1,238 @@
+/* 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, documentURI, browser) {
+ 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",
+ });
+ }
+);