--- a/browser/extensions/formautofill/FormAutofillParent.jsm
+++ b/browser/extensions/formautofill/FormAutofillParent.jsm
@@ -3,17 +3,17 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* Implements a service used to access storage and communicate with content.
*
* A "fields" array is used to communicate with FormAutofillContent. Each item
* represents a single input field in the content page as well as its
* @autocomplete properties. The schema is as below. Please refer to
- * FormAutofillContent.jsm for more details.
+ * FormAutofillContent.js for more details.
*
* [
* {
* section,
* addressType,
* contactType,
* fieldName,
* value,
--- a/browser/extensions/formautofill/content/FormAutofillContent.js
+++ b/browser/extensions/formautofill/content/FormAutofillContent.js
@@ -6,17 +6,55 @@
* Form Autofill frame script.
*/
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr, manager: Cm} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-let {FormAutoCompleteResult} = Cu.import("resource://gre/modules/nsFormAutoCompleteResult.jsm", {});
+Cu.import("resource://gre/modules/nsFormAutoCompleteResult.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormLikeFactory",
+ "resource://gre/modules/FormLikeFactory.jsm");
+
+const formFillController = Cc["@mozilla.org/satchel/form-fill-controller;1"]
+ .getService(Ci.nsIFormFillController);
+
+const AUTOFILL_FIELDS_THRESHOLD = 3;
+
+/**
+ * Returns the autocomplete information of fields according to heuristics.
+ */
+let FormAutofillHeuristics = {
+ VALID_FIELDS: [
+ "organization",
+ "street-address",
+ "address-level2",
+ "address-level1",
+ "postal-code",
+ "country",
+ "tel",
+ "email",
+ ],
+
+ getInfo(element) {
+ if (!(element instanceof Ci.nsIDOMHTMLInputElement)) {
+ return null;
+ }
+
+ let info = element.getAutocompleteInfo();
+ if (!info || !info.fieldName ||
+ !this.VALID_FIELDS.includes(info.fieldName)) {
+ return null;
+ }
+
+ return info;
+ },
+};
/**
* Handles profile autofill for a DOM Form element.
* @param {HTMLFormElement} form Form that need to be auto filled
*/
function FormAutofillHandler(form) {
this.form = form;
this.fieldDetails = [];
@@ -50,24 +88,19 @@ FormAutofillHandler.prototype = {
* @returns {Array<Object>} Serializable data structure that can be sent to the user
* interface, or null if the operation failed because the constraints
* on the allowed fields were not honored.
*/
collectFormFields() {
let autofillData = [];
for (let element of this.form.elements) {
- // Query the interface and exclude elements that cannot be autocompleted.
- if (!(element instanceof Ci.nsIDOMHTMLInputElement)) {
- continue;
- }
-
// Exclude elements to which no autocomplete field has been assigned.
- let info = element.getAutocompleteInfo();
- if (!info.fieldName || ["on", "off"].includes(info.fieldName)) {
+ let info = FormAutofillHeuristics.getInfo(element);
+ if (!info) {
continue;
}
// Store the association between the field metadata and the element.
if (this.fieldDetails.some(f => f.section == info.section &&
f.addressType == info.addressType &&
f.contactType == info.contactType &&
f.fieldName == info.fieldName)) {
@@ -115,18 +148,19 @@ FormAutofillHandler.prototype = {
// Get the field details, if it was processed by the user interface.
let fieldDetail = this.fieldDetails[field.index];
// Avoid the invalid value set
if (!fieldDetail || !field.value) {
continue;
}
- let info = fieldDetail.element.getAutocompleteInfo();
- if (field.section != info.section ||
+ let info = FormAutofillHeuristics.getInfo(fieldDetail.element);
+ if (!info ||
+ field.section != info.section ||
field.addressType != info.addressType ||
field.contactType != info.contactType ||
field.fieldName != info.fieldName) {
Cu.reportError("Autocomplete tokens mismatched");
continue;
}
fieldDetail.element.setUserInput(field.value);
@@ -208,19 +242,16 @@ AutofillProfileAutoCompleteSearch.protot
* Stops an asynchronous search that is in progress
*/
stopSearch() {
},
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AutofillProfileAutoCompleteSearch]);
-// TODO: Remove this lint option once we apply ProfileAutocomplete while
-// content script initialization.
-/* eslint no-unused-vars: [2, {"vars": "local"}] */
let ProfileAutocomplete = {
_registered: false,
_factory: null,
ensureRegistered() {
if (this._registered) {
return;
}
@@ -235,8 +266,68 @@ let ProfileAutocomplete = {
return;
}
this._factory.unregister();
this._factory = null;
this._registered = false;
},
};
+
+/**
+ * Handles content's interactions.
+ *
+ * NOTE: Declares it by "var" to make it accessible in unit tests.
+ */
+var FormAutofillContent = {
+ init() {
+ ProfileAutocomplete.ensureRegistered();
+
+ addEventListener("DOMContentLoaded", this);
+ },
+
+ handleEvent(evt) {
+ if (!evt.isTrusted) {
+ return;
+ }
+
+ switch (evt.type) {
+ case "DOMContentLoaded":
+ let doc = evt.target;
+ if (!(doc instanceof Ci.nsIDOMHTMLDocument)) {
+ return;
+ }
+ this._identifyAutofillFields(doc);
+ break;
+ }
+ },
+
+ _identifyAutofillFields(doc) {
+ let forms = [];
+
+ // Collects root forms from inputs.
+ for (let field of doc.getElementsByTagName("input")) {
+ let formLike = FormLikeFactory.createFromField(field);
+ if (!forms.some(form => form.rootElement === formLike.rootElement)) {
+ forms.push(formLike);
+ }
+ }
+
+ // Collects the fields that can be autofilled from each form and marks them
+ // as autofill fields if the amount is above the threshold.
+ forms.forEach(form => {
+ let formHandler = new FormAutofillHandler(form);
+ formHandler.collectFormFields();
+ if (formHandler.fieldDetails.length < AUTOFILL_FIELDS_THRESHOLD) {
+ return;
+ }
+
+ formHandler.fieldDetails.forEach(
+ detail => this._markAsAutofillField(detail.element));
+ });
+ },
+
+ _markAsAutofillField(field) {
+ formFillController.markAsAutofillField(field);
+ },
+};
+
+FormAutofillContent.init();
--- a/browser/extensions/formautofill/test/unit/head.js
+++ b/browser/extensions/formautofill/test/unit/head.js
@@ -1,13 +1,13 @@
/**
* Provides infrastructure for automated formautofill components tests.
*/
-/* exported getTempFile */
+/* exported loadFormAutofillContent, getTempFile */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
@@ -33,17 +33,19 @@ Components.manager.addBootstrappedManife
// While the previous test file should have deleted all the temporary files it
// used, on Windows these might still be pending deletion on the physical file
// system. Thus, start from a new base number every time, to make a collision
// with a file that is still pending deletion highly unlikely.
let gFileCounter = Math.floor(Math.random() * 1000000);
function loadFormAutofillContent() {
- let facGlobal = {};
+ let facGlobal = {
+ addEventListener: function() {},
+ };
let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Ci.mozIJSSubScriptLoader);
loader.loadSubScriptWithOptions("chrome://formautofill/content/FormAutofillContent.js", {
target: facGlobal,
});
return facGlobal;
}
@@ -79,16 +81,15 @@ function getTempFile(leafName) {
});
return file;
}
add_task(function* test_common_initialize() {
Services.prefs.setBoolPref("browser.formautofill.enabled", true);
Services.prefs.setBoolPref("dom.forms.autocomplete.experimental", true);
- loadFormAutofillContent();
// Clean up after every test.
do_register_cleanup(() => {
Services.prefs.clearUserPref("browser.formautofill.enabled");
Services.prefs.clearUserPref("dom.forms.autocomplete.experimental");
});
});
--- a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
@@ -10,18 +10,16 @@ const TESTCASES = [
{
description: "Form without autocomplete property",
document: `<form><input id="given-name"><input id="family-name">
<input id="street-addr"><input id="city"><input id="country">
<input id='email'><input id="tel"></form>`,
fieldDetails: [],
profileData: [],
expectedResult: {
- "given-name": "",
- "family-name": "",
"street-addr": "",
"city": "",
"country": "",
"email": "",
"tel": "",
},
},
{
@@ -47,18 +45,16 @@ const TESTCASES = [
{"section": "", "addressType": "", "fieldName": "family-name", "contactType": "", "index": 1, "value": "bar"},
{"section": "", "addressType": "", "fieldName": "street-address", "contactType": "", "index": 2, "value": "2 Harrison St"},
{"section": "", "addressType": "", "fieldName": "address-level2", "contactType": "", "index": 3, "value": "San Francisco"},
{"section": "", "addressType": "", "fieldName": "country", "contactType": "", "index": 4, "value": "US"},
{"section": "", "addressType": "", "fieldName": "email", "contactType": "", "index": 5, "value": "foo@mozilla.com"},
{"section": "", "addressType": "", "fieldName": "tel", "contactType": "", "index": 6, "value": "1234567"},
],
expectedResult: {
- "given-name": "foo",
- "family-name": "bar",
"street-addr": "2 Harrison St",
"city": "San Francisco",
"country": "US",
"email": "foo@mozilla.com",
"tel": "1234567",
},
},
{
@@ -84,18 +80,16 @@ const TESTCASES = [
{"section": "", "addressType": "shipping", "fieldName": "family-name", "contactType": "", "index": 1, "value": "bar"},
{"section": "", "addressType": "shipping", "fieldName": "street-address", "contactType": "", "index": 2, "value": "2 Harrison St"},
{"section": "", "addressType": "shipping", "fieldName": "address-level2", "contactType": "", "index": 3, "value": "San Francisco"},
{"section": "", "addressType": "shipping", "fieldName": "country", "contactType": "", "index": 4, "value": "US"},
{"section": "", "addressType": "shipping", "fieldName": "email", "contactType": "", "index": 5, "value": "foo@mozilla.com"},
{"section": "", "addressType": "shipping", "fieldName": "tel", "contactType": "", "index": 6, "value": "1234567"},
],
expectedResult: {
- "given-name": "foo",
- "family-name": "bar",
"street-addr": "2 Harrison St",
"city": "San Francisco",
"country": "US",
"email": "foo@mozilla.com",
"tel": "1234567",
},
},
{
@@ -121,18 +115,16 @@ const TESTCASES = [
{"section": "", "addressType": "shipping", "fieldName": "family-name", "contactType": "", "index": 1, "value": "bar"},
{"section": "", "addressType": "shipping", "fieldName": "street-address", "contactType": "", "index": 2, "value": "2 Harrison St"},
{"section": "", "addressType": "shipping", "fieldName": "address-level2", "contactType": "", "index": 3, "value": "San Francisco"},
{"section": "", "addressType": "shipping", "fieldName": "country", "contactType": "", "index": 4, "value": "US"},
{"section": "", "addressType": "shipping", "fieldName": "email", "contactType": "", "index": 5},
{"section": "", "addressType": "shipping", "fieldName": "tel", "contactType": "", "index": 6},
],
expectedResult: {
- "given-name": "foo",
- "family-name": "bar",
"street-addr": "2 Harrison St",
"city": "San Francisco",
"country": "US",
"email": "",
"tel": "",
},
},
{
@@ -158,18 +150,16 @@ const TESTCASES = [
{"section": "", "addressType": "shipping", "fieldName": "family-name", "contactType": "", "index": 1, "value": "bar"},
{"section": "", "addressType": "shipping", "fieldName": "street-address", "contactType": "", "index": 2, "value": "2 Harrison St"},
{"section": "", "addressType": "shipping", "fieldName": "address-level2", "contactType": "", "index": 3, "value": "San Francisco"},
{"section": "", "addressType": "shipping", "fieldName": "country", "contactType": "", "index": 4, "value": "US"},
{"section": "", "addressType": "shipping", "fieldName": "email", "contactType": "", "index": 5, "value": "foo@mozilla.com"},
{"section": "", "addressType": "shipping", "fieldName": "tel", "contactType": "", "index": 6, "value": "1234567"},
],
expectedResult: {
- "given-name": "foo",
- "family-name": "bar",
"street-addr": "",
"city": "",
"country": "",
"email": "foo@mozilla.com",
"tel": "1234567",
},
},
];
--- a/browser/extensions/formautofill/test/unit/test_collectFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_collectFormFields.js
@@ -20,27 +20,23 @@ const TESTCASES = [
document: `<form><input id="given-name" autocomplete="given-name">
<input id="family-name" autocomplete="family-name">
<input id="street-addr" autocomplete="street-address">
<input id="city" autocomplete="address-level2">
<input id="country" autocomplete="country">
<input id="email" autocomplete="email">
<input id="tel" autocomplete="tel"></form>`,
returnedFormat: [
- {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name", "index": 0},
- {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name", "index": 1},
- {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address", "index": 2},
- {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2", "index": 3},
- {"section": "", "addressType": "", "contactType": "", "fieldName": "country", "index": 4},
- {"section": "", "addressType": "", "contactType": "", "fieldName": "email", "index": 5},
- {"section": "", "addressType": "", "contactType": "", "fieldName": "tel", "index": 6},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address", "index": 0},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2", "index": 1},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "country", "index": 2},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "email", "index": 3},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "tel", "index": 4},
],
fieldDetails: [
- {"section": "", "addressType": "", "contactType": "", "fieldName": "given-name", "element": {}},
- {"section": "", "addressType": "", "contactType": "", "fieldName": "family-name", "element": {}},
{"section": "", "addressType": "", "contactType": "", "fieldName": "street-address", "element": {}},
{"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2", "element": {}},
{"section": "", "addressType": "", "contactType": "", "fieldName": "country", "element": {}},
{"section": "", "addressType": "", "contactType": "", "fieldName": "email", "element": {}},
{"section": "", "addressType": "", "contactType": "", "fieldName": "tel", "element": {}},
],
},
{
@@ -48,27 +44,23 @@ const TESTCASES = [
document: `<form><input id="given-name" autocomplete="shipping given-name">
<input id="family-name" autocomplete="shipping family-name">
<input id="street-addr" autocomplete="shipping street-address">
<input id="city" autocomplete="shipping address-level2">
<input id="country" autocomplete="shipping country">
<input id='email' autocomplete="shipping email">
<input id="tel" autocomplete="shipping tel"></form>`,
returnedFormat: [
- {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name", "index": 0},
- {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name", "index": 1},
- {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address", "index": 2},
- {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2", "index": 3},
- {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "index": 4},
- {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email", "index": 5},
- {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel", "index": 6},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address", "index": 0},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2", "index": 1},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "index": 2},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email", "index": 3},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel", "index": 4},
],
fieldDetails: [
- {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name", "element": {}},
- {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name", "element": {}},
{"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address", "element": {}},
{"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2", "element": {}},
{"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "element": {}},
{"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email", "element": {}},
{"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel", "element": {}},
],
},
{
@@ -76,27 +68,23 @@ const TESTCASES = [
document: `<form><input id="given-name" autocomplete="shipping given-name">
<input id="family-name" autocomplete="shipping family-name">
<input id="street-addr" autocomplete="shipping street-address">
<input id="city" autocomplete="shipping address-level2">
<input id="country" autocomplete="shipping country">
<input id='email' autocomplete="shipping email">
<input id="tel" autocomplete="shipping tel"></form>`,
returnedFormat: [
- {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name", "index": 0},
- {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name", "index": 1},
- {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address", "index": 2},
- {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2", "index": 3},
- {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "index": 4},
- {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email", "index": 5},
- {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel", "index": 6},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address", "index": 0},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2", "index": 1},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "index": 2},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email", "index": 3},
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel", "index": 4},
],
fieldDetails: [
- {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "given-name", "element": {}},
- {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "family-name", "element": {}},
{"section": "", "addressType": "shipping", "contactType": "", "fieldName": "street-address", "element": {}},
{"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level2", "element": {}},
{"section": "", "addressType": "shipping", "contactType": "", "fieldName": "country", "element": {}},
{"section": "", "addressType": "shipping", "contactType": "", "fieldName": "email", "element": {}},
{"section": "", "addressType": "shipping", "contactType": "", "fieldName": "tel", "element": {}},
],
},
];
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_markAsAutofillField.js
@@ -0,0 +1,72 @@
+"use strict";
+
+let {FormAutofillContent} = loadFormAutofillContent();
+
+const TESTCASES = [
+ {
+ description: "Form containing 5 fields with autocomplete attribute.",
+ document: `<form>
+ <input id="street-addr" autocomplete="street-address">
+ <input id="city" autocomplete="address-level2">
+ <input id="country" autocomplete="country">
+ <input id="email" autocomplete="email">
+ <input id="tel" autocomplete="tel">
+ <input id="without-autocomplete-1">
+ <input id="without-autocomplete-2">
+ </form>`,
+ expectedResult: [
+ "street-addr",
+ "city",
+ "country",
+ "email",
+ "tel",
+ ],
+ },
+ {
+ description: "Form containing only 2 fields with autocomplete attribute.",
+ document: `<form>
+ <input id="street-addr" autocomplete="street-address">
+ <input id="city" autocomplete="address-level2">
+ <input id="without-autocomplete-1">
+ <input id="without-autocomplete-2">
+ </form>`,
+ expectedResult: [],
+ },
+ {
+ description: "Fields without form element.",
+ document: `<input id="street-addr" autocomplete="street-address">
+ <input id="city" autocomplete="address-level2">
+ <input id="country" autocomplete="country">
+ <input id="email" autocomplete="email">
+ <input id="tel" autocomplete="tel">
+ <input id="without-autocomplete-1">
+ <input id="without-autocomplete-2">`,
+ expectedResult: [
+ "street-addr",
+ "city",
+ "country",
+ "email",
+ "tel",
+ ],
+ },
+];
+
+let markedFieldId = [];
+FormAutofillContent._markAsAutofillField = function(field) {
+ markedFieldId.push(field.id);
+};
+
+TESTCASES.forEach(testcase => {
+ add_task(function* () {
+ do_print("Starting testcase: " + testcase.description);
+
+ markedFieldId = [];
+
+ let doc = MockDocument.createTestDocument(
+ "http://localhost:8080/test/", testcase.document);
+ FormAutofillContent._identifyAutofillFields(doc);
+
+ Assert.deepEqual(markedFieldId, testcase.expectedResult,
+ "Check the fields were marked correctly.");
+ });
+});
--- a/browser/extensions/formautofill/test/unit/xpcshell.ini
+++ b/browser/extensions/formautofill/test/unit/xpcshell.ini
@@ -3,8 +3,9 @@ firefox-appdir = browser
head = head.js
tail =
support-files =
[test_autofillFormFields.js]
[test_collectFormFields.js]
[test_populateFieldValues.js]
[test_profileStorage.js]
+[test_markAsAutofillField.js]