Bug 1729640 - P3. Notify event when a form with password is submitted r=sfoster,tgiles,geckoview-reviewers,agi

Notify "passwordmgr-form-submission-detected" when a form with password
is submitted.
The event should be notified regardless of whether the password manager
decides to show the doorhanger or not. To support this, this patch
also refactors _maybeSendFormInteractionMessage function to distinguish
"onFormSubmit" event and "showDoorhanger" event.

Differential Revision: https://phabricator.services.mozilla.com/D127104
This commit is contained in:
Dimi 2021-11-02 13:47:48 +00:00
Родитель 0a9d7a3a16
Коммит c31102442d
21 изменённых файлов: 220 добавлений и 77 удалений

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

@ -39,7 +39,7 @@ add_task(async function test_policy_masterpassword_doorhanger() {
// Submit the form with the new credentials. This will cause the doorhanger
// notification to be displayed.
let formSubmittedPromise = listenForTestNotification("FormSubmit");
let formSubmittedPromise = listenForTestNotification("ShowDoorhanger");
await SpecialPowers.spawn(browser, [], async function() {
let doc = this.content.document;
doc.getElementById("form-basic").submit();

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

@ -72,7 +72,7 @@ class GeckoViewAutoFillChild extends GeckoViewActorChild {
}
break;
}
case "PasswordManager:onFormSubmit": {
case "PasswordManager:ShowDoorhanger": {
const { form: formLike } = aEvent.detail;
this._autofill.commitAutofill(formLike);
break;

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

@ -689,7 +689,7 @@ function startup() {
mozSystemGroup: true,
capture: false,
},
"PasswordManager:onFormSubmit": {},
"PasswordManager:ShowDoorhanger": {},
},
},
allFrames: true,

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

@ -1928,7 +1928,7 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild {
this._maybeSendFormInteractionMessage(
form,
"PasswordManager:onFormSubmit",
"PasswordManager:ShowDoorhanger",
{
targetField: null,
isSubmission: true,
@ -1943,7 +1943,15 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild {
* Extracts and validates information from a form-like element on the page. If validation is
* successful, sends a message to the parent process requesting that it show a dialog.
*
* If validation fails, this method is a noop.
* The validation works are divided into two parts:
* 1. Whether this is a valid form with a password (validate in this function)
* 2. Whether the password manager decides to send interaction message for this form
* (validate in _maybeSendFormInteractionMessageContinue)
*
* When the function is triggered by a form submission event, and the form is valid (pass #1),
* We still send the message to the parent even the validation of #2 fails. This is because
* there might be someone who is interested in form submission events regardless of whether
* the password manager decides to show the doorhanger or not.
*
* @param {LoginForm} form
* @param {string} messageName used to categorize the type of message sent to the parent process.
@ -1951,6 +1959,9 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild {
* @param {boolean} options.isSubmission if true, this function call was prompted by a form submission.
* @param {boolean?} options.triggeredByFillingGenerated whether or not this call was triggered by a
* generated password being filled into a form-like element.
* @param {boolean?} options.ignoreConnect Whether to ignore isConnected attribute of a element.
*
* @returns {Boolean} whether the message is sent to the parent process.
*/
_maybeSendFormInteractionMessage(
form,
@ -1959,12 +1970,90 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild {
) {
let doc = form.ownerDocument;
let win = doc.defaultView;
let logMessagePrefix = isSubmission ? "form submission" : "field edit";
let passwordField = null;
if (targetField && targetField.hasBeenTypePassword) {
passwordField = targetField;
}
let origin = LoginHelper.getLoginOrigin(doc.documentURI);
if (!origin) {
log(`(${logMessagePrefix} ignored -- invalid origin)`);
return;
}
// Get the appropriate fields from the form.
let recipes = LoginRecipesContent.getRecipes(this, origin, win);
let fields = {
targetField,
...this._getFormFields(form, true, recipes, { ignoreConnect }),
};
// It's possible the field triggering this message isn't one of those found by _getFormFields' heuristics
if (
passwordField &&
passwordField != fields.newPasswordField &&
passwordField != fields.oldPasswordField &&
passwordField != fields.confirmPasswordField
) {
fields.newPasswordField = passwordField;
}
// Need at least 1 valid password field to do anything.
if (fields.newPasswordField == null) {
if (isSubmission && fields.usernameField) {
log(
"_onFormSubmit: username-only form. Record the username field but not sending prompt"
);
this.stateForDocument(doc).mockUsernameOnlyField = {
name: fields.usernameField.name,
value: fields.usernameField.value,
};
}
return;
}
this._maybeSendFormInteractionMessageContinue(form, messageName, {
...fields,
isSubmission,
triggeredByFillingGenerated,
});
if (isSubmission) {
// Notify `PasswordManager:onFormSubmit` as long as we detect submission event on a
// valid form with a password field.
this.sendAsyncMessage(
"PasswordManager:onFormSubmit",
{},
{
fields,
isSubmission,
triggeredByFillingGenerated,
}
);
}
}
/**
* Continues the works that are not done in _maybeSendFormInteractionMessage.
* See comments in _maybeSendFormInteractionMessage for more details.
*/
_maybeSendFormInteractionMessageContinue(
form,
messageName,
{
targetField,
usernameField,
newPasswordField,
oldPasswordField,
confirmPasswordField,
isSubmission,
triggeredByFillingGenerated,
}
) {
let logMessagePrefix = isSubmission ? "form submission" : "field edit";
let dismissedPrompt = !isSubmission;
let doc = form.ownerDocument;
let win = doc.defaultView;
let detail = { messageSent: false };
try {
@ -1985,50 +2074,6 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild {
return;
}
let origin = LoginHelper.getLoginOrigin(doc.documentURI);
if (!origin) {
log(`(${logMessagePrefix} ignored -- invalid origin)`);
return;
}
let formActionOrigin = LoginHelper.getFormActionOrigin(form);
let recipes = LoginRecipesContent.getRecipes(this, origin, win);
// Get the appropriate fields from the form.
let {
usernameField,
newPasswordField,
oldPasswordField,
confirmPasswordField,
} = this._getFormFields(form, true, recipes, { ignoreConnect });
// It's possible the field triggering this message isn't one of those found by _getFormFields' heuristics
if (
passwordField &&
passwordField != newPasswordField &&
passwordField != oldPasswordField &&
passwordField != confirmPasswordField
) {
newPasswordField = passwordField;
}
let docState = this.stateForDocument(doc);
// Need at least 1 valid password field to do anything.
if (newPasswordField == null) {
if (isSubmission && usernameField) {
log(
"_onFormSubmit: username-only form. Record the username field but not sending prompt"
);
docState.mockUsernameOnlyField = {
name: usernameField.name,
value: usernameField.value,
};
}
return;
}
let fullyMungedPattern = /^\*+$|^•+$|^\.+$/;
// Check `isSubmission` to allow munged passwords in dismissed by default doorhangers (since
// they are initiated by the user) in case this matches their actual password.
@ -2043,6 +2088,7 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild {
// form doesn't have one. This means if there is a username field found in the current
// form, we don't compare it to the saved one, which might be a better choice in some cases.
// The reason we are not doing it now is because we haven't found a real world example.
let docState = this.stateForDocument(doc);
if (!usernameField) {
if (docState.mockUsernameOnlyField) {
usernameField = docState.mockUsernameOnlyField;
@ -2087,6 +2133,7 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild {
// if the password field is a three digit number. Also dismiss prompt if
// the password is a credit card number and the password field has attribute
// autocomplete="cc-number".
let dismissedPrompt = !isSubmission;
let newPasswordFieldValue = newPasswordField.value;
if (
(!dismissedPrompt &&
@ -2128,6 +2175,7 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild {
let { login: autoFilledLogin } =
docState.fillsByRootElement.get(form.rootElement) || {};
let browsingContextId = win.windowGlobalChild.browsingContext.id;
let formActionOrigin = LoginHelper.getFormActionOrigin(form);
detail = {
browsingContextId,

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

@ -295,13 +295,11 @@ class LoginManagerParent extends JSWindowActorParent {
}
case "PasswordManager:onFormSubmit": {
let browser = this.getRootBrowser();
let submitPromise = this.onFormSubmit(browser, context.origin, data);
if (gListenerForTests) {
submitPromise.then(() => {
gListenerForTests("FormSubmit", { origin: context.origin, data });
});
}
Services.obs.notifyObservers(
null,
"passwordmgr-form-submission-detected",
context.origin
);
break;
}
@ -325,6 +323,20 @@ class LoginManagerParent extends JSWindowActorParent {
break;
}
case "PasswordManager:ShowDoorhanger": {
let browser = this.getRootBrowser();
let submitPromise = this.showDoorhanger(browser, context.origin, data);
if (gListenerForTests) {
submitPromise.then(() => {
gListenerForTests("ShowDoorhanger", {
origin: context.origin,
data,
});
});
}
break;
}
case "PasswordManager:autoCompleteLogins": {
return this.doAutocompleteSearch(context.origin, data);
}
@ -826,7 +838,7 @@ class LoginManagerParent extends JSWindowActorParent {
return prompterSvc;
}
async onFormSubmit(
async showDoorhanger(
browser,
formOrigin,
{
@ -865,7 +877,7 @@ class LoginManagerParent extends JSWindowActorParent {
let browsingContext = BrowsingContext.get(browsingContextId);
let framePrincipalOrigin =
browsingContext.currentWindowGlobal.documentPrincipal.origin;
log("onFormSubmit, got framePrincipalOrigin: ", framePrincipalOrigin);
log("showDoorhanger, got framePrincipalOrigin: ", framePrincipalOrigin);
let formLogin = new LoginInfo(
formOrigin,

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

@ -121,6 +121,7 @@ skip-if =
os == 'mac' && webrender && bits == 64 # Bug 1683848
os == 'linux' && !debug && bits == 64 # Bug 1683848
win10_2004 && !fission # Bug 1723573
[browser_message_onFormSubmit.js]
[browser_openPasswordManager.js]
[browser_private_window.js]
support-files =

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

@ -79,7 +79,7 @@ add_task(async function test() {
}
);
let processedPromise = listenForTestNotification("FormSubmit");
let processedPromise = listenForTestNotification("ShowDoorhanger");
SpecialPowers.spawn(tab.linkedBrowser, [], () => {
content.document.getElementById("form-basic").submit();
});

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

@ -6,7 +6,7 @@
function getDataFromNextSubmitMessage() {
return new Promise(resolve => {
LoginManagerParent.setListenerForTests((msg, data) => {
if (msg == "FormSubmit") {
if (msg == "ShowDoorhanger") {
resolve(data);
}
});

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

@ -149,7 +149,7 @@ async function test_save_change({
}
);
let formSubmittedPromise = listenForTestNotification("FormSubmit");
let formSubmittedPromise = listenForTestNotification("ShowDoorhanger");
await SpecialPowers.spawn(browser, [], async function() {
let doc = this.content.document;
doc.getElementById("form-basic").submit();

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

@ -39,7 +39,7 @@ function listenForNotifications(count, expectedFormOrigin) {
LoginManagerParent.setListenerForTests((msg, data) => {
if (msg == "FormProcessed") {
notifications.push("FormProcessed: " + data.browsingContext.id);
} else if (msg == "FormSubmit") {
} else if (msg == "ShowDoorhanger") {
is(
data.origin,
expectedFormOrigin,

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

@ -33,7 +33,7 @@ add_task(async function test_doorhanger_dismissal_un() {
// password edited vs form submitted w. cc number as username
await clearMessageCache(browser);
let processedPromise = listenForTestNotification("FormSubmit");
let processedPromise = listenForTestNotification("ShowDoorhanger");
await SpecialPowers.spawn(browser, [], async () => {
content.document.getElementById("form-basic-submit").click();
});
@ -80,7 +80,7 @@ add_task(async function test_doorhanger_dismissal_pw() {
// password edited vs form submitted w. cc number as password
await clearMessageCache(browser);
let processedPromise = listenForTestNotification("FormSubmit");
let processedPromise = listenForTestNotification("ShowDoorhanger");
await SpecialPowers.spawn(browser, [], async () => {
content.document.getElementById("form-basic-submit").click();
});
@ -121,7 +121,7 @@ add_task(async function test_doorhanger_shown_on_un_with_invalid_ccnumber() {
// password edited vs form submitted w. cc number as password
await clearMessageCache(browser);
let processedPromise = listenForTestNotification("FormSubmit");
let processedPromise = listenForTestNotification("ShowDoorhanger");
await SpecialPowers.spawn(browser, [], async () => {
content.document.getElementById("form-basic-submit").click();
});
@ -178,7 +178,7 @@ add_task(async function test_doorhanger_dismissal_on_change() {
// password edited vs form submitted w. cc number as username
await clearMessageCache(browser);
let processedPromise = listenForTestNotification("FormSubmit");
let processedPromise = listenForTestNotification("ShowDoorhanger");
await SpecialPowers.spawn(browser, [], async () => {
content.document.getElementById("form-basic-submit").click();
});

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

@ -141,7 +141,7 @@ async function test_save_change(testData) {
// Submit the form.
info(`submit the password-only form`);
let formSubmittedPromise = listenForTestNotification("FormSubmit");
let formSubmittedPromise = listenForTestNotification("ShowDoorhanger");
await SpecialPowers.spawn(browser, [], async function() {
let doc = this.content.document;
doc.getElementById("form-basic-submit").click();

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

@ -129,7 +129,7 @@ add_task(async function test_edit_password() {
// Submit the form in the content page with the credentials from the test
// case. This will cause the doorhanger notification to be displayed.
info("Submitting the form");
let formSubmittedPromise = listenForTestNotification("FormSubmit");
let formSubmittedPromise = listenForTestNotification("ShowDoorhanger");
let promiseShown = BrowserTestUtils.waitForEvent(
PopupNotifications.panel,
"popupshown",

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

@ -118,7 +118,7 @@ async function test_save_change(testData) {
// Submit the form with the new credentials. This will cause the doorhanger
// notification to be displayed.
let formSubmittedPromise = listenForTestNotification("FormSubmit");
let formSubmittedPromise = listenForTestNotification("ShowDoorhanger");
await SpecialPowers.spawn(browser, [], async function() {
let doc = this.content.document;
doc.getElementById("form-basic").submit();

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

@ -316,7 +316,7 @@ async function test_submit_telemetry(tc) {
}
info("Submitting form");
let formSubmittedPromise = listenForTestNotification("FormSubmit");
let formSubmittedPromise = listenForTestNotification("ShowDoorhanger");
await SpecialPowers.spawn(browser, [], async function() {
let doc = this.content.document;
doc.getElementById("form-basic").submit();

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

@ -302,7 +302,7 @@ async function testDoorhangerToggles({
let { panel } = browser.ownerGlobal.PopupNotifications;
// submit the form and wait for the doorhanger
info("Submitting the form");
let submittedPromise = listenForTestNotification("FormSubmit");
let submittedPromise = listenForTestNotification("ShowDoorhanger");
let promiseShown = BrowserTestUtils.waitForEvent(panel, "popupshown");
await submitForm(browser, "/");
await submittedPromise;

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

@ -112,7 +112,7 @@ add_task(async function test_edit_username() {
// Submit the form in the content page with the credentials from the test
// case. This will cause the doorhanger notification to be displayed.
info("Submitting the form");
let formSubmittedPromise = listenForTestNotification("FormSubmit");
let formSubmittedPromise = listenForTestNotification("ShowDoorhanger");
let promiseShown = BrowserTestUtils.waitForEvent(
PopupNotifications.panel,
"popupshown",

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

@ -6,7 +6,7 @@
function getDataFromNextSubmitMessage() {
return new Promise(resolve => {
LoginManagerParent.setListenerForTests((msg, data) => {
if (msg == "FormSubmit") {
if (msg == "ShowDoorhanger") {
resolve(data);
}
});

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

@ -0,0 +1,82 @@
/**
* Test "passwordmgr-form-submission-detected" should be notified
* regardless of whehter the password saving is enabled.
*/
async function waitForFormSubmissionDetected() {
return new Promise(resolve => {
Services.obs.addObserver(function observer(subject, topic) {
Services.obs.removeObserver(
observer,
"passwordmgr-form-submission-detected"
);
resolve();
}, "passwordmgr-form-submission-detected");
});
}
add_task(async function test_login_save_disable() {
await SpecialPowers.pushPrefEnv({
set: [["signon.rememberSignons", false]],
});
await BrowserTestUtils.withNewTab(
{
gBrowser,
url:
"https://example.com/browser/toolkit/components/" +
"passwordmgr/test/browser/form_basic.html",
},
async function(browser) {
await SimpleTest.promiseFocus(browser.ownerGlobal);
await changeContentFormValues(browser, {
"#form-basic-username": "username",
"#form-basic-password": "password",
});
let promise = waitForFormSubmissionDetected();
await SpecialPowers.spawn(browser, [], async function() {
let doc = this.content.document;
doc.getElementById("form-basic").submit();
});
await promise;
ok(true, "Test completed");
}
);
});
add_task(async function test_login_save_enable() {
await SpecialPowers.pushPrefEnv({
set: [["signon.rememberSignons", true]],
});
await BrowserTestUtils.withNewTab(
{
gBrowser,
url:
"https://example.com/browser/toolkit/components/" +
"passwordmgr/test/browser/form_basic.html",
},
async function(browser) {
await SimpleTest.promiseFocus(browser.ownerGlobal);
await changeContentFormValues(browser, {
"#form-basic-username": "username",
"#form-basic-password": "password",
});
// When login saving is enabled, we should receive both FormSubmit
// event and "passwordmgr-form-submission-detected" event
let p1 = waitForFormSubmissionDetected();
let p2 = listenForTestNotification("ShowDoorhanger");
await SpecialPowers.spawn(browser, [], async function() {
let doc = this.content.document;
doc.getElementById("form-basic").submit();
});
await Promise.all([p1, p2]);
ok(true, "Test completed");
}
);
});

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

@ -234,7 +234,7 @@ addMessageListener("setMasterPassword", ({ enable }) => {
});
LoginManagerParent.setListenerForTests((msg, { origin, data }) => {
if (msg == "FormSubmit") {
if (msg == "ShowDoorhanger") {
sendAsyncMessage("formSubmissionProcessed", { origin, data });
} else if (msg == "PasswordEditedOrGenerated") {
sendAsyncMessage("passwordEditedOrGenerated", { origin, data });

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

@ -208,9 +208,9 @@ add_task(async function test_no_save_dialog_when_password_is_fully_munged() {
let formSubmitListener = SpecialPowers.spawn(win, [], function() {
return new Promise(resolve => {
this.content.windowRoot.addEventListener(
"PasswordManager:onFormSubmit",
"PasswordManager:ShowDoorhanger",
event => {
info(`PasswordManager:onFormSubmit called. Event: ${JSON.stringify(event)}`);
info(`PasswordManager:ShowDoorhanger called. Event: ${JSON.stringify(event)}`);
resolve(event.detail.messageSent);
}
);