Bug 1145754 - Allow per-site recipes to adjust the username/password field detection for capture. r=dolske, a=lizzard
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Wed, 27 May 2015 15:10:32 -0700
changeset 266139 ceac3b53900f
parent 266138 477a0035c253
child 266140 0bf9b7602fec
push id4764
push userryanvm@gmail.com
push date2015-05-29 14:57 +0000
treeherdermozilla-beta@ceac3b53900f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdolske, lizzard
bugs1145754
milestone39.0
Bug 1145754 - Allow per-site recipes to adjust the username/password field detection for capture. r=dolske, a=lizzard
browser/devtools/webconsole/test/browser_webconsole_netlogging.js
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/LoginManagerParent.jsm
toolkit/components/passwordmgr/test/mochitest.ini
toolkit/components/passwordmgr/test/subtst_notifications_2pw_1un_1text.html
toolkit/components/passwordmgr/test/test_notifications.html
--- a/browser/devtools/webconsole/test/browser_webconsole_netlogging.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_netlogging.js
@@ -178,21 +178,23 @@ function testFormSubmission()
       indexOf("Content-Length: 20"), -1, "Content-length is correct");
     isnot(lastRequest.request.postData.text.
       indexOf("name=foo+bar&age=144"), -1, "Form data is correct");
     is(lastRequest.response.content.text.indexOf("<!DOCTYPE HTML>"), 0,
       "Response body's beginning is okay");
 
     executeSoon(testNetworkPanel);
   };
+  ContentTask.spawn(gBrowser.selectedBrowser, { }, `function()
+  {
+    let form = content.document.querySelector("form");
+    form.submit();
+  }`);
+}
 
-  let form = content.document.querySelector("form");
-  ok(form, "we have the HTML form");
-  form.submit();
-}
 
 function testNetworkPanel()
 {
   // Open the NetworkPanel. The functionality of the NetworkPanel is tested
   // within separate test files.
   let networkPanel = hud.ui.openNetworkPanel(hud.ui.filterBox, lastRequest);
 
   networkPanel.panel.addEventListener("popupshown", function onPopupShown() {
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -529,21 +529,18 @@ var LoginManagerContent = {
   _isAutocompleteDisabled :  function (element) {
     if (element && element.hasAttribute("autocomplete") &&
         element.getAttribute("autocomplete").toLowerCase() == "off")
       return true;
 
     return false;
   },
 
-
-  /*
-   * _onFormSubmit
-   *
-   * Called by the our observer when notified of a form submission.
+  /**
+   * Called by our observer when notified of a form submission.
    * [Note that this happens before any DOM onsubmit handlers are invoked.]
    * Looks for a password change in the submitted form, so we can update
    * our stored password.
    */
   _onFormSubmit : function (form) {
     var doc = form.ownerDocument;
     var win = doc.defaultView;
 
@@ -567,21 +564,27 @@ var LoginManagerContent = {
     // Somewhat gross hack - we don't want to show the "remember password"
     // notification on about:accounts for Firefox.
     let topWin = win.top;
     if (/^about:accounts($|\?)/i.test(topWin.document.documentURI)) {
       log("(form submission ignored -- about:accounts)");
       return;
     }
 
-    var formSubmitURL = LoginUtils._getActionOrigin(form)
+    let formSubmitURL = LoginUtils._getActionOrigin(form);
+    let messageManager = messageManagerFromWindow(win);
+
+    let recipesArray = messageManager.sendSyncMessage("RemoteLogins:findRecipes", {
+      formOrigin: hostname,
+    })[0];
+    let recipes = new Set(recipesArray);
 
     // Get the appropriate fields from the form.
     var [usernameField, newPasswordField, oldPasswordField] =
-        this._getFormFields(form, true);
+          this._getFormFields(form, true, recipes);
 
     // Need at least 1 valid password field to do anything.
     if (newPasswordField == null)
       return;
 
     // Check for autocomplete=off attribute. We don't use it to prevent
     // autofilling (for existing logins), but won't save logins when it's
     // present and the storeWhenAutocompleteOff pref is false.
@@ -605,17 +608,16 @@ var LoginManagerContent = {
     let mockOldPassword = oldPasswordField ?
                             { name: oldPasswordField.name,
                               value: oldPasswordField.value } :
                             null;
 
     // Make sure to pass the opener's top in case it was in a frame.
     let opener = win.opener ? win.opener.top : null;
 
-    let messageManager = messageManagerFromWindow(win);
     messageManager.sendAsyncMessage("RemoteLogins:onFormSubmit",
                                     { hostname: hostname,
                                       formSubmitURL: formSubmitURL,
                                       usernameField: mockUsername,
                                       newPasswordField: mockPassword,
                                       oldPasswordField: mockOldPassword },
                                     { openerWin: opener });
   },
--- a/toolkit/components/passwordmgr/LoginManagerParent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm
@@ -158,30 +158,42 @@ PasswordsMeasurement2.prototype = Object
 function prefChanged() {
   gDebug = Services.prefs.getBoolPref("signon.debug");
 }
 
 Services.prefs.addObserver("signon.debug", prefChanged, false);
 prefChanged();
 
 var LoginManagerParent = {
+  /**
+   * Reference to the default LoginRecipesParent (instead of the initialization promise) for
+   * synchronous access. This is a temporary hack and new consumers should yield on
+   * recipeParentPromise instead.
+   *
+   * @type LoginRecipesParent
+   * @deprecated
+   */
+   _recipeManager: null,
+
   init: function() {
     let mm = Cc["@mozilla.org/globalmessagemanager;1"]
                .getService(Ci.nsIMessageListenerManager);
     mm.addMessageListener("RemoteLogins:findLogins", this);
+    mm.addMessageListener("RemoteLogins:findRecipes", this);
     mm.addMessageListener("RemoteLogins:onFormSubmit", this);
     mm.addMessageListener("RemoteLogins:autoCompleteLogins", this);
     mm.addMessageListener("LoginStats:LoginEncountered", this);
     mm.addMessageListener("LoginStats:LoginFillSuccessful", this);
     Services.obs.addObserver(this, "LoginStats:NewSavedPassword", false);
 
     XPCOMUtils.defineLazyGetter(this, "recipeParentPromise", () => {
       const { LoginRecipesParent } = Cu.import("resource://gre/modules/LoginRecipes.jsm", {});
-      let parent = new LoginRecipesParent();
-      return parent.initializationPromise;
+      this._recipeManager = new LoginRecipesParent();
+      return this._recipeManager.initializationPromise;
+
     });
 
   },
 
   observe: function (aSubject, aTopic, aData) {
 #ifndef ANDROID
 #ifdef MOZ_SERVICES_HEALTHREPORT
     if (aTopic == "LoginStats:NewSavedPassword") {
@@ -200,16 +212,21 @@ var LoginManagerParent = {
         this.sendLoginDataToChild(data.options.showMasterPassword,
                                   data.formOrigin,
                                   data.actionOrigin,
                                   data.requestId,
                                   msg.target.messageManager);
         break;
       }
 
+      case "RemoteLogins:findRecipes": {
+        let formHost = (new URL(data.formOrigin)).host;
+        return [...this._recipeManager.getRecipesForHost(formHost)];
+      }
+
       case "RemoteLogins:onFormSubmit": {
         // TODO Verify msg.target's principals against the formOrigin?
         this.onFormSubmit(data.hostname,
                           data.formSubmitURL,
                           data.usernameField,
                           data.newPasswordField,
                           data.oldPasswordField,
                           msg.objects.openerWin,
--- a/toolkit/components/passwordmgr/test/mochitest.ini
+++ b/toolkit/components/passwordmgr/test/mochitest.ini
@@ -9,16 +9,17 @@ support-files =
   pwmgr_common.js
   subtst_master_pass.html
   subtst_notifications_1.html
   subtst_notifications_10.html
   subtst_notifications_11.html
   subtst_notifications_11_popup.html
   subtst_notifications_2.html
   subtst_notifications_2pw_0un.html
+  subtst_notifications_2pw_1un_1text.html
   subtst_notifications_3.html
   subtst_notifications_4.html
   subtst_notifications_5.html
   subtst_notifications_6.html
   subtst_notifications_7.html
   subtst_notifications_8.html
   subtst_notifications_9.html
   subtst_privbrowsing_1.html
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/subtst_notifications_2pw_1un_1text.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Subtest for Login Manager notifications with 2 password fields and 1 username field and one other text field before the first password field</title>
+</head>
+<body>
+<h2>1 username field followed by a text field followed by 2 username fields</h2>
+<form id="form" action="formsubmit.sjs">
+  <input id="user"  name="user"  value="staticpw">
+  <input id="city"  name="city"  value="city">
+  <input id="pass"  name="pass"  type="password">
+  <input id="pin"   name="pin"   type="password" value="static-pin">
+  <button type="submit">Submit</button>
+</form>
+
+<script>
+function submitForm() {
+  userField.value = "notifyu1";
+  passField.value = "notifyp1";
+  form.submit();
+}
+
+window.onload = submitForm;
+var form      = document.getElementById("form");
+var userField = document.getElementById("user");
+var passField = document.getElementById("pass");
+
+</script>
+</body>
+</html>
--- a/toolkit/components/passwordmgr/test/test_notifications.html
+++ b/toolkit/components/passwordmgr/test/test_notifications.html
@@ -1,27 +1,28 @@
 <!DOCTYPE HTML>
 <html>
 <head>
+  <meta charset="utf-8">
   <title>Test for Login Manager</title>
-  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>  
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
   <script type="text/javascript" src="notification_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 Login Manager test: notifications
 <p id="display"></p>
 
 <div id="content" style="display: none">
   <iframe id="iframe"></iframe>
 </div>
 
 <pre id="test">
-<script class="testbody" type="text/javascript">
+<script class="testbody" type="application/javascript;version=1.8">
 
 /** Test for Login Manager: notifications. **/
 
 // Set testpath to the directory where we live. Used to load tests from
 // alternate Mochitest servers (different hostnames, same content).
 var testpath = document.location.pathname + "/../";
 
 var subtests = [
@@ -46,16 +47,18 @@ var subtests = [
                    "subtst_notifications_9.html", // 19
                    "subtst_notifications_10.html",  // 20
                    "http://test1.example.org:80" + testpath + "subtst_notifications_1.html", // 21
                    "http://test1.example.org:80" + testpath + "subtst_notifications_7.html", // 22
                    "http://test1.example.org:80" + testpath + "subtst_notifications_6.html", // 23
                    "subtst_notifications_2pw_0un.html",  // 24
                    "subtst_notifications_2pw_0un.html",  // 25
                    "subtst_notifications_2pw_0un.html",  // 26
+                   "http://example.org" + testpath + "subtst_notifications_2pw_1un_1text.html", // 27
+                   "http://example.org" + testpath + "subtst_notifications_2pw_1un_1text.html", // 28
                ];
 
 
 var ignoreLoad = false;
 function handleLoad(aEvent) {
     // ignore every other load event ... We get one for loading the subtest (which
     // we want to ignore), and another when the subtest's form submits itself
     // (which we want to handle, to start the next test).
@@ -416,16 +419,54 @@ function checkTest() {
         is(gotPass, "notifyp2", "Checking submitted password");
         popup = getPopup(popupNotifications, "password-change");
         ok(popup, "got notification popup");
         popup.remove();
         // remove that login
         pwmgr.removeLogin(login2B);
         break;
 
+      case 27: {
+        // Check that we capture the proper fields when a field recipe is in use.
+        is(gotUser, "notifyu1", "Checking submitted username");
+        is(gotPass, "notifyp1", "Checking submitted password");
+        popup = getPopup(popupNotifications, "password-save");
+        ok(popup, "got notification popup");
+
+        // Sanity check, no logins should exist yet.
+        let logins = pwmgr.getAllLogins();
+        is(logins.length, 0, "Should not have any logins yet");
+
+        clickPopupButton(popup, kRememberButton);
+        break;
+      }
+
+      case 28: {
+        // Same subtest, make sure we didn't prompt for an existing login.
+        is(gotUser, "notifyu1", "Checking submitted username");
+        is(gotPass, "notifyp1", "Checking submitted password");
+        popup = getPopup(popupNotifications, "password-save");
+        ok(!popup, "checking for no notification popup");
+
+        // Check to make sure we updated the timestamps and use count on the
+        // existing login that was submitted for this form.
+        let logins = pwmgr.getAllLogins();
+        is(logins.length, 1, "Should only have 1 login");
+        ok(SpecialPowers.call_Instanceof(logins[0], Ci.nsILoginMetaInfo), "metainfo QI");
+        is(logins[0].username, "notifyu1", "check .username for existing login submission");
+        is(logins[0].password, "notifyp1", "check .password for existing login submission");
+        is(logins[0].timesUsed, 2, "check .timesUsed for existing login submission");
+        ok(logins[0].timeLastUsed > logins[0].timeCreated, "timeLastUsed bumped");
+        ok(logins[0].timeCreated == logins[0].timePasswordChanged, "timeChanged not updated");
+
+        // remove the added login
+        pwmgr.removeAllLogins();
+        break;
+      }
+
       default:
         ok(false, "Unexpected call to checkTest for test #" + testNum);
 
     }
 
     // TODO:
     // * existing login test, form has different password --> change password, no save prompt
 }
@@ -453,25 +494,40 @@ var login1 = new nsLoginInfo("http://moc
                              "notifyu1", "notifyp1", "user", "pass");
 var login2 = new nsLoginInfo("http://mochi.test:8888", "http://mochi.test:8888", null,
                              "", "notifyp1", "", "pass");
 var login1B = new nsLoginInfo("http://mochi.test:8888", "http://mochi.test:8888", null,
                               "notifyu1B", "notifyp1B", "user", "pass");
 var login2B = new nsLoginInfo("http://mochi.test:8888", "http://mochi.test:8888", null,
                               "", "notifyp1B", "", "pass");
 
+var parentScriptURL = SimpleTest.getTestFileURL("pwmgr_common.js");
+var mm = SpecialPowers.loadChromeScript(parentScriptURL);
+
 var iframe = document.getElementById("iframe");
 iframe.onload = handleLoad;
 
 // popupNotifications (not *popup*) is a constant, per-tab container. So, we
 // only need to fetch it once.
 var popupNotifications = getPopupNotifications(window.top);
 ok(popupNotifications, "Got popupNotifications");
 
 var testNum = 1;
-ok(true, "Starting test #" + testNum);
-iframe.src = subtests[testNum-1];
+
+// Load recipes for this test.
+mm.sendAsyncMessage("loadRecipes", {
+  siteRecipes: [{
+    hosts: ["example.org"],
+    usernameSelector: "#user",
+    passwordSelector: "#pass",
+  }],
+});
+
+mm.addMessageListener("loadedRecipes", function loadedRecipes() {
+  ok(true, "Starting test #" + testNum);
+  iframe.src = subtests[testNum-1];
+})
 
 SimpleTest.waitForExplicitFinish();
 </script>
 </pre>
 </body>
 </html>