Bug 1168707 - Run password autofill code when a password field is added to a document outside of a <form>. r=dolske

--HG--
rename : toolkit/components/passwordmgr/test/test_formless_submit.html => toolkit/components/passwordmgr/test/test_formless_autofill.html
extra : commitid : 21T06K7Wsbw
extra : rebase_source : 004502682fc071c2e9cbef6554b97f2f50266628
This commit is contained in:
Matthew Noorenberghe 2015-06-29 00:11:22 -07:00
Родитель b85dffd754
Коммит 16853474ad
5 изменённых файлов: 270 добавлений и 7 удалений

Просмотреть файл

@ -56,6 +56,9 @@ addEventListener("DOMFormHasPassword", function(event) {
LoginManagerContent.onDOMFormHasPassword(event, content);
InsecurePasswordUtils.checkForInsecurePasswords(event.target);
});
addEventListener("DOMInputPasswordAdded", function(event) {
LoginManagerContent.onDOMInputPasswordAdded(event, content);
});
addEventListener("pageshow", function(event) {
LoginManagerContent.onPageShow(event, content);
});

Просмотреть файл

@ -8,12 +8,14 @@ this.EXPORTED_SYMBOLS = [ "LoginManagerContent",
"UserAutoCompleteResult" ];
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
const PASSWORD_INPUT_ADDED_COALESCING_THRESHOLD_MS = 1;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoginRecipesContent",
"resource://gre/modules/LoginRecipes.jsm");
@ -93,6 +95,30 @@ var LoginManagerContent = {
_messages: [ "RemoteLogins:loginsFound",
"RemoteLogins:loginsAutoCompleted" ],
/**
* WeakMap of the root element of a FormLike to the FormLike representing its fields.
*
* This is used to be able to lookup an existing FormLike for a given root element since multiple
* calls to FormLikeFactory won't give the exact same object. When batching fills we don't always
* want to use the most recent list of elements for a FormLike since we may end up doing multiple
* fills for the same set of elements when a field gets added between arming and running the
* DeferredTask.
*
* @type {WeakMap}
*/
_formLikeByRootElement: new WeakMap(),
/**
* WeakMap of the root element of a WeakMap to the DeferredTask to fill its fields.
*
* This is used to be able to throttle fills for a FormLike since onDOMInputPasswordAdded gets
* dispatched for each password field added to a document but we only want to fill once per
* FormLike when multiple fields are added at once.
*
* @type {WeakMap}
*/
_deferredPasswordAddedTasksByRootElement: new WeakMap(),
// Map from form login requests to information about that request.
_requests: new Map(),
@ -219,6 +245,11 @@ var LoginManagerContent = {
let form = aElement.form;
let win = doc.defaultView;
if (!form) {
return Promise.reject("Bug 1173583: _autoCompleteSearchAsync needs to be " +
"updated to work outside of <form>");
}
let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI);
let actionOrigin = LoginUtils._getActionOrigin(form);
@ -246,7 +277,69 @@ var LoginManagerContent = {
}
let form = event.target;
let formLike = FormLikeFactory.createFromForm(form);
log("onDOMFormHasPassword:", form, formLike);
this._fetchLoginsFromParentAndFillForm(formLike, window);
},
onDOMInputPasswordAdded(event, window) {
if (!event.isTrusted) {
return;
}
let pwField = event.target;
if (pwField.form) {
// Handled by onDOMFormHasPassword which is already throttled.
return;
}
let formLike = FormLikeFactory.createFromPasswordField(pwField);
log("onDOMInputPasswordAdded:", pwField, formLike);
let deferredTask = this._deferredPasswordAddedTasksByRootElement.get(formLike.rootElement);
if (!deferredTask) {
log("Creating a DeferredTask to call _fetchLoginsFromParentAndFillForm soon");
this._formLikeByRootElement.set(formLike.rootElement, formLike);
deferredTask = new DeferredTask(function* deferredInputProcessing() {
// Get the updated formLike instead of the one at the time of creating the DeferredTask via
// a closure since it could be stale since FormLike.elements isn't live.
let formLike2 = this._formLikeByRootElement.get(formLike.rootElement);
log("Running deferred processing of onDOMInputPasswordAdded", formLike2);
this._deferredPasswordAddedTasksByRootElement.delete(formLike2.rootElement);
this._fetchLoginsFromParentAndFillForm(formLike2, window);
this._formLikeByRootElement.delete(formLike.rootElement);
}.bind(this), PASSWORD_INPUT_ADDED_COALESCING_THRESHOLD_MS);
this._deferredPasswordAddedTasksByRootElement.set(formLike.rootElement, deferredTask);
}
if (deferredTask.isArmed) {
log("DeferredTask is already armed so just updating the FormLike");
// We update the FormLike so it (most important .elements) is fresh when the task eventually
// runs since changes to the elements could affect our field heuristics.
this._formLikeByRootElement.set(formLike.rootElement, formLike);
} else {
if (window.document.readyState == "complete") {
log("Arming the DeferredTask we just created since document.readyState == 'complete'");
deferredTask.arm();
} else {
window.addEventListener("DOMContentLoaded", function armPasswordAddedTask() {
window.removeEventListener("DOMContentLoaded", armPasswordAddedTask);
log("Arming the onDOMInputPasswordAdded DeferredTask due to DOMContentLoaded");
deferredTask.arm();
});
}
}
},
/**
* Fetch logins from the parent for a given form and then attempt to fill it.
*
* @param {FormLike} form to fetch the logins for then try autofill.
* @param {Window} window
*/
_fetchLoginsFromParentAndFillForm(form, window) {
// Always record the most recently added form with a password field.
this.stateForDocument(form.ownerDocument).loginForm = form;
@ -259,7 +352,6 @@ var LoginManagerContent = {
return;
}
log("onDOMFormHasPassword for", form.ownerDocument.documentURI);
this._getLoginDataFromParent(form, { showMasterPassword: true })
.then(this.loginsFound.bind(this))
.then(null, Cu.reportError);
@ -402,7 +494,7 @@ var LoginManagerContent = {
if (!this._isUsernameFieldType(acInputField))
return;
var acForm = acInputField.form;
var acForm = acInputField.form; // XXX: Bug 1173583 - This doesn't work outside of <form>.
if (!acForm)
return;
@ -515,8 +607,10 @@ var LoginManagerContent = {
fieldOverrideRecipe.passwordSelector
);
if (pwOverrideField) {
// The field from the password override may be in a different FormLike.
let formLike = FormLikeFactory.createFromPasswordField(pwOverrideField);
pwFields = [{
index : [...pwOverrideField.form.elements].indexOf(pwOverrideField),
index : [...formLike.elements].indexOf(pwOverrideField),
element : pwOverrideField,
}];
}
@ -914,7 +1008,7 @@ var LoginManagerContent = {
let messageManager = messageManagerFromWindow(win);
messageManager.sendAsyncMessage("LoginStats:LoginFillSuccessful");
} finally {
Services.obs.notifyObservers(form, "passwordmgr-processed-form", null);
Services.obs.notifyObservers(form.rootElement, "passwordmgr-processed-form", null);
}
},

Просмотреть файл

@ -5,6 +5,7 @@ support-files =
notification_common.js
pwmgr_common.js
[test_formless_autofill.html]
[test_formless_submit.html]
[test_privbrowsing_perwindowpb.html]
skip-if = true # Bug 1173337

Просмотреть файл

@ -182,11 +182,11 @@ function commonInit(selfFilling) {
form.appendChild(password);
var observer = SpecialPowers.wrapCallback(function(subject, topic, data) {
var form = subject.QueryInterface(SpecialPowers.Ci.nsIDOMNode);
if (form.id !== 'observerforcer')
var formLikeRoot = subject.QueryInterface(SpecialPowers.Ci.nsIDOMNode);
if (formLikeRoot.id !== 'observerforcer')
return;
SpecialPowers.removeObserver(observer, "passwordmgr-processed-form");
form.parentNode.removeChild(form);
formLikeRoot.remove();
SimpleTest.executeSoon(() => {
var event = new Event("runTests");
window.dispatchEvent(event);
@ -259,6 +259,23 @@ function dumpLogin(label, login) {
ok(true, label + loginText);
}
/**
* Resolves when a specified number of forms have been processed.
*/
function promiseFormsProcessed(expectedCount = 1) {
var processedCount = 0;
return new Promise((resolve, reject) => {
function onProcessedForm(subject, topic, data) {
processedCount++;
if (processedCount == expectedCount) {
SpecialPowers.removeObserver(onProcessedForm, "passwordmgr-processed-form");
resolve(subject, data);
}
}
SpecialPowers.addObserver(onProcessedForm, "passwordmgr-processed-form", false);
});
}
// Code to run when loaded as a chrome script in tests via loadChromeScript
if (this.addMessageListener) {
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;

Просмотреть файл

@ -0,0 +1,148 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Test autofilling of fields outside of a form</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="pwmgr_common.js"></script>
<link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
</head>
<body>
<script type="application/javascript;version=1.8">
SimpleTest.waitForExplicitFinish();
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
Cu.import("resource://gre/modules/Task.jsm");
const LMCBackstagePass = Cu.import("resource://gre/modules/LoginManagerContent.jsm");
const { LoginManagerContent, FormLikeFactory } = LMCBackstagePass;
let parentScriptURL = SimpleTest.getTestFileURL("pwmgr_common.js");
let mm = SpecialPowers.loadChromeScript(parentScriptURL);
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("loginFrame").addEventListener("load", (evt) => {
// Tell the parent to setup test logins.
mm.sendAsyncMessage("setupParent");
});
});
// When the setup is done, load a recipe for this test.
mm.addMessageListener("doneSetup", function doneSetup() {
mm.sendAsyncMessage("loadRecipes", {
siteRecipes: [{
hosts: ["mochi.test:8888"],
usernameSelector: "input[name='recipeuname']",
passwordSelector: "input[name='recipepword']",
}],
});
});
mm.addMessageListener("loadedRecipes", () => runTest());
const DEFAULT_ORIGIN = "http://mochi.test:8888";
const TESTCASES = [
{
// Inputs
document: `<input type=password>`,
// Expected outputs
expectedInputValues: ["testpass"],
},
{
document: `<input>
<input type=password>`,
expectedInputValues: ["testuser", "testpass"],
},
{
document: `<input>
<input type=password>
<input type=password>`,
expectedInputValues: ["testuser", "testpass", ""],
},
{
document: `<input>
<input type=password>
<input type=password>
<input type=password>`,
expectedInputValues: ["testuser", "testpass", "", ""],
},
{
document: `<input>
<input type=password form="form1">
<input type=password>
<form id="form1">
<input>
<input type=password>
</form>`,
expectedFormCount: 2,
expectedInputValues: ["testuser", "testpass", "testpass", "", ""],
},
{
document: `<!-- formless password field selector recipe test -->
<input>
<input type=password>
<input>
<input type=password name="recipepword">`,
expectedInputValues: ["", "", "testuser", "testpass"],
},
{
document: `<!-- formless username and password field selector recipe test -->
<input name="recipeuname">
<input>
<input type=password>
<input type=password name="recipepword">`,
expectedInputValues: ["testuser", "", "", "testpass"],
},
{
document: `<!-- form and formless recipe field selector test -->
<input name="recipeuname">
<input>
<input type=password form="form1"> <!-- not filled since recipe affects both FormLikes -->
<input type=password>
<input type=password name="recipepword">
<form id="form1">
<input>
<input type=password>
</form>`,
expectedFormCount: 2,
expectedInputValues: ["testuser", "", "", "", "testpass", "", ""],
},
];
let runTest = Task.async(function*() {
let loginFrame = document.getElementById("loginFrame");
let frameDoc = loginFrame.contentWindow.document;
for (let tc of TESTCASES) {
info("Starting testcase: " + JSON.stringify(tc));
let numFormLikesExpected = tc.expectedFormCount || 1;
let processedFormPromise = promiseFormsProcessed(numFormLikesExpected);
frameDoc.documentElement.innerHTML = tc.document;
info("waiting for " + numFormLikesExpected + " processed form(s)");
yield processedFormPromise;
let testInputs = frameDoc.documentElement.querySelectorAll("input");
is(testInputs.length, tc.expectedInputValues.length, "Check number of inputs");
for (let i = 0; i < tc.expectedInputValues.length; i++) {
let expectedValue = tc.expectedInputValues[i];
is(testInputs[i].value, expectedValue,
"Check expected input value " + i + ": " + expectedValue);
}
}
SimpleTest.finish();
});
</script>
<p id="display"></p>
<div id="content">
<iframe id="loginFrame" src="http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/blank.html"></iframe>
</div>
<pre id="test"></pre>
</body>
</html>