зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1166947 - Run login capture code upon page navigation if there are password fields present. 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:
Родитель
44386dff2e
Коммит
ae959f8295
|
@ -4294,6 +4294,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
|
||||
|
@ -39,6 +40,8 @@ skip-if = toolkit == 'android' # autocomplete
|
|||
[test_form_action_javascript.html]
|
||||
[test_formless_autofill.html]
|
||||
[test_formless_submit.html]
|
||||
[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>
|
Загрузка…
Ссылка в новой задаче