From 29e1c4d8d8de09b7f29060f43155c7dbc52d69b3 Mon Sep 17 00:00:00 2001 From: Sean Lee Date: Thu, 26 Oct 2017 17:57:36 +0800 Subject: [PATCH] Bug 1339731 - Refactor FormAutofillHandler to support multiple section machanism. r=lchang,ralin MozReview-Commit-ID: D9g5fKTeTaL --HG-- extra : rebase_source : 1b19750a6f1d9137b9e21170b854d89cd6d2859c --- browser/app/profile/firefox.js | 1 + .../formautofill/FormAutofillContent.jsm | 9 +- .../formautofill/FormAutofillHandler.jsm | 600 ++++++++++-------- .../formautofill/FormAutofillHeuristics.jsm | 32 +- .../extensions/formautofill/test/unit/head.js | 10 +- .../test/unit/test_autofillFormFields.js | 17 +- .../test/unit/test_collectFormFields.js | 7 +- .../test/unit/test_getAdaptedProfiles.js | 108 +++- 8 files changed, 485 insertions(+), 299 deletions(-) diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index a8be50994341..2130678ced50 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -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. diff --git a/browser/extensions/formautofill/FormAutofillContent.jsm b/browser/extensions/formautofill/FormAutofillContent.jsm index 2666bd9a98c1..bd89b835fb19 100644 --- a/browser/extensions/formautofill/FormAutofillContent.jsm +++ b/browser/extensions/formautofill/FormAutofillContent.jsm @@ -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) { diff --git a/browser/extensions/formautofill/FormAutofillHandler.jsm b/browser/extensions/formautofill/FormAutofillHandler.jsm index f5882548579c..fb39e4b1cea7 100644 --- a/browser/extensions/formautofill/FormAutofillHandler.jsm +++ b/browser/extensions/formautofill/FormAutofillHandler.jsm @@ -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; + } +} + diff --git a/browser/extensions/formautofill/FormAutofillHeuristics.jsm b/browser/extensions/formautofill/FormAutofillHeuristics.jsm index b6c47687e7bf..4d96e030a83e 100644 --- a/browser/extensions/formautofill/FormAutofillHeuristics.jsm +++ b/browser/extensions/formautofill/FormAutofillHeuristics.jsm @@ -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} - * all field details in the form. + * @returns {Array>} + * 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); +}); + diff --git a/browser/extensions/formautofill/test/unit/head.js b/browser/extensions/formautofill/test/unit/head.js index 6e4dd22d3752..424bcd6719d9 100644 --- a/browser/extensions/formautofill/test/unit/head.js +++ b/browser/extensions/formautofill/test/unit/head.js @@ -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"); }); }); diff --git a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js index 55fb61adc06d..8b8d0ceefd49 100644 --- a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js +++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js @@ -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); diff --git a/browser/extensions/formautofill/test/unit/test_collectFormFields.js b/browser/extensions/formautofill/test/unit/test_collectFormFields.js index d507ecb87e05..a6543f6c8e1b 100644 --- a/browser/extensions/formautofill/test/unit/test_collectFormFields.js +++ b/browser/extensions/formautofill/test/unit/test_collectFormFields.js @@ -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); }); })(); diff --git a/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js b/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js index a6ef8a9c11f0..eb2efd4e4fbb 100644 --- a/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js +++ b/browser/extensions/formautofill/test/unit/test_getAdaptedProfiles.js @@ -29,6 +29,8 @@ const TESTCASES = [ { description: "Address form with street-address", document: `
+ +
`, profileData: [Object.assign({}, DEFAULT_ADDRESS_RECORD)], @@ -70,6 +72,7 @@ const TESTCASES = [ { description: "Address form with street-address, address-line1", document: `
+
`, @@ -132,6 +135,7 @@ const TESTCASES = [ { description: "Address form with exact matching options in select", document: `
+ + document: ` + +
`, @@ -561,7 +573,9 @@ const TESTCASES = [ }, { description: "Compound cc-exp: {MON1}/{YEAR4}", - document: `
+
`, @@ -571,7 +585,9 @@ const TESTCASES = [ }, { description: "Compound cc-exp: {MON2}/{YEAR2}", - document: `
+
`, @@ -581,7 +597,9 @@ const TESTCASES = [ }, { description: "Compound cc-exp: {MON2}/{YEAR4}", - document: `
+
`, @@ -591,7 +609,9 @@ const TESTCASES = [ }, { description: "Compound cc-exp: {MON1}-{YEAR2}", - document: `
+
`, @@ -601,7 +621,9 @@ const TESTCASES = [ }, { description: "Compound cc-exp: {MON1}-{YEAR4}", - document: `
+
`, @@ -611,7 +633,9 @@ const TESTCASES = [ }, { description: "Compound cc-exp: {MON2}-{YEAR2}", - document: `
+
`, @@ -621,7 +645,9 @@ const TESTCASES = [ }, { description: "Compound cc-exp: {MON2}-{YEAR4}", - document: `
+
`, @@ -631,7 +657,9 @@ const TESTCASES = [ }, { description: "Compound cc-exp: {YEAR2}-{MON2}", - document: `
+
`, @@ -641,7 +669,9 @@ const TESTCASES = [ }, { description: "Compound cc-exp: {YEAR4}-{MON2}", - document: `
+
`, @@ -651,7 +681,9 @@ const TESTCASES = [ }, { description: "Compound cc-exp: {YEAR4}/{MON2}", - document: `
+
`, @@ -661,7 +693,9 @@ const TESTCASES = [ }, { description: "Compound cc-exp: {MON2}{YEAR2}", - document: `
+
`, @@ -671,7 +705,9 @@ const TESTCASES = [ }, { description: "Compound cc-exp: {YEAR2}{MON2}", - document: `
+
`, @@ -681,7 +717,9 @@ const TESTCASES = [ }, { description: "Fill a cc-exp without cc-exp-month value in the profile", - document: `
+
`, @@ -697,7 +735,9 @@ const TESTCASES = [ }, { description: "Fill a cc-exp without cc-exp-year value in the profile", - document: `
+
`, @@ -714,6 +754,7 @@ const TESTCASES = [ { description: "Fill a cc-exp* without cc-exp-month value in the profile", document: `
+
`, + document: `
+
`, 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: `
`, + document: `
+
`, 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: `
`, + document: `
+
`, 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: `
`, + document: `
+
`, 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: `
`, + document: `
+
`, 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: `
`, + document: `
+
`, 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: `
`, + document: `
+
`, 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: `
`, + document: `
+
`, profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)], expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)], }, { description: "Use placeholder to adjust cc-exp format [mm foo yyyy].", - document: `
`, + document: `
+
`, profileData: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)], expectedResult: [Object.assign({}, DEFAULT_CREDITCARD_RECORD)], }, { description: "Use placeholder to adjust cc-exp format [mm - - yyyy].", - document: `
`, + document: `
+
`, 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);