Bug 917325 - Consider autocomplete fieldname when determining what is a username field. r=MattN
☠☠ backed out by d4e6ec91e2c3 ☠ ☠
authorSam Foster <sfoster@mozilla.com>
Fri, 08 Mar 2019 18:26:44 +0000
changeset 521177 64e2378a756b
parent 521176 cce35becf719
child 521178 5d5db39b0497
push id10862
push userffxbld-merge
push dateMon, 11 Mar 2019 13:01:11 +0000
treeherdermozilla-beta@a2e7f5c935da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs917325
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 917325 - Consider autocomplete fieldname when determining what is a username field. r=MattN Differential Revision: https://phabricator.services.mozilla.com/D22557
toolkit/components/passwordmgr/LoginHelper.jsm
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/test/mochitest/mochitest.ini
toolkit/components/passwordmgr/test/mochitest/test_autofill_autocomplete_types.html
toolkit/components/passwordmgr/test/unit/test_isUsernameFieldType.js
toolkit/components/passwordmgr/test/unit/xpcshell.ini
--- a/toolkit/components/passwordmgr/LoginHelper.jsm
+++ b/toolkit/components/passwordmgr/LoginHelper.jsm
@@ -629,24 +629,32 @@ var LoginHelper = {
       // If the element isn't connected then it isn't visible to the user so
       // shouldn't be considered. It must have been connected in the past.
       return false;
     }
 
     let fieldType = (element.hasAttribute("type") ?
                      element.getAttribute("type").toLowerCase() :
                      element.type);
-    if (fieldType == "text" ||
-        fieldType == "email" ||
-        fieldType == "url" ||
-        fieldType == "tel" ||
-        fieldType == "number") {
-      return true;
+    if (!(fieldType == "text" ||
+          fieldType == "email" ||
+          fieldType == "url" ||
+          fieldType == "tel" ||
+          fieldType == "number")) {
+      return false;
     }
-    return false;
+
+    let acFieldName = element.getAutocompleteInfo().fieldName;
+    if (!(acFieldName == "username" ||
+          acFieldName == "off" ||
+          acFieldName == "on" ||
+          acFieldName == "")) {
+      return false;
+    }
+    return true;
   },
 
   /**
    * For each login, add the login to the password manager if a similar one
    * doesn't already exist. Merge it otherwise with the similar existing ones.
    *
    * @param {Object[]} loginDatas - For each login, the data that needs to be added.
    * @returns {nsILoginInfo[]} the newly added logins, filtered if no login was added.
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -825,16 +825,17 @@ var LoginManagerContent = {
       return [null, null, null];
     }
 
     if (!usernameField) {
       // Locate the username field in the form by searching backwards
       // from the first password field, assume the first text field is the
       // username. We might not find a username field if the user is
       // already logged in to the site.
+
       for (var i = pwFields[0].index - 1; i >= 0; i--) {
         var element = form.elements[i];
         if (!LoginHelper.isUsernameFieldType(element)) {
           continue;
         }
 
         if (fieldOverrideRecipe && fieldOverrideRecipe.notUsernameSelector &&
             element.matches(fieldOverrideRecipe.notUsernameSelector)) {
@@ -844,20 +845,20 @@ var LoginManagerContent = {
         usernameField = element;
         break;
       }
     }
 
     if (!usernameField) {
       log("(form -- no username field found)");
     } else {
-      log("Username field ", usernameField, "has name/value:",
-          usernameField.name, "/", usernameField.value);
+      let acFieldName = usernameField.getAutocompleteInfo().fieldName;
+      log("Username field ", usernameField, "has name/value/autocomplete:",
+          usernameField.name, "/", usernameField.value, "/", acFieldName);
     }
-
     // If we're not submitting a form (it's a page load), there are no
     // password field values for us to use for identifying fields. So,
     // just assume the first password field is the one to be filled in.
     if (!isSubmission || pwFields.length == 1) {
       var passwordField = pwFields[0].element;
       log("Password field", passwordField, "has name: ", passwordField.name);
       return [usernameField, passwordField, null];
     }
--- a/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
+++ b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
@@ -26,16 +26,18 @@ skip-if = toolkit == 'android' && !is_fe
 [test_autocomplete_highlight.html]
 scheme = https
 skip-if = toolkit == 'android' # autocomplete
 [test_autocomplete_https_upgrade.html]
 skip-if = toolkit == 'android' # autocomplete
 [test_autocomplete_sandboxed.html]
 scheme = https
 skip-if = toolkit == 'android' # autocomplete
+[test_autofill_autocomplete_types.html]
+scheme = https
 [test_autofill_highlight.html]
 scheme = https
 skip-if = toolkit == 'android' # Bug 1531185
 [test_autofill_https_upgrade.html]
 skip-if = toolkit == 'android' # Bug 1259768
 [test_autofill_sandboxed.html]
 scheme = https
 skip-if = toolkit == 'android'
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/mochitest/test_autofill_autocomplete_types.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test autofilling with autocomplete types (username, off, cc-type, etc.)</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>
+Test autofilling with autocomplete types (username, off, cc-type, etc.)
+
+<script>
+let readyPromise = registerRunTests();
+
+runInParent(function setup() {
+  const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+  let login1 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+               createInstance(Ci.nsILoginInfo);
+  login1.init("https://example.com", "https://autocomplete", null,
+              "testuser@example.com", "testpass1", "", "");
+  Services.logins.addLogin(login1);
+});
+</script>
+
+<p id="display"></p>
+<div id="content">
+
+  <form id="form0" action="https://autocomplete">
+    <input  type="text" name="uname">
+    <input  type="text" autocomplete="">
+    <input  type="password" name="pword">
+    <button type="submit">Submit</button>
+  </form>
+
+  <form id="form1" action="https://autocomplete">
+    <input  type="text" name="uname">
+    <input  type="text" autocomplete="username">
+    <input  type="password" name="pword">
+    <button type="submit">Submit</button>
+  </form>
+
+  <form id="form2" action="https://autocomplete">
+    <input  type="text" name="uname">
+    <input  type="text" autocomplete="off" name="acfield">
+    <input  type="password" name="pword">
+    <button type="submit">Submit</button>
+  </form>
+
+  <form id="form3" action="https://autocomplete">
+    <input  type="text" name="uname">
+    <input  type="text" autocomplete="on" name="acfield">
+    <input  type="password" name="pword">
+    <button type="submit">Submit</button>
+  </form>
+
+  <form id="form4" action="https://autocomplete">
+    <input  type="text" name="uname">
+    <input  type="text" autocomplete="nosuchtype" name="acfield">
+    <input  type="password" name="pword">
+    <button type="submit">Submit</button>
+  </form>
+
+  <form id="form5" action="https://autocomplete">
+    <input  type="text" name="uname">
+    <input  type="text" autocomplete="cc-number" name="acfield">
+    <input  type="password" name="pword">
+    <button type="submit">Submit</button>
+  </form>
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/*
+  Test for Login Manager: Skip over inappropriate autcomplete types when finding username field
+ */
+
+add_task(async function setup() {
+  ok(readyPromise, "check promise is available");
+  await readyPromise;
+});
+
+/* Tests for autofill of single-user forms with various autocomplete types */
+add_task(async function test_autofill_autocomplete_types() {
+  checkForm(0, null, "testuser@example.com", "testpass1");
+  checkForm(1, null, "testuser@example.com", "testpass1");
+  checkForm(2, null, "testuser@example.com", "testpass1");
+  checkForm(3, null, "testuser@example.com", "testpass1");
+  checkForm(4, null, "testuser@example.com", "testpass1");
+  checkForm(5, "testuser@example.com", null, "testpass1");
+});
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_isUsernameFieldType.js
@@ -0,0 +1,154 @@
+/*
+ * Test for LoginHelper.isUsernameFieldType
+ */
+
+"use strict";
+
+const autocompleteTypes = {
+  "": true,
+  "on": true,
+  "off": true,
+  "name": false,
+  "unrecognized-type": true,
+  "given-name": false,
+  "additional-name": false,
+  "family-name": false,
+  "nickname": false,
+  "username": true,
+  "new-password": false,
+  "current-password": false,
+  "organization-title": false,
+  "organization": false,
+  "street-address": false,
+  "address-line1": false,
+  "address-line2": false,
+  "address-line3": false,
+  "address-level4": false,
+  "address-level3": false,
+  "address-level2": false,
+  "address-level1": false,
+  "country": false,
+  "country-name": false,
+  "postal-code": false,
+  "cc-name": false,
+  "cc-given-name": false,
+  "cc-additional-name": false,
+  "cc-family-name": false,
+  "cc-number": false,
+  "cc-exp": false,
+  "cc-exp-month": false,
+  "cc-exp-year": false,
+  "cc-csc": false,
+  "cc-type": false,
+  "transaction-currency": false,
+  "transaction-amount": false,
+  "language": false,
+  "bday": false,
+  "bday-day": false,
+  "bday-month": false,
+  "bday-year": false,
+  "sex": false,
+  "url": false,
+  "photo": false,
+  "tel": false,
+  "tel-country-code": false,
+  "tel-national": false,
+  "tel-area-code": false,
+  "tel-local": false,
+  "tel-local-prefix": false,
+  "tel-local-suffix": false,
+  "tel-extension": false,
+  "email": false,
+  "impp": false,
+};
+
+const TESTCASES = [
+  {
+    description: "type=text",
+    document: `<input type="text">`,
+    expected: true,
+  },
+  {
+    description: "type=email, no autocomplete attribute",
+    document: `<input type="email">`,
+    expected: true,
+  },
+  {
+    description: "type=url, no autocomplete attribute",
+    document: `<input type="url">`,
+    expected: true,
+  },
+  {
+    description: "type=tel, no autocomplete attribute",
+    document: `<input type="tel">`,
+    expected: true,
+  },
+  {
+    description: "type=number, no autocomplete attribute",
+    document: `<input type="number">`,
+    expected: true,
+  },
+  {
+    description: "type=search, no autocomplete attribute",
+    document: `<input type="search">`,
+    expected: false,
+  },
+  {
+    description: "type=range, no autocomplete attribute",
+    document: `<input type="range">`,
+    expected: false,
+  },
+  {
+    description: "type=date, no autocomplete attribute",
+    document: `<input type="date">`,
+    expected: false,
+  },
+  {
+    description: "type=month, no autocomplete attribute",
+    document: `<input type="month">`,
+    expected: false,
+  },
+  {
+    description: "type=week, no autocomplete attribute",
+    document: `<input type="week">`,
+    expected: false,
+  },
+  {
+    description: "type=time, no autocomplete attribute",
+    document: `<input type="time">`,
+    expected: false,
+  },
+  {
+    description: "type=datetime, no autocomplete attribute",
+    document: `<input type="datetime">`,
+    expected: false,
+  },
+  {
+    description: "type=datetime-local, no autocomplete attribute",
+    document: `<input type="datetime-local">`,
+    expected: false,
+  },
+  {
+    description: "type=color, no autocomplete attribute",
+    document: `<input type="color">`,
+    expected: false,
+  },
+];
+
+for (let [name, expected] of Object.entries(autocompleteTypes)) {
+  TESTCASES.push({
+    description: `type=text autocomplete=${name}`,
+    document: `<input type="text" autocomplete="${name}">`,
+    expected,
+  });
+}
+
+TESTCASES.forEach(testcase => {
+  add_task(async function() {
+    info("Starting testcase: " + testcase.description);
+    let document = MockDocument.createTestDocument("http://localhost:8080/test/",
+                                                   testcase.document);
+    let input = document.querySelector("input");
+    Assert.equal(LoginHelper.isUsernameFieldType(input), testcase.expected, testcase.description);
+  });
+});
--- a/toolkit/components/passwordmgr/test/unit/xpcshell.ini
+++ b/toolkit/components/passwordmgr/test/unit/xpcshell.ini
@@ -20,16 +20,17 @@ run-if = buildapp == "browser"
 [test_dedupeLogins.js]
 [test_disabled_hosts.js]
 [test_doLoginsMatch.js]
 [test_getFormFields.js]
 [test_getPasswordFields.js]
 [test_getPasswordOrigin.js]
 [test_getUserNameAndPasswordFields.js]
 [test_isOriginMatching.js]
+[test_isUsernameFieldType.js]
 [test_legacy_empty_formSubmitURL.js]
 [test_legacy_validation.js]
 [test_login_autocomplete_result.js]
 skip-if = os == "android"
 [test_logins_change.js]
 [test_logins_decrypt_failure.js]
 skip-if = os == "android" # Bug 1171687: Needs fixing on Android
 [test_logins_metainfo.js]