mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-11 12:25:53 +00:00
Bug 433238 - Password manager contextual menu password field manual fill. r=MattN
--HG-- extra : commitid : C2Thcw28VRq extra : rebase_source : 6b6b628738bf715109161961bdced4489685058c
This commit is contained in:
parent
68d9768596
commit
7ec5131e30
@ -417,6 +417,23 @@
|
||||
label="&bidiSwitchPageDirectionItem.label;"
|
||||
accesskey="&bidiSwitchPageDirectionItem.accesskey;"
|
||||
oncommand="gContextMenu.switchPageDirection();"/>
|
||||
<menuseparator id="fill-login-separator" hidden="true"/>
|
||||
<menu id="fill-login"
|
||||
label="&fillPasswordMenu.label;"
|
||||
class="menu-iconic"
|
||||
accesskey="&fillPasswordMenu.accesskey;"
|
||||
hidden="true">
|
||||
<menupopup id="fill-login-popup">
|
||||
<menuitem id="fill-login-no-logins"
|
||||
label="&noLoginSuggestions.label;"
|
||||
disabled="true"
|
||||
hidden="true"/>
|
||||
<menuseparator id="saved-logins-separator"/>
|
||||
<menuitem id="fill-login-saved-passwords"
|
||||
label="&viewSavedLogins.label;"
|
||||
oncommand="gContextMenu.openPasswordManager();"/>
|
||||
</menupopup>
|
||||
</menu>
|
||||
<menuseparator id="inspect-separator" hidden="true"/>
|
||||
<menuitem id="context-inspect"
|
||||
hidden="true"
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
|
||||
Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm");
|
||||
Components.utils.import("resource://gre/modules/LoginManagerContextMenu.jsm");
|
||||
Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
|
||||
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
@ -12,6 +13,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
|
||||
"resource:///modules/CustomizableUI.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Pocket",
|
||||
"resource:///modules/Pocket.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
|
||||
"resource://gre/modules/LoginHelper.jsm");
|
||||
|
||||
var gContextMenuContentData = null;
|
||||
|
||||
@ -67,6 +70,7 @@ nsContextMenu.prototype = {
|
||||
InlineSpellCheckerUI.clearSuggestionsFromMenu();
|
||||
InlineSpellCheckerUI.clearDictionaryListFromMenu();
|
||||
InlineSpellCheckerUI.uninit();
|
||||
LoginManagerContextMenu.clearLoginsFromMenu(document);
|
||||
|
||||
// This handler self-deletes, only run it if it is still there:
|
||||
if (this._onPopupHiding) {
|
||||
@ -86,6 +90,7 @@ nsContextMenu.prototype = {
|
||||
this.initMediaPlayerItems();
|
||||
this.initLeaveDOMFullScreenItems();
|
||||
this.initClickToPlayItems();
|
||||
this.initPasswordManagerItems();
|
||||
},
|
||||
|
||||
initPageMenuSeparator: function CM_initPageMenuSeparator() {
|
||||
@ -500,6 +505,33 @@ nsContextMenu.prototype = {
|
||||
this.showItem("context-sep-ctp", this.onCTPPlugin);
|
||||
},
|
||||
|
||||
initPasswordManagerItems: function() {
|
||||
let showFillPassword = this.onPassword;
|
||||
let disableFillPassword = !Services.logins.isLoggedIn || this.target.disabled || this.target.readOnly;
|
||||
this.showItem("fill-login-separator", showFillPassword);
|
||||
this.showItem("fill-login", showFillPassword);
|
||||
this.setItemAttr("fill-login", "disabled", disableFillPassword);
|
||||
|
||||
if (!showFillPassword || disableFillPassword) {
|
||||
return;
|
||||
}
|
||||
let documentURI = gContextMenuContentData.documentURIObject;
|
||||
let fragment = LoginManagerContextMenu.addLoginsToMenu(this.target, this.browser, documentURI);
|
||||
|
||||
this.showItem("fill-login-no-logins", !fragment);
|
||||
|
||||
if (!fragment) {
|
||||
return;
|
||||
}
|
||||
let popup = document.getElementById("fill-login-popup");
|
||||
let insertBeforeElement = document.getElementById("fill-login-no-logins");
|
||||
popup.insertBefore(fragment, insertBeforeElement);
|
||||
},
|
||||
|
||||
openPasswordManager: function() {
|
||||
LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host);
|
||||
},
|
||||
|
||||
inspectNode: function CM_inspectNode() {
|
||||
let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
|
||||
let gBrowser = this.browser.ownerDocument.defaultView.gBrowser;
|
||||
@ -571,6 +603,7 @@ nsContextMenu.prototype = {
|
||||
this.isDesignMode = false;
|
||||
this.onCTPPlugin = false;
|
||||
this.canSpellCheck = false;
|
||||
this.onPassword = false;
|
||||
|
||||
if (this.isRemote) {
|
||||
this.selectionInfo = gContextMenuContentData.selectionInfo;
|
||||
@ -668,6 +701,7 @@ nsContextMenu.prototype = {
|
||||
this.onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0;
|
||||
this.onNumeric = (editFlags & SpellCheckHelper.NUMERIC) !== 0;
|
||||
this.onEditableArea = (editFlags & SpellCheckHelper.EDITABLE) !== 0;
|
||||
this.onPassword = (editFlags & SpellCheckHelper.PASSWORD) !== 0;
|
||||
if (this.onEditableArea) {
|
||||
if (this.isRemote) {
|
||||
InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
|
||||
|
@ -95,3 +95,13 @@
|
||||
#context-media-eme-learnmore {
|
||||
list-style-image: url("chrome://browser/skin/drm-icon.svg#chains");
|
||||
}
|
||||
|
||||
#fill-login {
|
||||
list-style-image: url("chrome://mozapps/skin/passwordmgr/key-16.png");
|
||||
}
|
||||
|
||||
@media (min-resolution: 1.1dppx) {
|
||||
#fill-login {
|
||||
list-style-image: url("chrome://mozapps/skin/passwordmgr/key-16@2x.png");
|
||||
}
|
||||
}
|
||||
|
@ -188,6 +188,7 @@ var LoginManagerContent = {
|
||||
loginFormOrigin: msg.data.loginFormOrigin,
|
||||
loginsFound: jsLoginsToXPCOM(msg.data.logins),
|
||||
recipes: msg.data.recipes,
|
||||
inputElement: msg.objects.inputElement,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -445,25 +446,39 @@ var LoginManagerContent = {
|
||||
* from the origin of the form used for the fill.
|
||||
* recipes:
|
||||
* Fill recipes transmitted together with the original message.
|
||||
* inputElement:
|
||||
* Optional input password element from the form we want to fill.
|
||||
* }
|
||||
*/
|
||||
fillForm({ topDocument, loginFormOrigin, loginsFound, recipes }) {
|
||||
fillForm({ topDocument, loginFormOrigin, loginsFound, recipes, inputElement }) {
|
||||
let topState = this.stateForDocument(topDocument);
|
||||
if (!topState.loginFormForFill) {
|
||||
log("fillForm: There is no login form anymore. The form may have been",
|
||||
"removed or the document may have changed.");
|
||||
return;
|
||||
}
|
||||
if (LoginUtils._getPasswordOrigin(topDocument.documentURI) !=
|
||||
loginFormOrigin) {
|
||||
log("fillForm: The requested origin doesn't match the one form the",
|
||||
"document. This may mean we navigated to a document from a different",
|
||||
"site before we had a chance to indicate this change in the user",
|
||||
"interface.");
|
||||
return;
|
||||
if (LoginUtils._getPasswordOrigin(topDocument.documentURI) != loginFormOrigin) {
|
||||
if (!inputElement ||
|
||||
LoginUtils._getPasswordOrigin(inputElement.ownerDocument.documentURI) != loginFormOrigin) {
|
||||
log("fillForm: The requested origin doesn't match the one form the",
|
||||
"document. This may mean we navigated to a document from a different",
|
||||
"site before we had a chance to indicate this change in the user",
|
||||
"interface.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._fillForm(topState.loginFormForFill, true, true, true, true,
|
||||
loginsFound, recipes);
|
||||
let form = topState.loginFormForFill;
|
||||
let clobberUsername = true;
|
||||
let options = {
|
||||
inputElement,
|
||||
};
|
||||
|
||||
// If we have a target input, fills it's form.
|
||||
if (inputElement) {
|
||||
form = FormLikeFactory.createFromPasswordField(inputElement);
|
||||
clobberUsername = false;
|
||||
}
|
||||
this._fillForm(form, true, clobberUsername, true, true, loginsFound, recipes, options);
|
||||
},
|
||||
|
||||
loginsFound: function({ form, loginsFound, recipes }) {
|
||||
@ -814,9 +829,11 @@ var LoginManagerContent = {
|
||||
* the user
|
||||
* @param {nsILoginInfo[]} foundLogins is an array of nsILoginInfo that could be used for the form
|
||||
* @param {Set} recipes that could be used to affect how the form is filled
|
||||
* @param {Object} [options = {}] is a list of options for this method.
|
||||
- [inputElement] is an optional target input element we want to fill
|
||||
*/
|
||||
_fillForm : function (form, autofillForm, clobberUsername, clobberPassword,
|
||||
userTriggered, foundLogins, recipes) {
|
||||
userTriggered, foundLogins, recipes, {inputElement} = {}) {
|
||||
let ignoreAutocomplete = true;
|
||||
const AUTOFILL_RESULT = {
|
||||
FILLED: 0,
|
||||
@ -855,6 +872,17 @@ var LoginManagerContent = {
|
||||
var [usernameField, passwordField, ignored] =
|
||||
this._getFormFields(form, false, recipes);
|
||||
|
||||
// If we have a password inputElement parameter and it's not
|
||||
// the same as the one heuristically found, use the parameter
|
||||
// one instead.
|
||||
if (inputElement) {
|
||||
if (inputElement.type != "password") {
|
||||
throw new Error("Unexpected input element type.");
|
||||
}
|
||||
passwordField = inputElement;
|
||||
usernameField = null;
|
||||
}
|
||||
|
||||
// Need a valid password field to do anything.
|
||||
if (passwordField == null) {
|
||||
log("not filling form, no password field found");
|
||||
|
183
toolkit/components/passwordmgr/LoginManagerContextMenu.jsm
Normal file
183
toolkit/components/passwordmgr/LoginManagerContextMenu.jsm
Normal file
@ -0,0 +1,183 @@
|
||||
/* 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.
|
||||
*/
|
||||
let 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._fillPassword(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.
|
||||
*/
|
||||
_fillPassword(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");
|
||||
});
|
@ -235,7 +235,7 @@ var LoginManagerParent = {
|
||||
* Trigger a login form fill and send relevant data (e.g. logins and recipes)
|
||||
* to the child process (LoginManagerContent).
|
||||
*/
|
||||
fillForm: Task.async(function* ({ browser, loginFormOrigin, login }) {
|
||||
fillForm: Task.async(function* ({ browser, loginFormOrigin, login, inputElement }) {
|
||||
let recipes = [];
|
||||
if (loginFormOrigin) {
|
||||
let formHost;
|
||||
@ -251,11 +251,13 @@ var LoginManagerParent = {
|
||||
// Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
|
||||
// doesn't support structured cloning.
|
||||
let jsLogins = JSON.parse(JSON.stringify([login]));
|
||||
|
||||
let objects = inputElement ? {inputElement} : null;
|
||||
browser.messageManager.sendAsyncMessage("RemoteLogins:fillForm", {
|
||||
loginFormOrigin,
|
||||
logins: jsLogins,
|
||||
recipes,
|
||||
});
|
||||
}, objects);
|
||||
}),
|
||||
|
||||
/**
|
||||
|
@ -56,7 +56,6 @@ else:
|
||||
'storage-json.js',
|
||||
]
|
||||
EXTRA_JS_MODULES += [
|
||||
'LoginDoorhangers.jsm',
|
||||
'LoginImport.jsm',
|
||||
'LoginStore.jsm',
|
||||
]
|
||||
@ -66,6 +65,12 @@ if CONFIG['OS_TARGET'] == 'WINNT':
|
||||
'OSCrypto_win.js',
|
||||
]
|
||||
|
||||
if CONFIG['MOZ_BUILD_APP'] == 'browser':
|
||||
EXTRA_JS_MODULES += [
|
||||
'LoginDoorhangers.jsm',
|
||||
'LoginManagerContextMenu.jsm',
|
||||
]
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
|
||||
with Files('**'):
|
||||
|
@ -26,3 +26,12 @@
|
||||
<!ENTITY spellDictionaries.accesskey "l">
|
||||
|
||||
<!ENTITY searchTextBox.clear.label "Clear">
|
||||
|
||||
<!ENTITY fillLoginMenu.label "Fill Login">
|
||||
<!ENTITY fillLoginMenu.accesskey "F">
|
||||
<!ENTITY fillPasswordMenu.label "Fill Password">
|
||||
<!ENTITY fillPasswordMenu.accesskey "F">
|
||||
<!ENTITY fillUsernameMenu.label "Fill Username">
|
||||
<!ENTITY fillUsernameMenu.accesskey "F">
|
||||
<!ENTITY noLoginSuggestions.label "(No Login Suggestions)">
|
||||
<!ENTITY viewSavedLogins.label "View Saved Logins">
|
||||
|
@ -56,3 +56,10 @@ removeAllPasswordsPrompt=Are you sure you wish to remove all passwords?
|
||||
removeAllPasswordsTitle=Remove all passwords
|
||||
loginsSpielAll=Passwords for the following sites are stored on your computer:
|
||||
loginsSpielFiltered=The following passwords match your search:
|
||||
# LOCALIZATION NOTE (loginHostAge):
|
||||
# This is used to show the context menu login items with their age.
|
||||
# 1st string is the username for the login, 2nd is the login's age.
|
||||
loginHostAge=%1$S (%2$S)
|
||||
# LOCALIZATION NOTE (noUsername):
|
||||
# String is used on the context menu when a login doesn't have a username.
|
||||
noUsername=No username
|
||||
|
@ -429,6 +429,9 @@ var SpellCheckHelper = {
|
||||
// Set when over an <input type="number"> or other non-text field.
|
||||
NUMERIC: 0x40,
|
||||
|
||||
// Set when over an <input type="password"> field.
|
||||
PASSWORD: 0x80,
|
||||
|
||||
isTargetAKeywordField(aNode, window) {
|
||||
if (!(aNode instanceof window.HTMLInputElement))
|
||||
return false;
|
||||
@ -479,6 +482,9 @@ var SpellCheckHelper = {
|
||||
}
|
||||
if (this.isTargetAKeywordField(element, window))
|
||||
flags |= this.KEYWORD;
|
||||
if (element.type == "password") {
|
||||
flags |= this.PASSWORD;
|
||||
}
|
||||
}
|
||||
} else if (element instanceof window.HTMLTextAreaElement) {
|
||||
flags |= this.TEXTINPUT | this.TEXTAREA;
|
||||
|
Loading…
Reference in New Issue
Block a user