Bug 1339731 - Refactor FormAutofillHandler to support multiple section machanism. r=lchang,ralin

MozReview-Commit-ID: D9g5fKTeTaL

--HG--
extra : rebase_source : 1b19750a6f1d9137b9e21170b854d89cd6d2859c
This commit is contained in:
Sean Lee 2017-10-26 17:57:36 +08:00
Родитель 532fb7bedc
Коммит 29e1c4d8d8
8 изменённых файлов: 485 добавлений и 299 удалений

Просмотреть файл

@ -1709,6 +1709,7 @@ pref("extensions.formautofill.creditCards.enabled", true);
pref("extensions.formautofill.creditCards.used", 0);
pref("extensions.formautofill.firstTimeUse", true);
pref("extensions.formautofill.heuristics.enabled", true);
pref("extensions.formautofill.section.enabled", true);
pref("extensions.formautofill.loglevel", "Warn");
// Whether or not to restore a session with lazy-browser tabs.

Просмотреть файл

@ -102,8 +102,9 @@ AutofillProfileAutoCompleteSearch.prototype = {
let info = FormAutofillContent.getInputDetails(focusedInput);
let isAddressField = FormAutofillUtils.isAddressField(info.fieldName);
let handler = FormAutofillContent.getFormHandler(focusedInput);
let allFieldNames = handler.allFieldNames;
let filledRecordGUID = isAddressField ? handler.address.filledRecordGUID : handler.creditCard.filledRecordGUID;
let section = handler.getSectionByElement(focusedInput);
let allFieldNames = section.allFieldNames;
let filledRecordGUID = isAddressField ? section.address.filledRecordGUID : section.creditCard.filledRecordGUID;
let searchPermitted = isAddressField ?
FormAutofillUtils.isAutofillAddressesEnabled :
FormAutofillUtils.isAutofillCreditCardsEnabled;
@ -149,7 +150,7 @@ AutofillProfileAutoCompleteSearch.prototype = {
// Sort addresses by timeLastUsed for showing the lastest used address at top.
records.sort((a, b) => b.timeLastUsed - a.timeLastUsed);
let adaptedRecords = handler.getAdaptedProfiles(records);
let adaptedRecords = handler.getAdaptedProfiles(records, focusedInput);
let result = null;
if (isAddressField) {
result = new AddressResult(searchString,
@ -481,7 +482,7 @@ var FormAutofillContent = {
getAllFieldNames(element) {
let formHandler = this.getFormHandler(element);
return formHandler ? formHandler.allFieldNames : null;
return formHandler ? formHandler.getAllFieldNames(element) : null;
},
identifyAutofillFields(element) {

Просмотреть файл

@ -8,7 +8,7 @@
"use strict";
this.EXPORTED_SYMBOLS = ["FormAutofillHandler"];
this.EXPORTED_SYMBOLS = ["FormAutofillHandler"]; /* exported FormAutofillHandler */
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
@ -25,202 +25,81 @@ XPCOMUtils.defineLazyModuleGetter(this, "FormLikeFactory",
this.log = null;
FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
/**
* Handles profile autofill for a DOM Form element.
* @param {FormLike} form Form that need to be auto filled
*/
function FormAutofillHandler(form) {
this._updateForm(form);
this.winUtils = this.form.rootElement.ownerGlobal.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
this.address = {
/**
* Similar to the `fieldDetails` above but contains address fields only.
*/
fieldDetails: [],
/**
* String of the filled address' guid.
*/
filledRecordGUID: null,
};
this.creditCard = {
/**
* Similar to the `fieldDetails` above but contains credit card fields only.
*/
fieldDetails: [],
/**
* String of the filled creditCard's guid.
*/
filledRecordGUID: null,
};
this._cacheValue = {
allFieldNames: null,
oneLineStreetAddress: null,
matchingSelectOption: null,
};
}
FormAutofillHandler.prototype = {
/**
* DOM Form element to which this object is attached.
*/
form: null,
/**
* Array of collected data about relevant form fields. Each item is an object
* storing the identifying details of the field and a reference to the
* originally associated element from the form.
*
* The "section", "addressType", "contactType", and "fieldName" values are
* used to identify the exact field when the serializable data is received
* from the backend. There cannot be multiple fields which have
* the same exact combination of these values.
*
* A direct reference to the associated element cannot be sent to the user
* interface because processing may be done in the parent process.
*/
fieldDetails: null,
/**
* Subcategory of handler that contains address related data.
*/
address: null,
/**
* Subcategory of handler that contains credit card related data.
*/
creditCard: null,
/**
* A WindowUtils reference of which Window the form belongs
*/
winUtils: null,
/**
* Enum for form autofill MANUALLY_MANAGED_STATES values
*/
fieldStateEnum: {
// not themed
NORMAL: null,
// highlighted
AUTO_FILLED: "-moz-autofill",
// highlighted && grey color text
PREVIEW: "-moz-autofill-preview",
},
/**
* Time in milliseconds since epoch when a user started filling in the form.
*/
timeStartedFillingMS: null,
/**
* Check the form is necessary to be updated. This function should be able to
* detect any changes including all control elements in the form.
* @param {HTMLElement} element The element supposed to be in the form.
* @returns {boolean} FormAutofillHandler.form is updated or not.
*/
updateFormIfNeeded(element) {
// When the following condition happens, FormAutofillHandler.form should be
// updated:
// * The count of form controls is changed.
// * When the element can not be found in the current form.
//
// However, we should improve the function to detect the element changes.
// e.g. a tel field is changed from type="hidden" to type="tel".
let _formLike;
let getFormLike = () => {
if (!_formLike) {
_formLike = FormLikeFactory.createFromField(element);
}
return _formLike;
class FormAutofillSection {
constructor(fieldDetails, winUtils) {
this.address = {
/**
* Similar to the `_validDetails` but contains address fields only.
*/
fieldDetails: [],
/**
* String of the filled address' guid.
*/
filledRecordGUID: null,
};
this.creditCard = {
/**
* Similar to the `_validDetails` but contains credit card fields only.
*/
fieldDetails: [],
/**
* String of the filled creditCard's' guid.
*/
filledRecordGUID: null,
};
let currentForm = element.form;
if (!currentForm) {
currentForm = getFormLike();
}
/**
* Enum for form autofill MANUALLY_MANAGED_STATES values
*/
this._FIELD_STATE_ENUM = {
// not themed
NORMAL: null,
// highlighted
AUTO_FILLED: "-moz-autofill",
// highlighted && grey color text
PREVIEW: "-moz-autofill-preview",
};
if (currentForm.elements.length != this.form.elements.length) {
log.debug("The count of form elements is changed.");
this._updateForm(getFormLike());
return true;
}
this.winUtils = winUtils;
if (this.form.elements.indexOf(element) === -1) {
log.debug("The element can not be found in the current form.");
this._updateForm(getFormLike());
return true;
}
return false;
},
/**
* Update the form with a new FormLike, and the related fields should be
* updated or clear to ensure the data consistency.
* @param {FormLike} form a new FormLike to replace the original one.
*/
_updateForm(form) {
this.form = form;
this.fieldDetails = [];
if (this.address) {
this.address.fieldDetails = [];
}
if (this.creditCard) {
this.creditCard.fieldDetails = [];
}
},
/**
* Set fieldDetails from the form about fields that can be autofilled.
*
* @param {boolean} allowDuplicates
* true to remain any duplicated field details otherwise to remove the
* duplicated ones.
* @returns {Array} The valid address and credit card details.
*/
collectFormFields(allowDuplicates = false) {
this._cacheValue.allFieldNames = null;
let fieldDetails = FormAutofillHeuristics.getFormInfo(this.form, allowDuplicates);
this.fieldDetails = fieldDetails ? fieldDetails : [];
log.debug("Collected details on", this.fieldDetails.length, "fields");
this.address.fieldDetails = this.fieldDetails.filter(
this.address.fieldDetails = fieldDetails.filter(
detail => FormAutofillUtils.isAddressField(detail.fieldName)
);
this.creditCard.fieldDetails = this.fieldDetails.filter(
detail => FormAutofillUtils.isCreditCardField(detail.fieldName)
);
if (this.address.fieldDetails.length < FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD) {
log.debug("Ignoring address related fields since it has only",
log.debug("Ignoring address related fields since the section has only",
this.address.fieldDetails.length,
"field(s)");
this.address.fieldDetails = [];
}
this.creditCard.fieldDetails = fieldDetails.filter(
detail => FormAutofillUtils.isCreditCardField(detail.fieldName)
);
if (!this._isValidCreditCardForm(this.creditCard.fieldDetails)) {
log.debug("Invalid credit card form");
log.debug("Invalid credit card section.");
this.creditCard.fieldDetails = [];
}
let validDetails = Array.of(...(this.address.fieldDetails),
...(this.creditCard.fieldDetails));
for (let detail of validDetails) {
let input = detail.elementWeakRef.get();
if (!input) {
continue;
}
input.addEventListener("input", this);
}
this._cacheValue = {
allFieldNames: null,
oneLineStreetAddress: null,
matchingSelectOption: null,
};
return validDetails;
},
this._validDetails = Array.of(...(this.address.fieldDetails),
...(this.creditCard.fieldDetails));
log.debug(this._validDetails.length, "valid fields in the section is collected.");
}
get validDetails() {
return this._validDetails;
}
getFieldDetailByElement(element) {
return this._validDetails.find(
detail => detail.elementWeakRef.get() == element
);
}
_isValidCreditCardForm(fieldDetails) {
let ccNumberReason = "";
@ -242,17 +121,18 @@ FormAutofillHandler.prototype = {
}
return hasCCNumber && (ccNumberReason == "autocomplete" || hasExpiryDate);
},
}
get allFieldNames() {
if (!this._cacheValue.allFieldNames) {
this._cacheValue.allFieldNames = this._validDetails.map(record => record.fieldName);
}
return this._cacheValue.allFieldNames;
}
getFieldDetailByName(fieldName) {
return this.fieldDetails.find(detail => detail.fieldName == fieldName);
},
getFieldDetailByElement(element) {
return this.fieldDetails.find(
detail => detail.elementWeakRef.get() == element
);
},
return this._validDetails.find(detail => detail.fieldName == fieldName);
}
getFieldDetailsByElement(element) {
let fieldDetail = this.getFieldDetailByElement(element);
@ -266,14 +146,7 @@ FormAutofillHandler.prototype = {
return this.creditCard.fieldDetails;
}
return [];
},
get allFieldNames() {
if (!this._cacheValue.allFieldNames) {
this._cacheValue.allFieldNames = this.fieldDetails.map(record => record.fieldName);
}
return this._cacheValue.allFieldNames;
},
}
_getOneLineStreetAddress(address) {
if (!this._cacheValue.oneLineStreetAddress) {
@ -283,7 +156,7 @@ FormAutofillHandler.prototype = {
this._cacheValue.oneLineStreetAddress[address] = FormAutofillUtils.toOneLineAddress(address);
}
return this._cacheValue.oneLineStreetAddress[address];
},
}
_addressTransformer(profile) {
if (profile["street-address"]) {
@ -307,7 +180,7 @@ FormAutofillHandler.prototype = {
}
}
}
},
}
/**
* Replace tel with tel-national if tel violates the input element's
@ -361,7 +234,7 @@ FormAutofillHandler.prototype = {
profile.tel = profile["tel-national"];
}
}
},
}
_matchSelectOptions(profile) {
if (!this._cacheValue.matchingSelectOption) {
@ -399,7 +272,7 @@ FormAutofillHandler.prototype = {
delete profile[fieldName];
}
}
},
}
_creditCardExpDateTransformer(profile) {
if (!profile["cc-exp"]) {
@ -435,7 +308,7 @@ FormAutofillHandler.prototype = {
result[2] +
String(ccExpMonth).padStart(result[3].length, "0");
}
},
}
getAdaptedProfiles(originalProfiles) {
for (let profile of originalProfiles) {
@ -445,7 +318,7 @@ FormAutofillHandler.prototype = {
this._creditCardExpDateTransformer(profile);
}
return originalProfiles;
},
}
/**
* Processes form fields that can be autofilled, and populates them with the
@ -457,7 +330,7 @@ FormAutofillHandler.prototype = {
* A focused input element needed to determine the address or credit
* card field.
*/
async autofillFormFields(profile, focusedInput) {
async autofillFields(profile, focusedInput) {
let focusedDetail = this.getFieldDetailByElement(focusedInput);
if (!focusedDetail) {
throw new Error("No fieldDetail for the focused input.");
@ -485,7 +358,7 @@ FormAutofillHandler.prototype = {
throw new Error("Unknown form fields");
}
log.debug("profile in autofillFormFields:", profile);
log.debug("profile in autofillFields:", profile);
targetSet.filledRecordGUID = profile.guid;
for (let fieldDetail of targetSet.fieldDetails) {
@ -532,41 +405,7 @@ FormAutofillHandler.prototype = {
this.changeFieldState(fieldDetail, "AUTO_FILLED");
}
}
// Handle the highlight style resetting caused by user's correction afterward.
log.debug("register change handler for filled form:", this.form);
const onChangeHandler = e => {
let hasFilledFields;
if (!e.isTrusted) {
return;
}
for (let fieldDetail of targetSet.fieldDetails) {
let element = fieldDetail.elementWeakRef.get();
if (!element) {
return;
}
if (e.target == element || (e.target == element.form && e.type == "reset")) {
this.changeFieldState(fieldDetail, "NORMAL");
}
hasFilledFields |= (fieldDetail.state == "AUTO_FILLED");
}
// Unregister listeners and clear guid once no field is in AUTO_FILLED state.
if (!hasFilledFields) {
this.form.rootElement.removeEventListener("input", onChangeHandler);
this.form.rootElement.removeEventListener("reset", onChangeHandler);
targetSet.filledRecordGUID = null;
}
};
this.form.rootElement.addEventListener("input", onChangeHandler);
this.form.rootElement.addEventListener("reset", onChangeHandler);
},
}
/**
* Populates result to the preview layers with given profile.
@ -577,7 +416,7 @@ FormAutofillHandler.prototype = {
* A focused input element for determining credit card or address fields.
*/
previewFormFields(profile, focusedInput) {
log.debug("preview profile in autofillFormFields:", profile);
log.debug("preview profile: ", profile);
// Always show the decrypted credit card number when Master Password is
// disabled.
@ -614,7 +453,7 @@ FormAutofillHandler.prototype = {
element.previewValue = value;
this.changeFieldState(fieldDetail, value ? "PREVIEW" : "NORMAL");
}
},
}
/**
* Clear preview text and background highlight of all fields.
@ -643,7 +482,7 @@ FormAutofillHandler.prototype = {
this.changeFieldState(fieldDetail, "NORMAL");
}
},
}
/**
* Change the state of a field to correspond with different presentations.
@ -660,12 +499,12 @@ FormAutofillHandler.prototype = {
log.warn(fieldDetail.fieldName, "is unreachable while changing state");
return;
}
if (!(nextState in this.fieldStateEnum)) {
if (!(nextState in this._FIELD_STATE_ENUM)) {
log.warn(fieldDetail.fieldName, "is trying to change to an invalid state");
return;
}
for (let [state, mmStateValue] of Object.entries(this.fieldStateEnum)) {
for (let [state, mmStateValue] of Object.entries(this._FIELD_STATE_ENUM)) {
// The NORMAL state is simply the absence of other manually
// managed states so we never need to add or remove it.
if (!mmStateValue) {
@ -680,7 +519,34 @@ FormAutofillHandler.prototype = {
}
fieldDetail.state = nextState;
},
}
clearFieldState(focusedInput) {
let fieldDetail = this.getFieldDetailByElement(focusedInput);
this.changeFieldState(fieldDetail, "NORMAL");
let targetSet;
if (FormAutofillUtils.isAddressField(focusedInput)) {
targetSet = this.address;
} else if (FormAutofillUtils.isCreditCardField(focusedInput)) {
targetSet = this.creditCard;
}
if (!targetSet.fieldDetails.some(detail => detail.state == "AUTO_FILLED")) {
targetSet.filledRecordGUID = null;
}
}
resetFieldStates() {
for (let fieldDetail of this._validDetails) {
this.changeFieldState(fieldDetail, "NORMAL");
}
this.address.filledRecordGUID = null;
this.creditCard.filledRecordGUID = null;
}
isFilled() {
return !!(this.address.filledRecordGUID || this.creditCard.filledRecordGUID);
}
_isAddressRecordCreatable(record) {
let hasName = 0;
@ -696,11 +562,11 @@ FormAutofillHandler.prototype = {
length++;
}
return (length + hasName) >= FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD;
},
}
_isCreditCardRecordCreatable(record) {
return record["cc-number"] && FormAutofillUtils.isCCNumber(record["cc-number"]);
},
}
/**
* Return the records that is converted from address/creditCard fieldDetails and
@ -794,7 +660,7 @@ FormAutofillHandler.prototype = {
}
return data;
},
}
_normalizeAddress(address) {
if (!address) {
@ -837,7 +703,7 @@ FormAutofillHandler.prototype = {
}
}
}
},
}
async _decrypt(cipherText, reauth) {
return new Promise((resolve) => {
@ -848,7 +714,225 @@ FormAutofillHandler.prototype = {
Services.cpmm.sendAsyncMessage("FormAutofill:GetDecryptedString", {cipherText, reauth});
});
},
}
}
/**
* Handles profile autofill for a DOM Form element.
*/
class FormAutofillHandler {
/**
* Initialize the form from `FormLike` object to handle the section or form
* operations.
* @param {FormLike} form Form that need to be auto filled
*/
constructor(form) {
/**
* DOM Form element to which this object is attached.
*/
this.form = null;
this._updateForm(form);
/**
* A WindowUtils reference of which Window the form belongs
*/
this.winUtils = this.form.rootElement.ownerGlobal.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
this.sections = [];
/**
* Array of collected data about relevant form fields. Each item is an object
* storing the identifying details of the field and a reference to the
* originally associated element from the form.
*
* The "section", "addressType", "contactType", and "fieldName" values are
* used to identify the exact field when the serializable data is received
* from the backend. There cannot be multiple fields which have
* the same exact combination of these values.
*
* A direct reference to the associated element cannot be sent to the user
* interface because processing may be done in the parent process.
*/
this.fieldDetails = null;
/**
* Time in milliseconds since epoch when a user started filling in the form.
*/
this.timeStartedFillingMS = null;
}
/**
* Check the form is necessary to be updated. This function should be able to
* detect any changes including all control elements in the form.
* @param {HTMLElement} element The element supposed to be in the form.
* @returns {boolean} FormAutofillHandler.form is updated or not.
*/
updateFormIfNeeded(element) {
// When the following condition happens, FormAutofillHandler.form should be
// updated:
// * The count of form controls is changed.
// * When the element can not be found in the current form.
//
// However, we should improve the function to detect the element changes.
// e.g. a tel field is changed from type="hidden" to type="tel".
let _formLike;
let getFormLike = () => {
if (!_formLike) {
_formLike = FormLikeFactory.createFromField(element);
}
return _formLike;
};
let currentForm = element.form;
if (!currentForm) {
currentForm = getFormLike();
}
if (currentForm.elements.length != this.form.elements.length) {
log.debug("The count of form elements is changed.");
this._updateForm(getFormLike());
return true;
}
if (this.form.elements.indexOf(element) === -1) {
log.debug("The element can not be found in the current form.");
this._updateForm(getFormLike());
return true;
}
return false;
}
/**
* Update the form with a new FormLike, and the related fields should be
* updated or clear to ensure the data consistency.
* @param {FormLike} form a new FormLike to replace the original one.
*/
_updateForm(form) {
this.form = form;
this.fieldDetails = [];
this.sections = [];
}
/**
* Set fieldDetails from the form about fields that can be autofilled.
*
* @param {boolean} allowDuplicates
* true to remain any duplicated field details otherwise to remove the
* duplicated ones.
* @returns {Array} The valid address and credit card details.
*/
collectFormFields(allowDuplicates = false) {
let sections = FormAutofillHeuristics.getFormInfo(this.form, allowDuplicates);
let allValidDetails = [];
for (let fieldDetails of sections) {
let section = new FormAutofillSection(fieldDetails, this.winUtils);
this.sections.push(section);
allValidDetails.push(...section.validDetails);
}
for (let detail of allValidDetails) {
let input = detail.elementWeakRef.get();
if (!input) {
continue;
}
input.addEventListener("input", this);
}
this.fieldDetails = allValidDetails;
return allValidDetails;
}
getFieldDetailByElement(element) {
return this.fieldDetails.find(
detail => detail.elementWeakRef.get() == element
);
}
getSectionByElement(element) {
return this.sections.find(
section => section.getFieldDetailByElement(element)
);
}
getFieldDetailsByElement(element) {
let fieldDetail = this.getFieldDetailByElement(element);
if (!fieldDetail) {
return [];
}
return this.getSectionByElement(element).getFieldDetailsByElement(element);
}
getAllFieldNames(focusedInput) {
let section = this.getSectionByElement(focusedInput);
return section.allFieldNames;
}
previewFormFields(profile, focusedInput) {
let section = this.getSectionByElement(focusedInput);
section.previewFormFields(profile, focusedInput);
}
clearPreviewedFormFields(focusedInput) {
let section = this.getSectionByElement(focusedInput);
section.clearPreviewedFormFields(focusedInput);
}
getAdaptedProfiles(originalProfiles, focusedInput) {
let section = this.getSectionByElement(focusedInput);
section.getAdaptedProfiles(originalProfiles);
return originalProfiles;
}
hasFilledSection() {
return this.sections.some(section => section.isFilled());
}
/**
* Processes form fields that can be autofilled, and populates them with the
* profile provided by backend.
*
* @param {Object} profile
* A profile to be filled in.
* @param {HTMLElement} focusedInput
* A focused input element needed to determine the address or credit
* card field.
*/
async autofillFormFields(profile, focusedInput) {
let noFilledSections = !this.hasFilledSection();
await this.getSectionByElement(focusedInput).autofillFields(profile, focusedInput);
// Handle the highlight style resetting caused by user's correction afterward.
log.debug("register change handler for filled form:", this.form);
const onChangeHandler = e => {
if (!e.isTrusted) {
return;
}
if (e.type == "input") {
let section = this.getSectionByElement(e.target);
section.clearFieldState(e.target);
} else if (e.type == "reset") {
for (let section of this.sections) {
section.resetFieldStates();
}
}
// Unregister listeners once no field is in AUTO_FILLED state.
if (!this.hasFilledSection()) {
this.form.rootElement.removeEventListener("input", onChangeHandler);
this.form.rootElement.removeEventListener("reset", onChangeHandler);
}
};
if (noFilledSections) {
this.form.rootElement.addEventListener("input", onChangeHandler);
this.form.rootElement.addEventListener("reset", onChangeHandler);
}
}
handleEvent(event) {
switch (event.type) {
@ -867,5 +951,15 @@ FormAutofillHandler.prototype = {
this.timeStartedFillingMS = Date.now();
break;
}
},
};
}
createRecords() {
// TODO [Bug 1415073] `FormAutofillHandler.createRecords` should traverse
// all sections and aggregate the records into one result.
if (this.sections.length > 0) {
return this.sections[0].createRecords();
}
return null;
}
}

Просмотреть файл

@ -20,6 +20,7 @@ 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";
/**
* A scanner for traversing all elements in a form and retrieving the field
@ -544,8 +545,9 @@ this.FormAutofillHeuristics = {
},
/**
* This function should provide all field details of a form. The details
* contain the autocomplete info (e.g. fieldName, section, etc).
* This function should provide all field details of a form which are placed
* in the belonging section. The details contain the autocomplete info
* (e.g. fieldName, section, etc).
*
* `allowDuplicates` is used for the xpcshell-test purpose currently because
* the heuristics should be verified that some duplicated elements still can
@ -556,8 +558,8 @@ this.FormAutofillHeuristics = {
* @param {boolean} allowDuplicates
* true to remain any duplicated field details otherwise to remove the
* duplicated ones.
* @returns {Array<Object>}
* all field details in the form.
* @returns {Array<Array<Object>>}
* all sections within its field details in the form.
*/
getFormInfo(form, allowDuplicates = false) {
const eligibleFields = Array.from(form.elements)
@ -582,11 +584,19 @@ this.FormAutofillHeuristics = {
LabelUtils.clearLabelMap();
if (allowDuplicates) {
return fieldScanner.fieldDetails;
if (!this._sectionEnabled) {
// When the section feature is disabled, `getFormInfo` should provide a
// single section result.
return [allowDuplicates ? fieldScanner.fieldDetails : fieldScanner.trimmedFieldDetail];
}
return fieldScanner.trimmedFieldDetail;
return this._groupingFields(fieldScanner, allowDuplicates);
},
_groupingFields(fieldScanner, allowDuplicates) {
// TODO [Bug 1415077] This function should be able to handle the section
// part of autocomplete attr.
return [allowDuplicates ? fieldScanner.fieldDetails : fieldScanner.trimmedFieldDetail];
},
_regExpTableHashValue(...signBits) {
@ -895,3 +905,11 @@ Services.prefs.addObserver(PREF_HEURISTICS_ENABLED, () => {
this.FormAutofillHeuristics._prefEnabled = Services.prefs.getBoolPref(PREF_HEURISTICS_ENABLED);
});
XPCOMUtils.defineLazyGetter(this.FormAutofillHeuristics, "_sectionEnabled", () => {
return Services.prefs.getBoolPref(PREF_SECTION_ENABLED);
});
Services.prefs.addObserver(PREF_SECTION_ENABLED, () => {
this.FormAutofillHeuristics._sectionEnabled = Services.prefs.getBoolPref(PREF_SECTION_ENABLED);
});

Просмотреть файл

@ -100,7 +100,13 @@ function runHeuristicsTest(patterns, fixturePathPrefix) {
Assert.equal(forms.length, testPattern.expectedResult.length, "Expected form count.");
forms.forEach((form, formIndex) => {
let formInfo = FormAutofillHeuristics.getFormInfo(form);
let sections = FormAutofillHeuristics.getFormInfo(form);
if (testPattern.expectedResult[formIndex].length == 0) {
return;
}
// TODO [Bug 1415077] the test should be able to support traversing all
// sections.
let formInfo = sections[0];
do_print("FieldName Prediction Results: " + formInfo.map(i => i.fieldName));
do_print("FieldName Expected Results: " + testPattern.expectedResult[formIndex].map(i => i.fieldName));
Assert.equal(formInfo.length, testPattern.expectedResult[formIndex].length, "Expected field count.");
@ -167,6 +173,7 @@ add_task(async function head_initialize() {
Services.prefs.setStringPref("extensions.formautofill.available", "on");
Services.prefs.setBoolPref("extensions.formautofill.creditCards.available", true);
Services.prefs.setBoolPref("extensions.formautofill.heuristics.enabled", true);
Services.prefs.setBoolPref("extensions.formautofill.section.enabled", false);
Services.prefs.setBoolPref("dom.forms.autocomplete.formautofill", true);
// Clean up after every test.
@ -174,6 +181,7 @@ add_task(async function head_initialize() {
Services.prefs.clearUserPref("extensions.formautofill.available");
Services.prefs.clearUserPref("extensions.formautofill.creditCards.available");
Services.prefs.clearUserPref("extensions.formautofill.heuristics.enabled");
Services.prefs.clearUserPref("extensions.formautofill.section.enabled");
Services.prefs.clearUserPref("dom.forms.autocomplete.formautofill");
});
});

Просмотреть файл

@ -504,7 +504,7 @@ function do_test(testcases, testFn) {
let handler = new FormAutofillHandler(formLike);
let promises = [];
// Replace the interal decrypt method with MasterPassword API
handler._decrypt = async (cipherText, reauth) => {
let decryptHelper = async (cipherText, reauth) => {
let string;
try {
string = await MasterPassword.decrypt(cipherText, reauth);
@ -518,7 +518,14 @@ function do_test(testcases, testFn) {
};
handler.collectFormFields();
let handlerInfo = handler[testcase.expectedFillingForm];
for (let section of handler.sections) {
section._decrypt = decryptHelper;
}
// TODO [Bug 1415077] We can assume all test cases with only one section
// should be filled. Eventually, the test needs to verify the filling
// feature in a multiple section case.
let handlerInfo = handler.sections[0][testcase.expectedFillingForm];
handlerInfo.fieldDetails.forEach(field => {
let element = field.elementWeakRef.get();
if (!testcase.profileData[field.fieldName]) {
@ -529,9 +536,9 @@ function do_test(testcases, testFn) {
promises.push(...testFn(testcase, element));
});
let [adaptedProfile] = handler.getAdaptedProfiles([testcase.profileData]);
let focuedInput = doc.getElementById(testcase.focusedInputId);
await handler.autofillFormFields(adaptedProfile, focuedInput);
let focusedInput = doc.getElementById(testcase.focusedInputId);
let [adaptedProfile] = handler.getAdaptedProfiles([testcase.profileData], focusedInput);
await handler.autofillFormFields(adaptedProfile, focusedInput);
Assert.equal(handlerInfo.filledRecordGUID, testcase.profileData.guid,
"Check if filledRecordGUID is set correctly");
await Promise.all(promises);

Просмотреть файл

@ -440,8 +440,11 @@ for (let tc of TESTCASES) {
let handler = new FormAutofillHandler(formLike);
let validFieldDetails = handler.collectFormFields(testcase.allowDuplicates);
verifyDetails(handler.address.fieldDetails, testcase.addressFieldDetails);
verifyDetails(handler.creditCard.fieldDetails, testcase.creditCardFieldDetails);
// TODO [Bug 1415077] We can assume all test cases with only one section
// should be filled. Eventually, the test needs to verify the filling
// feature in a multiple section case.
verifyDetails(handler.sections[0].address.fieldDetails, testcase.addressFieldDetails);
verifyDetails(handler.sections[0].creditCard.fieldDetails, testcase.creditCardFieldDetails);
verifyDetails(validFieldDetails, testcase.validFieldDetails);
});
})();

Просмотреть файл

@ -29,6 +29,8 @@ const TESTCASES = [
{
description: "Address form with street-address",
document: `<form>
<input autocomplete="given-name">
<input autocomplete="family-name">
<input id="street-addr" autocomplete="street-address">
</form>`,
profileData: [Object.assign({}, DEFAULT_ADDRESS_RECORD)],
@ -70,6 +72,7 @@ const TESTCASES = [
{
description: "Address form with street-address, address-line1",
document: `<form>
<input autocomplete="given-name">
<input id="street-addr" autocomplete="street-address">
<input id="line1" autocomplete="address-line1">
</form>`,
@ -132,6 +135,7 @@ const TESTCASES = [
{
description: "Address form with exact matching options in select",
document: `<form>
<input autocomplete="given-name">
<select autocomplete="address-level1">
<option id="option-address-level1-XX" value="XX">Dummy</option>
<option id="option-address-level1-CA" value="CA">California</option>
@ -162,6 +166,7 @@ const TESTCASES = [
{
description: "Address form with inexact matching options in select",
document: `<form>
<input autocomplete="given-name">
<select autocomplete="address-level1">
<option id="option-address-level1-XX" value="XX">Dummy</option>
<option id="option-address-level1-OO" value="OO">California</option>
@ -192,6 +197,7 @@ const TESTCASES = [
{
description: "Address form with value-omitted options in select",
document: `<form>
<input autocomplete="given-name">
<select autocomplete="address-level1">
<option id="option-address-level1-1" value="">Dummy</option>
<option id="option-address-level1-2" value="">California</option>
@ -222,6 +228,7 @@ const TESTCASES = [
{
description: "Address form with options with the same value in select ",
document: `<form>
<input autocomplete="given-name">
<select autocomplete="address-level1">
<option id="option-address-level1-same1" value="same">Dummy</option>
<option id="option-address-level1-same2" value="same">California</option>
@ -252,6 +259,7 @@ const TESTCASES = [
{
description: "Address form without matching options in select for address-level1 and country",
document: `<form>
<input autocomplete="given-name">
<select autocomplete="address-level1">
<option id="option-address-level1-dummy1" value="">Dummy</option>
<option id="option-address-level1-dummy2" value="">Dummy 2</option>
@ -465,6 +473,7 @@ const TESTCASES = [
{
description: "Credit Card form with matching options of cc-exp-year and cc-exp-month",
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp-month">
<option id="option-cc-exp-month-01" value="1">01</option>
<option id="option-cc-exp-month-02" value="2">02</option>
@ -504,6 +513,7 @@ const TESTCASES = [
{
description: "Credit Card form with matching options which contain labels",
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp-month">
<option value="" selected="selected">Month</option>
<option label="01 - January" id="option-cc-exp-month-01" value="object:17">dummy</option>
@ -551,7 +561,9 @@ const TESTCASES = [
},
{
description: "Compound cc-exp: {MON1}/{YEAR2}",
document: `<form><select autocomplete="cc-exp">
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp">
<option value="3/17">3/17</option>
<option value="1/25" id="selected-cc-exp">1/25</option>
</select></form>`,
@ -561,7 +573,9 @@ const TESTCASES = [
},
{
description: "Compound cc-exp: {MON1}/{YEAR4}",
document: `<form><select autocomplete="cc-exp">
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp">
<option value="3/2017">3/2017</option>
<option value="1/2025" id="selected-cc-exp">1/2025</option>
</select></form>`,
@ -571,7 +585,9 @@ const TESTCASES = [
},
{
description: "Compound cc-exp: {MON2}/{YEAR2}",
document: `<form><select autocomplete="cc-exp">
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp">
<option value="03/17">03/17</option>
<option value="01/25" id="selected-cc-exp">01/25</option>
</select></form>`,
@ -581,7 +597,9 @@ const TESTCASES = [
},
{
description: "Compound cc-exp: {MON2}/{YEAR4}",
document: `<form><select autocomplete="cc-exp">
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp">
<option value="03/2017">03/2017</option>
<option value="01/2025" id="selected-cc-exp">01/2025</option>
</select></form>`,
@ -591,7 +609,9 @@ const TESTCASES = [
},
{
description: "Compound cc-exp: {MON1}-{YEAR2}",
document: `<form><select autocomplete="cc-exp">
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp">
<option value="3-17">3-17</option>
<option value="1-25" id="selected-cc-exp">1-25</option>
</select></form>`,
@ -601,7 +621,9 @@ const TESTCASES = [
},
{
description: "Compound cc-exp: {MON1}-{YEAR4}",
document: `<form><select autocomplete="cc-exp">
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp">
<option value="3-2017">3-2017</option>
<option value="1-2025" id="selected-cc-exp">1-2025</option>
</select></form>`,
@ -611,7 +633,9 @@ const TESTCASES = [
},
{
description: "Compound cc-exp: {MON2}-{YEAR2}",
document: `<form><select autocomplete="cc-exp">
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp">
<option value="03-17">03-17</option>
<option value="01-25" id="selected-cc-exp">01-25</option>
</select></form>`,
@ -621,7 +645,9 @@ const TESTCASES = [
},
{
description: "Compound cc-exp: {MON2}-{YEAR4}",
document: `<form><select autocomplete="cc-exp">
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp">
<option value="03-2017">03-2017</option>
<option value="01-2025" id="selected-cc-exp">01-2025</option>
</select></form>`,
@ -631,7 +657,9 @@ const TESTCASES = [
},
{
description: "Compound cc-exp: {YEAR2}-{MON2}",
document: `<form><select autocomplete="cc-exp">
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp">
<option value="17-03">17-03</option>
<option value="25-01" id="selected-cc-exp">25-01</option>
</select></form>`,
@ -641,7 +669,9 @@ const TESTCASES = [
},
{
description: "Compound cc-exp: {YEAR4}-{MON2}",
document: `<form><select autocomplete="cc-exp">
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp">
<option value="2017-03">2017-03</option>
<option value="2025-01" id="selected-cc-exp">2025-01</option>
</select></form>`,
@ -651,7 +681,9 @@ const TESTCASES = [
},
{
description: "Compound cc-exp: {YEAR4}/{MON2}",
document: `<form><select autocomplete="cc-exp">
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp">
<option value="2017/3">2017/3</option>
<option value="2025/1" id="selected-cc-exp">2025/1</option>
</select></form>`,
@ -661,7 +693,9 @@ const TESTCASES = [
},
{
description: "Compound cc-exp: {MON2}{YEAR2}",
document: `<form><select autocomplete="cc-exp">
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp">
<option value="0317">0317</option>
<option value="0125" id="selected-cc-exp">0125</option>
</select></form>`,
@ -671,7 +705,9 @@ const TESTCASES = [
},
{
description: "Compound cc-exp: {YEAR2}{MON2}",
document: `<form><select autocomplete="cc-exp">
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp">
<option value="1703">1703</option>
<option value="2501" id="selected-cc-exp">2501</option>
</select></form>`,
@ -681,7 +717,9 @@ const TESTCASES = [
},
{
description: "Fill a cc-exp without cc-exp-month value in the profile",
document: `<form><select autocomplete="cc-exp">
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp">
<option value="03/17">03/17</option>
<option value="01/25">01/25</option>
</select></form>`,
@ -697,7 +735,9 @@ const TESTCASES = [
},
{
description: "Fill a cc-exp without cc-exp-year value in the profile",
document: `<form><select autocomplete="cc-exp">
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp">
<option value="03/17">03/17</option>
<option value="01/25">01/25</option>
</select></form>`,
@ -714,6 +754,7 @@ const TESTCASES = [
{
description: "Fill a cc-exp* without cc-exp-month value in the profile",
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp-month">
<option value="03">03</option>
<option value="01">01</option>
@ -736,6 +777,7 @@ const TESTCASES = [
{
description: "Fill a cc-exp* without cc-exp-year value in the profile",
document: `<form>
<input autocomplete="cc-number">
<select autocomplete="cc-exp-month">
<option value="03">03</option>
<option value="01">01</option>
@ -757,7 +799,8 @@ const TESTCASES = [
},
{
description: "Use placeholder to adjust cc-exp format [mm/yy].",
document: `<form><input placeholder="mm/yy" autocomplete="cc-exp"></form>`,
document: `<form><input autocomplete="cc-number">
<input placeholder="mm/yy" autocomplete="cc-exp"></form>`,
profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
"cc-exp": "01/25",
@ -765,7 +808,8 @@ const TESTCASES = [
},
{
description: "Use placeholder to adjust cc-exp format [mm / yy].",
document: `<form><input placeholder="mm / yy" autocomplete="cc-exp"></form>`,
document: `<form><input autocomplete="cc-number">
<input placeholder="mm / yy" autocomplete="cc-exp"></form>`,
profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
"cc-exp": "01/25",
@ -773,7 +817,8 @@ const TESTCASES = [
},
{
description: "Use placeholder to adjust cc-exp format [MM / YY].",
document: `<form><input placeholder="MM / YY" autocomplete="cc-exp"></form>`,
document: `<form><input autocomplete="cc-number">
<input placeholder="MM / YY" autocomplete="cc-exp"></form>`,
profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
"cc-exp": "01/25",
@ -781,7 +826,8 @@ const TESTCASES = [
},
{
description: "Use placeholder to adjust cc-exp format [mm / yyyy].",
document: `<form><input placeholder="mm / yyyy" autocomplete="cc-exp"></form>`,
document: `<form><input autocomplete="cc-number">
<input placeholder="mm / yyyy" autocomplete="cc-exp"></form>`,
profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
"cc-exp": "01/2025",
@ -789,7 +835,8 @@ const TESTCASES = [
},
{
description: "Use placeholder to adjust cc-exp format [mm - yyyy].",
document: `<form><input placeholder="mm - yyyy" autocomplete="cc-exp"></form>`,
document: `<form><input autocomplete="cc-number">
<input placeholder="mm - yyyy" autocomplete="cc-exp"></form>`,
profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
"cc-exp": "01-2025",
@ -797,7 +844,8 @@ const TESTCASES = [
},
{
description: "Use placeholder to adjust cc-exp format [yyyy-mm].",
document: `<form><input placeholder="yyyy-mm" autocomplete="cc-exp"></form>`,
document: `<form><input autocomplete="cc-number">
<input placeholder="yyyy-mm" autocomplete="cc-exp"></form>`,
profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
"cc-exp": "2025-01",
@ -805,7 +853,8 @@ const TESTCASES = [
},
{
description: "Use placeholder to adjust cc-exp format [yyy-mm].",
document: `<form><input placeholder="yyy-mm" autocomplete="cc-exp"></form>`,
document: `<form><input autocomplete="cc-number">
<input placeholder="yyy-mm" autocomplete="cc-exp"></form>`,
profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD, {
"cc-exp": "025-01",
@ -813,19 +862,22 @@ const TESTCASES = [
},
{
description: "Use placeholder to adjust cc-exp format [mmm yyyy].",
document: `<form><input placeholder="mmm yyyy" autocomplete="cc-exp"></form>`,
document: `<form><input autocomplete="cc-number">
<input placeholder="mmm yyyy" autocomplete="cc-exp"></form>`,
profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
},
{
description: "Use placeholder to adjust cc-exp format [mm foo yyyy].",
document: `<form><input placeholder="mm foo yyyy" autocomplete="cc-exp"></form>`,
document: `<form><input autocomplete="cc-number">
<input placeholder="mm foo yyyy" autocomplete="cc-exp"></form>`,
profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
},
{
description: "Use placeholder to adjust cc-exp format [mm - - yyyy].",
document: `<form><input placeholder="mm - - yyyy" autocomplete="cc-exp"></form>`,
document: `<form><input autocomplete="cc-number">
<input placeholder="mm - - yyyy" autocomplete="cc-exp"></form>`,
profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)],
},
@ -842,7 +894,8 @@ for (let testcase of TESTCASES) {
let handler = new FormAutofillHandler(formLike);
handler.collectFormFields();
let adaptedRecords = handler.getAdaptedProfiles(testcase.profileData);
let focusedInput = form.elements[0];
let adaptedRecords = handler.getAdaptedProfiles(testcase.profileData, focusedInput);
Assert.deepEqual(adaptedRecords, testcase.expectedResult);
if (testcase.expectedOptionElements) {
@ -853,7 +906,8 @@ for (let testcase of TESTCASES) {
Assert.notEqual(expectedOption, null);
let value = testcase.profileData[i][field];
let cache = handler._cacheValue.matchingSelectOption.get(select);
let section = handler.getSectionByElement(select);
let cache = section._cacheValue.matchingSelectOption.get(select);
let targetOption = cache[value] && cache[value].get();
Assert.notEqual(targetOption, null);