Bug 1166947 - Run login capture code upon page navigation if there are password fields present outside a <form>. r=dolske

MozReview-Commit-ID: Edx3XM3caX3

--HG--
rename : toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html => toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation.html
rename : toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html => toolkit/components/passwordmgr/test/mochitest/test_formless_submit_navigation_negative.html
This commit is contained in:
Matthew Noorenberghe 2016-07-27 22:12:25 -07:00
Родитель 1ecad9867e
Коммит 141ab0bb7f
8 изменённых файлов: 563 добавлений и 11 удалений

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

@ -4287,6 +4287,7 @@ pref("signon.rememberSignons.visibilityToggle", false);
#endif
pref("signon.autofillForms", true);
pref("signon.autologin.proxy", false);
pref("signon.formlessCapture.enabled", true);
pref("signon.storeWhenAutocompleteOff", true);
pref("signon.ui.experimental", false);
pref("signon.debug", false);

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

@ -35,6 +35,7 @@ this.LoginHelper = {
* Warning: these only update if a logger was created.
*/
debug: Services.prefs.getBoolPref("signon.debug"),
formlessCaptureEnabled: Services.prefs.getBoolPref("signon.formlessCapture.enabled"),
schemeUpgrades: Services.prefs.getBoolPref("signon.schemeUpgrades"),
createLogger(aLogPrefix) {
@ -53,6 +54,7 @@ this.LoginHelper = {
// Watch for pref changes and update this.debug and the maxLogLevel for created loggers
Services.prefs.addObserver("signon.", () => {
this.debug = Services.prefs.getBoolPref("signon.debug");
this.formlessCaptureEnabled = Services.prefs.getBoolPref("signon.formlessCapture.enabled");
this.schemeUpgrades = Services.prefs.getBoolPref("signon.schemeUpgrades");
logger.maxLogLevel = getMaxLogLevel();
}, false);

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

@ -44,6 +44,7 @@ var gEnabled, gAutofillForms, gStoreWhenAutocompleteOff;
var observer = {
QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsIFormSubmitObserver,
Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference]),
// nsIFormSubmitObserver
@ -69,6 +70,45 @@ var observer = {
gAutofillForms = Services.prefs.getBoolPref("signon.autofillForms");
gStoreWhenAutocompleteOff = Services.prefs.getBoolPref("signon.storeWhenAutocompleteOff");
},
// nsIWebProgressListener
onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
// Only handle pushState/replaceState here.
if (!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) ||
!(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE)) {
return;
}
log("onLocationChange handled:", aLocation.spec, aWebProgress.DOMWindow.document);
LoginManagerContent._onNavigation(aWebProgress.DOMWindow.document);
},
onStateChange(aWebProgress, aRequest, aState, aStatus) {
if (!(aState & Ci.nsIWebProgressListener.STATE_START)) {
return;
}
// We only care about when a page triggered a load, not the user. For example:
// clicking refresh/back/forward, typing a URL and hitting enter, and loading a bookmark aren't
// likely to be when a user wants to save a login.
let channel = aRequest.QueryInterface(Ci.nsIChannel);
let triggeringPrincipal = channel.loadInfo.triggeringPrincipal;
if (triggeringPrincipal.isNullPrincipal ||
triggeringPrincipal.equals(Services.scriptSecurityManager.getSystemPrincipal())) {
return;
}
// Don't handle history navigation, reload, or pushState not triggered via chrome UI.
// e.g. history.go(-1), location.reload(), history.replaceState()
if (!(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_NORMAL)) {
log("onStateChange: loadType isn't LOAD_CMD_NORMAL:", aWebProgress.loadType);
return;
}
log("onStateChange handled:", channel);
LoginManagerContent._onNavigation(aWebProgress.DOMWindow.document);
},
};
Services.obs.addObserver(observer, "earlyformsubmit", false);
@ -279,6 +319,25 @@ var LoginManagerContent = {
messageData);
},
setupProgressListener(window) {
if (!LoginHelper.formlessCaptureEnabled) {
return;
}
try {
let webProgress = window.QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIWebNavigation).
QueryInterface(Ci.nsIDocShell).
QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(observer,
Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
Ci.nsIWebProgress.NOTIFY_LOCATION);
} catch (ex) {
// Ignore NS_ERROR_FAILURE if the progress listener was already added
}
},
onDOMFormHasPassword(event, window) {
if (!event.isTrusted) {
return;
@ -295,6 +354,8 @@ var LoginManagerContent = {
return;
}
this.setupProgressListener(window);
let pwField = event.target;
if (pwField.form) {
// Handled by onDOMFormHasPassword which is already throttled.
@ -316,7 +377,6 @@ var LoginManagerContent = {
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);
@ -348,9 +408,6 @@ var LoginManagerContent = {
* @param {Window} window
*/
_fetchLoginsFromParentAndFillForm(form, window) {
// Always record the most recently added form with a password field.
this.stateForDocument(form.ownerDocument).loginForm = form;
this._updateLoginFormPresence(window);
let messageManager = messageManagerFromWindow(window);
@ -377,12 +434,14 @@ var LoginManagerContent = {
/**
* Retrieves a reference to the state object associated with the given
* document. This is initialized to an empty object.
* document. This is initialized to an object with default values.
*/
stateForDocument(document) {
let loginFormState = this.loginFormStateByDocument.get(document);
if (!loginFormState) {
loginFormState = {};
loginFormState = {
loginFormRootElements: new Set(),
};
this.loginFormStateByDocument.set(document, loginFormState);
}
return loginFormState;
@ -394,6 +453,7 @@ var LoginManagerContent = {
* visibility of the password fill doorhanger anchor.
*/
_updateLoginFormPresence(topWindow) {
log("_updateLoginFormPresence", topWindow.location.href);
// For the login form presence notification, we currently support only one
// origin for each browser, so the form origin will always match the origin
// of the top level document.
@ -403,9 +463,9 @@ var LoginManagerContent = {
// Returns the first known loginForm present in this window or in any
// same-origin subframes. Returns null if no loginForm is currently present.
let getFirstLoginForm = thisWindow => {
let loginForm = this.stateForDocument(thisWindow.document).loginForm;
if (loginForm) {
return loginForm;
let loginForms = this.stateForDocument(thisWindow.document).loginFormRootElements;
if (loginForms.size) {
return [...loginForms][0];
}
for (let i = 0; i < thisWindow.frames.length; i++) {
let frame = thisWindow.frames[i];
@ -425,7 +485,7 @@ var LoginManagerContent = {
let hasInsecureLoginForms = (thisWindow, parentIsInsecure) => {
let doc = thisWindow.document;
let isInsecure = parentIsInsecure || !this.isDocumentSecure(doc);
let hasLoginForm = !!this.stateForDocument(doc).loginForm;
let hasLoginForm = this.stateForDocument(doc).loginFormRootElements.size > 0;
return (hasLoginForm && isInsecure) ||
Array.some(thisWindow.frames,
frame => hasInsecureLoginForms(frame, isInsecure));
@ -434,6 +494,7 @@ var LoginManagerContent = {
// Store the actual form to use on the state for the top-level document.
let topState = this.stateForDocument(topWindow.document);
topState.loginFormForFill = getFirstLoginForm(topWindow);
log("_updateLoginFormPresence: topState.loginFormForFill", topState.loginFormForFill);
// Determine whether to show the anchor icon for the current tab.
let messageManager = messageManagerFromWindow(topWindow);
@ -739,8 +800,11 @@ var LoginManagerContent = {
}
log("Password field (new) id/name is: ", newPasswordField.id, " / ", newPasswordField.name);
if (oldPasswordField)
if (oldPasswordField) {
log("Password field (old) id/name is: ", oldPasswordField.id, " / ", oldPasswordField.name);
} else {
log("Password field (old):", oldPasswordField);
}
return [usernameField, newPasswordField, oldPasswordField];
},
@ -753,6 +817,37 @@ var LoginManagerContent = {
return element && element.autocomplete == "off";
},
/**
* Trigger capture on any relevant FormLikes due to a navigation alone (not
* necessarily due to an actual form submission). This method is used to
* capture logins for cases where form submit events are not used.
*
* To avoid multiple notifications for the same FormLike, this currently
* avoids capturing when dealing with a real <form> which are ideally already
* using a submit event.
*
* @param {Document} document being navigated
*/
_onNavigation(aDocument) {
let state = this.stateForDocument(aDocument);
let loginFormRootElements = state.loginFormRootElements;
log("_onNavigation: state:", state, "loginFormRootElements size:", loginFormRootElements.size,
"document:", aDocument);
for (let formRoot of state.loginFormRootElements) {
if (formRoot instanceof Ci.nsIDOMHTMLFormElement) {
// For now only perform capture upon navigation for FormLike's without
// a <form> to avoid capture from both an earlyformsubmit and
// navigation for the same "form".
log("Ignoring navigation for the form root to avoid multiple prompts " +
"since it was for a real <form>");
continue;
}
let formLike = this._formLikeByRootElement.get(formRoot);
this._onFormSubmit(formLike);
}
},
/**
* Called by our observer when notified of a form submission.
* [Note that this happens before any DOM onsubmit handlers are invoked.]
@ -762,6 +857,7 @@ var LoginManagerContent = {
* @param {FormLike} form
*/
_onFormSubmit(form) {
log("_onFormSubmit", form);
var doc = form.ownerDocument;
var win = doc.defaultView;
@ -856,6 +952,7 @@ var LoginManagerContent = {
*/
_fillForm(form, autofillForm, clobberUsername, clobberPassword,
userTriggered, foundLogins, recipes, {inputElement} = {}) {
log("_fillForm", form.elements);
let ignoreAutocomplete = true;
const AUTOFILL_RESULT = {
FILLED: 0,
@ -1163,6 +1260,7 @@ var LoginUtils = {
if (allowJS && uri.scheme == "javascript")
return "javascript:";
// Build this manually instead of using prePath to avoid including the userPass portion.
realm = uri.scheme + "://" + uri.hostPort;
} catch (e) {
// bug 159484 - disallow url types that don't support a hostPort.
@ -1313,6 +1411,12 @@ var FormLikeFactory = {
}
this._addToJSONProperty(formLike);
let state = LoginManagerContent.stateForDocument(formLike.ownerDocument);
state.loginFormRootElements.add(formLike.rootElement);
log("adding", formLike.rootElement, "to loginFormRootElements for", formLike.ownerDocument);
LoginManagerContent._formLikeByRootElement.set(formLike.rootElement, formLike);
return formLike;
},
@ -1361,6 +1465,13 @@ var FormLikeFactory = {
rootElement: doc.documentElement,
};
let state = LoginManagerContent.stateForDocument(formLike.ownerDocument);
state.loginFormRootElements.add(formLike.rootElement);
log("adding", formLike.rootElement, "to loginFormRootElements for", formLike.ownerDocument);
LoginManagerContent._formLikeByRootElement.set(formLike.rootElement, formLike);
this._addToJSONProperty(formLike);
return formLike;
},

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

@ -40,6 +40,7 @@ support-files =
[browser_DOMFormHasPassword.js]
[browser_DOMInputPasswordAdded.js]
[browser_filldoorhanger.js]
[browser_formless_submit_chrome.js]
[browser_hasInsecureLoginForms.js]
[browser_hasInsecureLoginForms_streamConverter.js]
[browser_insecurePasswordWarning.js]

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

@ -0,0 +1,122 @@
/*
* Test that browser chrome UI interactions don't trigger a capture doorhanger.
*/
"use strict";
function* fillTestPage(aBrowser) {
yield ContentTask.spawn(aBrowser, null, function*() {
content.document.getElementById("form-basic-username").value = "my_username";
content.document.getElementById("form-basic-password").value = "my_password";
});
info("fields filled");
}
function* withTestPage(aTaskFn) {
return BrowserTestUtils.withNewTab({
gBrowser,
url: "https://example.com" + DIRECTORY_PATH + "formless_basic.html",
}, function*(aBrowser) {
info("tab opened");
yield fillTestPage(aBrowser);
yield* aTaskFn(aBrowser);
// Give a chance for the doorhanger to appear
yield new Promise(resolve => SimpleTest.executeSoon(resolve));
ok(!getCaptureDoorhanger("any"), "No doorhanger should be present");
});
}
add_task(function* setup() {
yield SimpleTest.promiseFocus(window);
});
add_task(function* test_urlbar_new_URL() {
yield withTestPage(function*(aBrowser) {
gURLBar.value = "";
let focusPromise = BrowserTestUtils.waitForEvent(gURLBar, "focus");
gURLBar.focus();
yield focusPromise;
info("focused");
EventUtils.sendString("http://mochi.test:8888/");
EventUtils.synthesizeKey("VK_RETURN", {});
yield BrowserTestUtils.browserLoaded(aBrowser, false, "http://mochi.test:8888/");
});
});
add_task(function* test_urlbar_fragment_enter() {
yield withTestPage(function*(aBrowser) {
gURLBar.focus();
EventUtils.synthesizeKey("VK_RIGHT", {});
EventUtils.sendString("#fragment");
EventUtils.synthesizeKey("VK_RETURN", {});
});
});
add_task(function* test_backButton_forwardButton() {
yield withTestPage(function*(aBrowser) {
// Load a new page in the tab so we can test going back
aBrowser.loadURI("https://example.com" + DIRECTORY_PATH + "formless_basic.html?second");
yield BrowserTestUtils.browserLoaded(aBrowser, false,
"https://example.com" + DIRECTORY_PATH +
"formless_basic.html?second");
yield fillTestPage(aBrowser);
let backPromise = BrowserTestUtils.browserStopped(aBrowser);
EventUtils.synthesizeMouseAtCenter(document.getElementById("back-button"), {});
yield backPromise;
// Give a chance for the doorhanger to appear
yield new Promise(resolve => SimpleTest.executeSoon(resolve));
ok(!getCaptureDoorhanger("any"), "No doorhanger should be present");
// Now go forward again after filling
yield fillTestPage(aBrowser);
let forwardButton = document.getElementById("forward-button");
yield BrowserTestUtils.waitForEvent(forwardButton, "transitionend");
info("transition done");
yield BrowserTestUtils.waitForCondition(() => {
return forwardButton.disabled == false;
});
let forwardPromise = BrowserTestUtils.browserStopped(aBrowser);
info("click the forward button");
EventUtils.synthesizeMouseAtCenter(forwardButton, {});
yield forwardPromise;
});
});
add_task(function* test_reloadButton() {
yield withTestPage(function*(aBrowser) {
let reloadButton = document.getElementById("urlbar-reload-button");
let loadPromise = BrowserTestUtils.browserLoaded(aBrowser, false,
"https://example.com" + DIRECTORY_PATH +
"formless_basic.html");
yield BrowserTestUtils.waitForCondition(() => {
return reloadButton.disabled == false;
});
EventUtils.synthesizeMouseAtCenter(reloadButton, {});
yield loadPromise;
});
});
add_task(function* test_back_keyboard_shortcut() {
if (Services.prefs.getIntPref("browser.backspace_action") != 0) {
ok(true, "Skipped testing backspace to go back since it's disabled");
return;
}
yield withTestPage(function*(aBrowser) {
// Load a new page in the tab so we can test going back
aBrowser.loadURI("https://example.com" + DIRECTORY_PATH + "formless_basic.html?second");
yield BrowserTestUtils.browserLoaded(aBrowser, false,
"https://example.com" + DIRECTORY_PATH +
"formless_basic.html?second");
yield fillTestPage(aBrowser);
let backPromise = BrowserTestUtils.browserStopped(aBrowser);
EventUtils.synthesizeKey("VK_BACK_SPACE", {});
yield backPromise;
});
});

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

@ -6,6 +6,7 @@ support-files =
../../../satchel/test/parent_utils.js
../../../satchel/test/satchel_common.js
../authenticate.sjs
../blank.html
../browser/form_basic.html
../browser/form_cross_origin_secure_action.html
../notification_common.js
@ -41,6 +42,8 @@ skip-if = toolkit == 'android' # autocomplete
skip-if = toolkit == 'android' # Bug 1259768
[test_formless_submit.html]
skip-if = toolkit == 'android' # Bug 1259768
[test_formless_submit_navigation.html]
[test_formless_submit_navigation_negative.html]
[test_input_events.html]
[test_input_events_for_identical_values.html]
[test_maxlength.html]

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

@ -0,0 +1,191 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Test capturing of fields outside of a form due to navigation</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/SpawnTask.js"></script>
<script src="pwmgr_common.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css" />
</head>
<body>
<script type="application/javascript;version=1.8">
const LMCBackstagePass = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerContent.jsm");
const { LoginManagerContent, FormLikeFactory } = LMCBackstagePass;
let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
let loadPromise = new Promise(resolve => {
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("loginFrame").addEventListener("load", (evt) => {
resolve();
});
});
});
add_task(function* setup() {
yield SpecialPowers.pushPrefEnv({
set: [
["signon.formlessCapture.enabled", true],
],
});
info("Waiting for page and frame loads");
yield loadPromise;
yield loadRecipes({
siteRecipes: [{
hosts: ["test1.mochi.test:8888"],
usernameSelector: "input[name='recipeuname']",
passwordSelector: "input[name='recipepword']",
}],
});
});
const DEFAULT_ORIGIN = "http://test1.mochi.test:8888";
const SCRIPTS = {
PUSHSTATE: `history.pushState({}, "Pushed state", "?pushed");`,
WINDOW_LOCATION: `window.location = "data:text/html;charset=utf-8,window.location";`,
};
const TESTCASES = [
{
// Inputs
document: `<input type=password value="pass1">`,
// Expected outputs similar to RemoteLogins:onFormSubmit
hostname: DEFAULT_ORIGIN,
formSubmitURL: DEFAULT_ORIGIN,
usernameFieldValue: null,
newPasswordFieldValue: "pass1",
oldPasswordFieldValue: null,
},
{
document: `<input value="user1">
<input type=password value="pass1">`,
hostname: DEFAULT_ORIGIN,
formSubmitURL: DEFAULT_ORIGIN,
usernameFieldValue: "user1",
newPasswordFieldValue: "pass1",
oldPasswordFieldValue: null,
},
{
document: `<input value="user1">
<input type=password value="pass1">
<input type=password value="pass2">`,
hostname: DEFAULT_ORIGIN,
formSubmitURL: DEFAULT_ORIGIN,
usernameFieldValue: "user1",
newPasswordFieldValue: "pass2",
oldPasswordFieldValue: "pass1",
},
{
document: `<input value="user1">
<input type=password value="pass1">
<input type=password value="pass2">
<input type=password value="pass2">`,
hostname: DEFAULT_ORIGIN,
formSubmitURL: DEFAULT_ORIGIN,
usernameFieldValue: "user1",
newPasswordFieldValue: "pass2",
oldPasswordFieldValue: "pass1",
},
{
document: `<input value="user1">
<input type=password value="user2" form="form1">
<input type=password value="pass1">
<form id="form1">
<input value="user3">
<input type=password value="pass2">
</form>`,
hostname: DEFAULT_ORIGIN,
formSubmitURL: DEFAULT_ORIGIN,
usernameFieldValue: "user1",
newPasswordFieldValue: "pass1",
oldPasswordFieldValue: null,
},
{
document: `<!-- recipe field override -->
<input name="recipeuname" value="username from recipe">
<input value="default field username">
<input type=password value="pass1">
<input name="recipepword" type=password value="pass2">`,
hostname: DEFAULT_ORIGIN,
formSubmitURL: DEFAULT_ORIGIN,
usernameFieldValue: "username from recipe",
newPasswordFieldValue: "pass2",
oldPasswordFieldValue: null,
},
];
function getSubmitMessage() {
info("getSubmitMessage");
return new Promise((resolve, reject) => {
chromeScript.addMessageListener("formSubmissionProcessed", function processed(...args) {
info("got formSubmissionProcessed");
chromeScript.removeMessageListener("formSubmissionProcessed", processed);
resolve(...args);
});
});
}
add_task(function* test() {
let loginFrame = document.getElementById("loginFrame");
for (let tc of TESTCASES) {
for (let scriptName of Object.keys(SCRIPTS)) {
info("Starting testcase with script " + scriptName + ": " + JSON.stringify(tc));
let loadedPromise = new Promise((resolve) => {
loginFrame.addEventListener("load", function frameLoaded() {
loginFrame.removeEventListener("load", frameLoaded);
resolve();
});
});
loginFrame.src = DEFAULT_ORIGIN + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html";
yield loadedPromise;
let frameDoc = SpecialPowers.wrap(loginFrame.contentWindow).document;
frameDoc.documentElement.innerHTML = tc.document;
// Wait for the form to be processed before trying to submit.
yield promiseFormsProcessed();
let processedPromise = getSubmitMessage();
info("Running " + scriptName + " script to cause a submission");
frameDoc.defaultView.eval(SCRIPTS[scriptName]);
let submittedResult = yield processedPromise;
// Check data sent via RemoteLogins:onFormSubmit
is(submittedResult.hostname, tc.hostname, "Check hostname");
is(submittedResult.formSubmitURL, tc.formSubmitURL, "Check formSubmitURL");
if (tc.usernameFieldValue === null) {
is(submittedResult.usernameField, tc.usernameFieldValue, "Check usernameField");
} else {
is(submittedResult.usernameField.value, tc.usernameFieldValue, "Check usernameField");
}
is(submittedResult.newPasswordField.value, tc.newPasswordFieldValue, "Check newPasswordFieldValue");
if (tc.oldPasswordFieldValue === null) {
is(submittedResult.oldPasswordField, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue");
} else {
is(submittedResult.oldPasswordField.value, tc.oldPasswordFieldValue, "Check oldPasswordFieldValue");
}
}
}
});
</script>
<p id="display"></p>
<div id="content">
<iframe id="loginFrame" src="http://test1.mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe>
</div>
<pre id="test"></pre>
</body>
</html>

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

@ -0,0 +1,121 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Test no capturing of fields outside of a form due to navigation</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/SpawnTask.js"></script>
<script src="pwmgr_common.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css" />
</head>
<body>
<script type="application/javascript;version=1.8">
const LMCBackstagePass = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerContent.jsm");
const { LoginManagerContent, FormLikeFactory } = LMCBackstagePass;
SimpleTest.requestFlakyTimeout("Testing that a message doesn't arrive");
let chromeScript = runInParent(SimpleTest.getTestFileURL("pwmgr_common.js"));
let loadPromise = new Promise(resolve => {
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("loginFrame").addEventListener("load", (evt) => {
resolve();
});
});
});
function submissionProcessed(...args) {
ok(false, "No formSubmissionProcessed should occur in this test");
info("got: " + JSON.stringify(args));
}
add_task(function* setup() {
yield SpecialPowers.pushPrefEnv({
set: [
["signon.formlessCapture.enabled", true],
],
});
info("Waiting for page and frame loads");
yield loadPromise;
chromeScript.addMessageListener("formSubmissionProcessed", submissionProcessed);
SimpleTest.registerCleanupFunction(() => {
chromeScript.removeMessageListener("formSubmissionProcessed", submissionProcessed);
});
});
const DEFAULT_ORIGIN = "http://test1.mochi.test:8888";
const SCRIPTS = {
PUSHSTATE: `history.pushState({}, "Pushed state", "?pushed");`,
WINDOW_LOCATION: `window.location = "data:text/html;charset=utf-8,window.location";`,
WINDOW_LOCATION_RELOAD: `window.location.reload();`,
HISTORY_BACK: `history.back();`,
HISTORY_GO_MINUS1: `history.go(-1);`,
};
const TESTCASES = [
// Begin test cases that shouldn't trigger capture.
{
// For now we don't trigger upon navigation if <form> is used.
document: `<form><input type=password value="pass1"></form>`,
},
{
// Empty password field
document: `<input type=password value="">`,
},
{
// Test with an input that would normally be captured but with SCRIPTS that
// shouldn't trigger capture.
document: `<input type=password value="pass2">`,
wouldCapture: true,
},
];
add_task(function* test() {
let loginFrame = document.getElementById("loginFrame");
for (let tc of TESTCASES) {
for (let scriptName of Object.keys(SCRIPTS)) {
if (tc.wouldCapture && ["PUSHSTATE", "WINDOW_LOCATION"].includes(scriptName)) {
// Don't run scripts that should actually capture for this testcase.
continue;
}
info("Starting testcase with script " + scriptName + ": " + JSON.stringify(tc));
let loadedPromise = new Promise((resolve) => {
loginFrame.addEventListener("load", function frameLoaded() {
loginFrame.removeEventListener("load", frameLoaded);
resolve();
});
});
loginFrame.src = DEFAULT_ORIGIN + "/tests/toolkit/components/passwordmgr/test/mochitest/blank.html";
yield loadedPromise;
let frameDoc = SpecialPowers.wrap(loginFrame.contentWindow).document;
frameDoc.documentElement.innerHTML = tc.document;
// Wait for the form to be processed before trying to submit.
yield promiseFormsProcessed();
info("Running " + scriptName + " script to check for a submission");
frameDoc.defaultView.eval(SCRIPTS[scriptName]);
// Wait for 500ms to see if the promise above resolves.
yield new Promise(resolve => setTimeout(resolve, 500));
ok(true, "Done waiting for captures");
}
}
});
</script>
<p id="display"></p>
<div id="content">
<iframe id="loginFrame" src="http://test1.mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe>
</div>
<pre id="test"></pre>
</body>
</html>