Bug 1529946 - Don't prompt to save the same username and password combination in the same document. r=MattN
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Tue, 12 Mar 2019 23:44:33 +0000
changeset 521753 cbb4a76be84b
parent 521752 19e13e0edc40
child 521754 c49fff912f99
push id10867
push userdvarga@mozilla.com
push dateThu, 14 Mar 2019 15:20:45 +0000
treeherdermozilla-beta@abad13547875 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1529946
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1529946 - Don't prompt to save the same username and password combination in the same document. r=MattN Don't prompt to save the same username and password combination in the same document. Differential Revision: https://phabricator.services.mozilla.com/D22039
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/test/mochitest/mochitest.ini
toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html
toolkit/components/passwordmgr/test/mochitest/test_one_doorhanger_per_un_pw.html
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -551,16 +551,21 @@ var LoginManagerContent = {
   stateForDocument(document) {
     let loginFormState = this.loginFormStateByDocument.get(document);
     if (!loginFormState) {
       loginFormState = {
         /**
          * Keeps track of filled fields and values.
          */
         fillsByRootElement: new WeakMap(),
+        /**
+         * Keeps track of logins that were last submitted.
+         */
+        lastSubmittedValuesByRootElement: new WeakMap(),
+        loginFormRootElements: new WeakSet(),
       };
       this.loginFormStateByDocument.set(document, loginFormState);
     }
     return loginFormState;
   },
 
   /**
    * Compute whether there is an insecure login form on any frame of the current page, and
@@ -1068,16 +1073,35 @@ var LoginManagerContent = {
                          null;
     let mockPassword = { name: newPasswordField.name,
                          value: newPasswordField.value };
     let mockOldPassword = oldPasswordField ?
                             { name: oldPasswordField.name,
                               value: oldPasswordField.value } :
                             null;
 
+    let usernameValue = usernameField ? usernameField.value : null;
+    let formLikeRoot = FormLikeFactory.findRootForField(newPasswordField);
+    let state = this.stateForDocument(doc);
+    let lastSubmittedValues = state.lastSubmittedValuesByRootElement.get(formLikeRoot);
+    if (lastSubmittedValues) {
+      if (lastSubmittedValues.username == usernameValue &&
+          lastSubmittedValues.password == newPasswordField.value) {
+        log("(form submission ignored -- already submitted with the same username and password)");
+        return;
+      }
+    }
+
+    // Save the last submitted values so we don't prompt twice for the same values using
+    // different capture methods e.g. a form submit event and upon navigation.
+    state.lastSubmittedValuesByRootElement.set(formLikeRoot, {
+      username: usernameValue,
+      password: newPasswordField.value,
+    });
+
     // Make sure to pass the opener's top ID in case it was in a frame.
     let openerTopWindowID = null;
     if (win.opener) {
       openerTopWindowID = win.opener.top.windowUtils.outerWindowID;
     }
 
     let autoFilledLogin = this.stateForDocument(doc).fillsByRootElement.get(form.rootElement);
     messageManager.sendAsyncMessage("PasswordManager:onFormSubmit",
--- a/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
+++ b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
@@ -89,16 +89,18 @@ scheme = https
 skip-if = os != 'mac' # Tests desktop prompts and bug 1333264
 support-files =
   chrome_timeout.js
   subtst_master_pass.html
 [test_maxlength.html]
 [test_autocomplete_new_password.html]
 scheme = https
 skip-if = toolkit == 'android' # autocomplete
+[test_one_doorhanger_per_un_pw.html]
+scheme = https
 [test_onsubmit_value_change.html]
 [test_passwords_in_type_password.html]
 [test_prompt.html]
 skip-if = os == "linux" || toolkit == 'android' # Tests desktop prompts
 [test_prompt_async.html]
 skip-if = toolkit == 'android' # Tests desktop prompts
 support-files = subtst_prompt_async.html
 [test_prompt_http.html]
--- a/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_formless_submit.html
@@ -9,21 +9,29 @@
   <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <script type="application/javascript">
 const {LoginFormFactory} = SpecialPowers.Cu.import("resource://gre/modules/LoginFormFactory.jsm");
 const LMCBackstagePass = SpecialPowers.Cu.import("resource://gre/modules/LoginManagerContent.jsm");
 const { LoginManagerContent } = LMCBackstagePass;
 
+function loadFrame() {
+  return new Promise(resolve => {
+    document.getElementById("loginFrame").addEventListener("load", (evt) => {
+      if (evt.target.contentWindow.location.href.includes("blank.html")) {
+        resolve();
+      }
+    });
+  });
+}
+
 let loadPromise = new Promise(resolve => {
   document.addEventListener("DOMContentLoaded", () => {
-    document.getElementById("loginFrame").addEventListener("load", (evt) => {
-      resolve();
-    });
+    resolve(loadFrame());
   });
 });
 
 add_task(async function setup() {
   info("Waiting for page and frame loads");
   await loadPromise;
 
   await loadRecipes({
@@ -129,21 +137,23 @@ function getSubmitMessage() {
       info("got formSubmissionProcessed");
       PWMGR_COMMON_PARENT.removeMessageListener("formSubmissionProcessed", processed);
       resolve(...args);
     });
   });
 }
 
 add_task(async function test() {
+  let count = 0;
   let loginFrame = document.getElementById("loginFrame");
-  let frameDoc = loginFrame.contentWindow.document;
 
   for (let tc of TESTCASES) {
+    let frameDoc = loginFrame.contentWindow.document;
     info("Starting testcase: " + JSON.stringify(tc));
+
     // eslint-disable-next-line no-unsanitized/property
     frameDoc.documentElement.innerHTML = tc.document;
     let inputForFormLike = frameDoc.querySelectorAll("input")[tc.inputIndexForFormLike];
 
     let formLike = LoginFormFactory.createFromField(inputForFormLike);
 
     info("Calling _onFormSubmit with FormLike");
     let processedPromise = getSubmitMessage();
@@ -163,21 +173,26 @@ add_task(async function test() {
 
     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");
     }
+
+    loadPromise = loadFrame();
+    loginFrame.contentWindow.location =
+      "http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html?" + count++;
+    await loadPromise;
   }
 });
 
 </script>
 
 <p id="display"></p>
 
 <div id="content">
-  <iframe id="loginFrame" src="http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/blank.html"></iframe>
+  <iframe id="loginFrame" src="http://mochi.test:8888/tests/toolkit/components/passwordmgr/test/mochitest/blank.html"></iframe>
 </div>
 <pre id="test"></pre>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_one_doorhanger_per_un_pw.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Don't repeatedly prompt to save the same username and password
+    combination in the same document</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/AddTask.js"></script>
+  <script type="text/javascript" src="pwmgr_common.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+  let chromeScript = runChecksAfterCommonInit();
+
+  function getSubmitMessage() {
+    info("getSubmitMessage");
+    return new Promise((resolve, reject) => {
+      chromeScript.addMessageListener("formSubmissionProcessed", function processed(...args) {
+        info("got formSubmissionProcessed");
+        chromeScript.removeMessageListener("formSubmissionProcessed", processed);
+        resolve(...args);
+      });
+    });
+  }
+
+  SimpleTest.requestFlakyTimeout("Giving a chance for the unexpected popupshown to occur");
+</script>
+<p id="display"></p>
+
+<div id="content" style="display: none">
+   <form id="form1" onsubmit="return false;">
+    <input  type="text"     name="uname" id="ufield">
+    <input  type="password" name="pword" id="pfield">
+    <button type="submit" id="submitBtn">Submit</button>
+   </form>
+</div>
+
+<pre id="test"></pre>
+<script>
+  /** Test for Login Manager: Don't repeatedly prompt to save the
+      same username and password combination in the same document **/
+
+  add_task(async function test_prompt_does_not_reappear() {
+    let username = document.getElementById("ufield");
+    let password = document.getElementById("pfield");
+    let submitButton = document.getElementById("submitBtn");
+
+    username.value = "user";
+    password.value = "pass";
+
+    let processedPromise = getSubmitMessage();
+    let promptShownPromise = promisePromptShown("passwordmgr-prompt-save");
+    submitButton.click();
+    await processedPromise;
+    await promptShownPromise;
+
+    is(username.value, "user", "Checking for filled username");
+    is(password.value, "pass", "Checking for filled password");
+
+    let promptShown = false;
+    promptShownPromise = promisePromptShown("passwordmgr-prompt-save").then(value => {
+      promptShown = true;
+    });
+    submitButton.click();
+    await new Promise(resolve => setTimeout(resolve, 1000));
+    ok(!promptShown, "Prompt is not shown for the same login values a second time");
+  });
+</script>
+</body>
+</html>