зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
b85dffd754
Коммит
16853474ad
|
@ -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>
|
Загрузка…
Ссылка в новой задаче