--- a/browser/extensions/formautofill/FormAutofillHeuristics.jsm
+++ b/browser/extensions/formautofill/FormAutofillHeuristics.jsm
@@ -16,35 +16,38 @@ Cu.import("resource://gre/modules/Servic
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://formautofill/FormAutofillUtils.jsm");
this.log = null;
FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
const PREF_HEURISTICS_ENABLED = "extensions.formautofill.heuristics.enabled";
const PREF_SECTION_ENABLED = "extensions.formautofill.section.enabled";
+const DEFAULT_SECTION_NAME = "-moz-section-default";
/**
* A scanner for traversing all elements in a form and retrieving the field
* detail with FormAutofillHeuristics.getInfo function. It also provides a
* cursor (parsingIndex) to indicate which element is waiting for parsing.
*/
class FieldScanner {
/**
* Create a FieldScanner based on form elements with the existing
* fieldDetails.
*
* @param {Array.DOMElement} elements
* The elements from a form for each parser.
*/
- constructor(elements) {
+ constructor(elements, {allowDuplicates = false, sectionEnabled = true}) {
this._elementsWeakRef = Cu.getWeakReference(elements);
this.fieldDetails = [];
this._parsingIndex = 0;
this._sections = [];
+ this._allowDuplicates = allowDuplicates;
+ this._sectionEnabled = sectionEnabled;
}
get _elements() {
return this._elementsWeakRef.get();
}
/**
* This cursor means the index of the element which is waiting for parsing.
@@ -107,32 +110,73 @@ class FieldScanner {
}
}
this._sections.push({
name,
fieldDetails: [fieldDetail],
});
}
- getSectionFieldDetails(allowDuplicates) {
- // TODO: [Bug 1416664] If there is only one section which is not defined by
- // `autocomplete` attribute, the sections should be classified by the
- // heuristics.
- return this._sections.map(section => {
- if (allowDuplicates) {
- return section.fieldDetails;
+ _classifySections() {
+ let fieldDetails = this._sections[0].fieldDetails;
+ this._sections = [];
+ let seenTypes = new Set();
+ let previousType;
+ let sectionCount = 0;
+
+ for (let fieldDetail of fieldDetails) {
+ if (!fieldDetail.fieldName) {
+ continue;
+ }
+ if (seenTypes.has(fieldDetail.fieldName) &&
+ previousType != fieldDetail.fieldName) {
+ seenTypes.clear();
+ sectionCount++;
}
- return this._trimFieldDetails(section.fieldDetails);
- });
+ previousType = fieldDetail.fieldName;
+ seenTypes.add(fieldDetail.fieldName);
+ delete fieldDetail._duplicated;
+ this._pushToSection(DEFAULT_SECTION_NAME + "-" + sectionCount, fieldDetail);
+ }
+ }
+
+ /**
+ * The result is an array contains the sections with its belonging field
+ * details. If `this._sections` contains one section only with the default
+ * section name (DEFAULT_SECTION_NAME), `this._classifySections` should be
+ * able to identify all sections in the heuristic way.
+ *
+ * @returns {Array<Object>}
+ * The array with the sections, and the belonging fieldDetails are in
+ * each section.
+ */
+ getSectionFieldDetails() {
+ // When the section feature is disabled, `getSectionFieldDetails` should
+ // provide a single section result.
+ if (!this._sectionEnabled) {
+ return [this._getFinalDetails(this.fieldDetails)];
+ }
+ if (this._sections.length == 0) {
+ return [];
+ }
+ if (this._sections.length == 1 && this._sections[0].name == DEFAULT_SECTION_NAME) {
+ this._classifySections();
+ }
+
+ return this._sections.map(section =>
+ this._getFinalDetails(section.fieldDetails)
+ );
}
/**
* This function will prepare an autocomplete info object with getInfo
* function and push the detail to fieldDetails property. Any duplicated
* detail will be marked as _duplicated = true for the parser.
+ * Any field will be pushed into `this._sections` based on the section name
+ * in `autocomplete` attribute.
*
* Any element without the related detail will be used for adding the detail
* to the end of field details.
*/
pushDetail() {
let elementIndex = this.fieldDetails.length;
if (elementIndex >= this._elements.length) {
throw new Error("Try to push the non-existing element info.");
@@ -168,17 +212,17 @@ class FieldScanner {
_getSectionName(info) {
let names = [];
if (info.section) {
names.push(info.section);
}
if (info.addressType) {
names.push(info.addressType);
}
- return names.length ? names.join(" ") : "-moz-section-default";
+ return names.length ? names.join(" ") : DEFAULT_SECTION_NAME;
}
/**
* When a field detail should be changed its fieldName after parsing, use
* this function to update the fieldName which is at a specific index.
*
* @param {number} index
* The index indicates a field detail to be updated.
@@ -200,32 +244,34 @@ class FieldScanner {
findSameField(info) {
return this.fieldDetails.findIndex(f => f.section == info.section &&
f.addressType == info.addressType &&
f.fieldName == info.fieldName);
}
/**
- * Provide the field details without invalid field name and duplicated fields.
+ * Provide the final field details without invalid field name, and the
+ * duplicated fields will be removed as well. For the debugging purpose,
+ * the final `fieldDetails` will include the duplicated fields if
+ * `_allowDuplicates` is true.
*
* @param {Array<Object>} fieldDetails
* The field details for trimming.
* @returns {Array<Object>}
* The array with the field details without invalid field name and
* duplicated fields.
*/
- _trimFieldDetails(fieldDetails) {
+ _getFinalDetails(fieldDetails) {
+ if (this._allowDuplicates) {
+ return fieldDetails.filter(f => f.fieldName);
+ }
return fieldDetails.filter(f => f.fieldName && !f._duplicated);
}
- getFieldDetails(allowDuplicates) {
- return allowDuplicates ? this.fieldDetails : this._trimFieldDetails(this.fieldDetails);
- }
-
elementExisting(index) {
return index < this._elements.length;
}
}
this.LabelUtils = {
// The tag name list is from Chromium except for "STYLE":
// eslint-disable-next-line max-len
@@ -646,38 +692,33 @@ this.FormAutofillHeuristics = {
getFormInfo(form, allowDuplicates = false) {
const eligibleFields = Array.from(form.elements)
.filter(elem => FormAutofillUtils.isFieldEligibleForAutofill(elem));
if (eligibleFields.length <= 0) {
return [];
}
- let fieldScanner = new FieldScanner(eligibleFields);
+ let fieldScanner = new FieldScanner(eligibleFields,
+ {allowDuplicates, sectionEnabled: this._sectionEnabled});
while (!fieldScanner.parsingFinished) {
let parsedPhoneFields = this._parsePhoneFields(fieldScanner);
let parsedAddressFields = this._parseAddressFields(fieldScanner);
let parsedExpirationDateFields = this._parseCreditCardExpirationDateFields(fieldScanner);
// If there is no any field parsed, the parsing cursor can be moved
// forward to the next one.
if (!parsedPhoneFields && !parsedAddressFields && !parsedExpirationDateFields) {
fieldScanner.parsingIndex++;
}
}
LabelUtils.clearLabelMap();
- if (!this._sectionEnabled) {
- // When the section feature is disabled, `getFormInfo` should provide a
- // single section result.
- return [fieldScanner.getFieldDetails(allowDuplicates)];
- }
-
- return fieldScanner.getSectionFieldDetails(allowDuplicates);
+ return fieldScanner.getSectionFieldDetails();
},
_regExpTableHashValue(...signBits) {
return signBits.reduce((p, c, i) => p | !!c << i, 0);
},
_setRegExpListCache(regexps, b0, b1, b2) {
if (!this._regexpList) {
--- a/browser/extensions/formautofill/test/unit/heuristics/test_multiple_section.js
+++ b/browser/extensions/formautofill/test/unit/heuristics/test_multiple_section.js
@@ -36,12 +36,43 @@ runHeuristicsTest([
[
{"section": "section-my", "addressType": "", "contactType": "", "fieldName": "street-address"},
{"section": "section-my", "addressType": "", "contactType": "", "fieldName": "address-level2"},
{"section": "section-my", "addressType": "", "contactType": "", "fieldName": "address-level1"},
{"section": "section-my", "addressType": "", "contactType": "", "fieldName": "postal-code"},
{"section": "section-my", "addressType": "", "contactType": "", "fieldName": "country"},
],
],
+ [
+ [
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "name"},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "organization"},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address"},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level1"},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "postal-code"},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "country"},
+ ],
+ [
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address"},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level1"},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "postal-code"},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "country"},
+ ],
+ [
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "street-address"},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level2"},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "address-level1"},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "postal-code"},
+ {"section": "", "addressType": "", "contactType": "", "fieldName": "country"},
+ {"section": "", "addressType": "", "contactType": "work", "fieldName": "tel"},
+ {"section": "", "addressType": "", "contactType": "work", "fieldName": "email"},
+ ],
+ [
+ {"section": "", "addressType": "", "contactType": "home", "fieldName": "tel"},
+ {"section": "", "addressType": "", "contactType": "home", "fieldName": "email"},
+ ],
+ ],
],
},
], "../../fixtures/");