Bug 917325 - Consider autocomplete fieldname when determining what is a username field. r=MattN
authorSam Foster <sfoster@mozilla.com>
Sat, 09 Mar 2019 02:58:13 +0000
changeset 521227 004ff60a5824
parent 521226 c4581e15f19f
child 521228 e5e798d73ac6
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,19 @@ 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
+skip-if = toolkit == 'android' # bug 1533965
 [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]