1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * Defines a handler object to represent forms that autofill can handle.
11 var EXPORTED_SYMBOLS = ["FormAutofillHandler"];
13 const { AppConstants } = ChromeUtils.import(
14 "resource://gre/modules/AppConstants.jsm"
16 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
17 const { XPCOMUtils } = ChromeUtils.import(
18 "resource://gre/modules/XPCOMUtils.jsm"
20 const { FormAutofill } = ChromeUtils.import(
21 "resource://formautofill/FormAutofill.jsm"
24 ChromeUtils.defineModuleGetter(
27 "resource://formautofill/FormAutofillUtils.jsm"
29 ChromeUtils.defineModuleGetter(
31 "FormAutofillHeuristics",
32 "resource://formautofill/FormAutofillHeuristics.jsm"
34 ChromeUtils.defineModuleGetter(
37 "resource://gre/modules/FormLikeFactory.jsm"
40 XPCOMUtils.defineLazyGetter(this, "reauthPasswordPromptMessage", () => {
41 const brandShortName = FormAutofillUtils.brandBundle.GetStringFromName(
44 return FormAutofillUtils.stringBundle.formatStringFromName(
45 `useCreditCardPasswordPrompt.${AppConstants.platform}`,
51 FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]);
53 const { FIELD_STATES } = FormAutofillUtils;
55 class FormAutofillSection {
56 constructor(fieldDetails, winUtils) {
57 this.fieldDetails = fieldDetails;
58 this.filledRecordGUID = null;
59 this.winUtils = winUtils;
62 * Enum for form autofill MANUALLY_MANAGED_STATES values
64 this._FIELD_STATE_ENUM = {
66 [FIELD_STATES.NORMAL]: null,
68 [FIELD_STATES.AUTO_FILLED]: "-moz-autofill",
69 // highlighted && grey color text
70 [FIELD_STATES.PREVIEW]: "-moz-autofill-preview",
73 if (!this.isValidSection()) {
74 this.fieldDetails = [];
78 } related fields since it is an invalid section`
84 matchingSelectOption: null,
89 * Examine the section is a valid section or not based on its fieldDetails or
90 * other information. This method must be overrided.
92 * @returns {boolean} True for a valid section, otherwise false
96 throw new TypeError("isValidSection method must be overrided");
100 * Examine the section is an enabled section type or not based on its
101 * preferences. This method must be overrided.
103 * @returns {boolean} True for an enabled section type, otherwise false
107 throw new TypeError("isEnabled method must be overrided");
111 * Examine the section is createable for storing the profile. This method
114 * @param {Object} record The record for examining createable
115 * @returns {boolean} True for the record is createable, otherwise false
118 isRecordCreatable(record) {
119 throw new TypeError("isRecordCreatable method must be overrided");
123 * Override this method if the profile is needed to apply some transformers.
125 * @param {Object} profile
126 * A profile should be converted based on the specific requirement.
128 applyTransformers(profile) {}
131 * Override this method if the profile is needed to be customized for
134 * @param {Object} profile
135 * A profile for pre-processing before previewing values.
137 preparePreviewProfile(profile) {}
140 * Override this method if the profile is needed to be customized for filling
143 * @param {Object} profile
144 * A profile for pre-processing before filling values.
145 * @returns {boolean} Whether the profile should be filled.
147 async prepareFillingProfile(profile) {
152 * Override this methid if any data for `createRecord` is needed to be
153 * normailized before submitting the record.
155 * @param {Object} profile
156 * A record for normalization.
158 normalizeCreatingRecord(data) {}
161 * Override this method if there is any field value needs to compute for a
162 * specific case. Return the original value in the default case.
163 * @param {String} value
164 * The original field value.
165 * @param {Object} fieldDetail
166 * A fieldDetail of the related element.
167 * @param {HTMLElement} element
168 * A element for checking converting value.
171 * A string of the converted value.
173 computeFillingValue(value, fieldName, element) {
177 set focusedInput(element) {
178 this._focusedDetail = this.getFieldDetailByElement(element);
181 getFieldDetailByElement(element) {
182 return this.fieldDetails.find(
183 detail => detail.elementWeakRef.get() == element
187 get allFieldNames() {
188 if (!this._cacheValue.allFieldNames) {
189 this._cacheValue.allFieldNames = this.fieldDetails.map(
190 record => record.fieldName
193 return this._cacheValue.allFieldNames;
196 getFieldDetailByName(fieldName) {
197 return this.fieldDetails.find(detail => detail.fieldName == fieldName);
200 matchSelectOptions(profile) {
201 if (!this._cacheValue.matchingSelectOption) {
202 this._cacheValue.matchingSelectOption = new WeakMap();
205 for (let fieldName in profile) {
206 let fieldDetail = this.getFieldDetailByName(fieldName);
211 let element = fieldDetail.elementWeakRef.get();
212 if (ChromeUtils.getClassName(element) !== "HTMLSelectElement") {
216 let cache = this._cacheValue.matchingSelectOption.get(element) || {};
217 let value = profile[fieldName];
218 if (cache[value] && cache[value].get()) {
222 let option = FormAutofillUtils.findSelectOption(
228 cache[value] = Cu.getWeakReference(option);
229 this._cacheValue.matchingSelectOption.set(element, cache);
233 this._cacheValue.matchingSelectOption.set(element, cache);
235 // Delete the field so the phishing hint won't treat it as a "also fill"
237 delete profile[fieldName];
242 adaptFieldMaxLength(profile) {
243 for (let key in profile) {
244 let detail = this.getFieldDetailByName(key);
249 let element = detail.elementWeakRef.get();
254 let maxLength = element.maxLength;
256 maxLength === undefined ||
258 profile[key].length <= maxLength
264 profile[key] = profile[key].substr(0, maxLength);
271 getAdaptedProfiles(originalProfiles) {
272 for (let profile of originalProfiles) {
273 this.applyTransformers(profile);
275 return originalProfiles;
279 * Processes form fields that can be autofilled, and populates them with the
280 * profile provided by backend.
282 * @param {Object} profile
283 * A profile to be filled in.
285 async autofillFields(profile) {
286 let focusedDetail = this._focusedDetail;
287 if (!focusedDetail) {
288 throw new Error("No fieldDetail for the focused input.");
291 if (!(await this.prepareFillingProfile(profile))) {
292 log.debug("profile cannot be filled", profile);
295 log.debug("profile in autofillFields:", profile);
297 this.filledRecordGUID = profile.guid;
298 for (let fieldDetail of this.fieldDetails) {
299 // Avoid filling field value in the following cases:
300 // 1. a non-empty input field for an unfocused input
301 // 2. the invalid value set
302 // 3. value already chosen in select element
304 let element = fieldDetail.elementWeakRef.get();
309 element.previewValue = "";
310 let value = profile[fieldDetail.fieldName];
312 if (ChromeUtils.getClassName(element) === "HTMLInputElement" && value) {
313 // For the focused input element, it will be filled with a valid value
315 // For the others, the fields should be only filled when their values
317 let focusedInput = focusedDetail.elementWeakRef.get();
319 element == focusedInput ||
320 (element != focusedInput && !element.value)
322 element.setUserInput(value);
323 this._changeFieldState(fieldDetail, FIELD_STATES.AUTO_FILLED);
325 } else if (ChromeUtils.getClassName(element) === "HTMLSelectElement") {
326 let cache = this._cacheValue.matchingSelectOption.get(element) || {};
327 let option = cache[value] && cache[value].get();
331 // Do not change value or dispatch events if the option is already selected.
332 // Use case for multiple select is not considered here.
333 if (!option.selected) {
334 option.selected = true;
335 element.dispatchEvent(
336 new element.ownerGlobal.Event("input", { bubbles: true })
338 element.dispatchEvent(
339 new element.ownerGlobal.Event("change", { bubbles: true })
342 // Autofill highlight appears regardless if value is changed or not
343 this._changeFieldState(fieldDetail, FIELD_STATES.AUTO_FILLED);
349 * Populates result to the preview layers with given profile.
351 * @param {Object} profile
352 * A profile to be previewed with
354 previewFormFields(profile) {
355 log.debug("preview profile: ", profile);
357 this.preparePreviewProfile(profile);
359 for (let fieldDetail of this.fieldDetails) {
360 let element = fieldDetail.elementWeakRef.get();
361 let value = profile[fieldDetail.fieldName] || "";
363 // Skip the field that is null
368 if (ChromeUtils.getClassName(element) === "HTMLSelectElement") {
369 // Unlike text input, select element is always previewed even if
370 // the option is already selected.
372 let cache = this._cacheValue.matchingSelectOption.get(element) || {};
373 let option = cache[value] && cache[value].get();
375 value = option.text || "";
380 } else if (element.value) {
381 // Skip the field if it already has text entered.
384 element.previewValue = value;
385 this._changeFieldState(
387 value ? FIELD_STATES.PREVIEW : FIELD_STATES.NORMAL
393 * Clear preview text and background highlight of all fields.
395 clearPreviewedFormFields() {
396 log.debug("clear previewed fields in:", this.form);
398 for (let fieldDetail of this.fieldDetails) {
399 let element = fieldDetail.elementWeakRef.get();
401 log.warn(fieldDetail.fieldName, "is unreachable");
405 element.previewValue = "";
407 // We keep the state if this field has
408 // already been auto-filled.
409 if (fieldDetail.state == FIELD_STATES.AUTO_FILLED) {
413 this._changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
418 * Clear value and highlight style of all filled fields.
420 clearPopulatedForm() {
421 for (let fieldDetail of this.fieldDetails) {
422 let element = fieldDetail.elementWeakRef.get();
424 log.warn(fieldDetail.fieldName, "is unreachable");
428 // Only reset value for input element.
430 fieldDetail.state == FIELD_STATES.AUTO_FILLED &&
431 ChromeUtils.getClassName(element) === "HTMLInputElement"
433 element.setUserInput("");
439 * Change the state of a field to correspond with different presentations.
441 * @param {Object} fieldDetail
442 * A fieldDetail of which its element is about to update the state.
443 * @param {string} nextState
444 * Used to determine the next state
446 _changeFieldState(fieldDetail, nextState) {
447 let element = fieldDetail.elementWeakRef.get();
450 log.warn(fieldDetail.fieldName, "is unreachable while changing state");
453 if (!(nextState in this._FIELD_STATE_ENUM)) {
455 fieldDetail.fieldName,
456 "is trying to change to an invalid state"
460 if (fieldDetail.state == nextState) {
464 for (let [state, mmStateValue] of Object.entries(this._FIELD_STATE_ENUM)) {
465 // The NORMAL state is simply the absence of other manually
466 // managed states so we never need to add or remove it.
471 if (state == nextState) {
472 this.winUtils.addManuallyManagedState(element, mmStateValue);
474 this.winUtils.removeManuallyManagedState(element, mmStateValue);
479 case FIELD_STATES.NORMAL: {
480 if (fieldDetail.state == FIELD_STATES.AUTO_FILLED) {
481 element.removeEventListener("input", this, { mozSystemGroup: true });
485 case FIELD_STATES.AUTO_FILLED: {
486 element.addEventListener("input", this, { mozSystemGroup: true });
491 fieldDetail.state = nextState;
495 for (let fieldDetail of this.fieldDetails) {
496 const element = fieldDetail.elementWeakRef.get();
497 element.removeEventListener("input", this, { mozSystemGroup: true });
498 this._changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
500 this.filledRecordGUID = null;
504 return !!this.filledRecordGUID;
508 * Return the record that is converted from `fieldDetails` and only valid
509 * form record is included.
511 * @returns {Object|null}
512 * A record object consists of three properties:
513 * - guid: The id of the previously-filled profile or null if omitted.
514 * - record: A valid record converted from details with trimmed result.
515 * - untouchedFields: Fields that aren't touched after autofilling.
516 * Return `null` for any uncreatable or invalid record.
519 let details = this.fieldDetails;
520 if (!this.isEnabled() || !details || details.length == 0) {
525 guid: this.filledRecordGUID,
530 details.forEach(detail => {
531 let element = detail.elementWeakRef.get();
532 // Remove the unnecessary spaces
533 let value = element && element.value.trim();
534 value = this.computeFillingValue(value, detail, element);
536 if (!value || value.length > FormAutofillUtils.MAX_FIELD_VALUE_LENGTH) {
537 // Keep the property and preserve more information for updating
538 data.record[detail.fieldName] = "";
542 data.record[detail.fieldName] = value;
544 if (detail.state == FIELD_STATES.AUTO_FILLED) {
545 data.untouchedFields.push(detail.fieldName);
549 this.normalizeCreatingRecord(data);
551 if (!this.isRecordCreatable(data.record)) {
559 switch (event.type) {
561 if (!event.isTrusted) {
564 const target = event.target;
565 const targetFieldDetail = this.getFieldDetailByElement(target);
567 this._changeFieldState(targetFieldDetail, FIELD_STATES.NORMAL);
569 let isAutofilled = false;
570 let dimFieldDetails = [];
571 for (const fieldDetail of this.fieldDetails) {
572 const element = fieldDetail.elementWeakRef.get();
574 if (ChromeUtils.getClassName(element) === "HTMLSelectElement") {
575 // Dim fields are those we don't attempt to revert their value
576 // when clear the target set, such as <select>.
577 dimFieldDetails.push(fieldDetail);
579 isAutofilled |= fieldDetail.state == FIELD_STATES.AUTO_FILLED;
583 // Restore the dim fields to initial state as well once we knew
584 // that user had intention to clear the filled form manually.
585 for (const fieldDetail of dimFieldDetails) {
586 this._changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
588 this.filledRecordGUID = null;
596 class FormAutofillAddressSection extends FormAutofillSection {
597 constructor(fieldDetails, winUtils) {
598 super(fieldDetails, winUtils);
600 this._cacheValue.oneLineStreetAddress = null;
605 this.fieldDetails.length >= FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD
610 return FormAutofill.isAutofillAddressesEnabled;
613 isRecordCreatable(record) {
616 !FormAutofill.supportedCountries.includes(record.country)
618 // We don't want to save data in the wrong fields due to not having proper
619 // heuristic regexes in countries we don't yet support.
620 log.warn("isRecordCreatable: Country not supported:", record.country);
626 for (let key of Object.keys(record)) {
630 if (FormAutofillUtils.getCategoryFromFieldName(key) == "name") {
636 return length + hasName >= FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD;
639 _getOneLineStreetAddress(address) {
640 if (!this._cacheValue.oneLineStreetAddress) {
641 this._cacheValue.oneLineStreetAddress = {};
643 if (!this._cacheValue.oneLineStreetAddress[address]) {
644 this._cacheValue.oneLineStreetAddress[
646 ] = FormAutofillUtils.toOneLineAddress(address);
648 return this._cacheValue.oneLineStreetAddress[address];
651 addressTransformer(profile) {
652 if (profile["street-address"]) {
653 // "-moz-street-address-one-line" is used by the labels in
654 // ProfileAutoCompleteResult.
655 profile["-moz-street-address-one-line"] = this._getOneLineStreetAddress(
656 profile["street-address"]
658 let streetAddressDetail = this.getFieldDetailByName("street-address");
660 streetAddressDetail &&
661 ChromeUtils.getClassName(streetAddressDetail.elementWeakRef.get()) ===
664 profile["street-address"] = profile["-moz-street-address-one-line"];
667 let waitForConcat = [];
668 for (let f of ["address-line3", "address-line2", "address-line1"]) {
669 waitForConcat.unshift(profile[f]);
670 if (this.getFieldDetailByName(f)) {
671 if (waitForConcat.length > 1) {
672 profile[f] = FormAutofillUtils.toOneLineAddress(waitForConcat);
681 * Replace tel with tel-national if tel violates the input element's
683 * @param {Object} profile
684 * A profile to be converted.
686 telTransformer(profile) {
687 if (!profile.tel || !profile["tel-national"]) {
691 let detail = this.getFieldDetailByName("tel");
696 let element = detail.elementWeakRef.get();
698 let testPattern = str => {
700 // The pattern has to match the entire value.
701 _pattern = new RegExp("^(?:" + element.pattern + ")$", "u");
703 return _pattern.test(str);
705 if (element.pattern) {
706 if (testPattern(profile.tel)) {
709 } else if (element.maxLength) {
711 detail._reason == "autocomplete" &&
712 profile.tel.length <= element.maxLength
718 if (detail._reason != "autocomplete") {
719 // Since we only target people living in US and using en-US websites in
720 // MVP, it makes more sense to fill `tel-national` instead of `tel`
721 // if the field is identified by heuristics and no other clues to
722 // determine which one is better.
723 // TODO: [Bug 1407545] This should be improved once more countries are
725 profile.tel = profile["tel-national"];
726 } else if (element.pattern) {
727 if (testPattern(profile["tel-national"])) {
728 profile.tel = profile["tel-national"];
730 } else if (element.maxLength) {
731 if (profile["tel-national"].length <= element.maxLength) {
732 profile.tel = profile["tel-national"];
738 * Apply all address related transformers.
740 * @param {Object} profile
741 * A profile for adjusting address related value.
744 applyTransformers(profile) {
745 this.addressTransformer(profile);
746 this.telTransformer(profile);
747 this.matchSelectOptions(profile);
748 this.adaptFieldMaxLength(profile);
751 computeFillingValue(value, fieldDetail, element) {
752 // Try to abbreviate the value of select element.
754 fieldDetail.fieldName == "address-level1" &&
755 ChromeUtils.getClassName(element) === "HTMLSelectElement"
757 // Don't save the record when the option value is empty *OR* there
758 // are multiple options being selected. The empty option is usually
759 // assumed to be default along with a meaningless text to users.
760 if (!value || element.selectedOptions.length != 1) {
761 // Keep the property and preserve more information for address updating
764 let text = element.selectedOptions[0].text.trim();
766 FormAutofillUtils.getAbbreviatedSubregionName([value, text]) || text;
772 normalizeCreatingRecord(address) {
778 if (address.record.country) {
779 let detail = this.getFieldDetailByName("country");
780 // Try identifying country field aggressively if it doesn't come from
782 if (detail._reason != "autocomplete") {
783 let countryCode = FormAutofillUtils.identifyCountryCode(
784 address.record.country
787 address.record.country = countryCode;
793 FormAutofillUtils.compressTel(address.record);
794 if (address.record.tel) {
795 let allTelComponentsAreUntouched = Object.keys(address.record)
797 field => FormAutofillUtils.getCategoryFromFieldName(field) == "tel"
799 .every(field => address.untouchedFields.includes(field));
800 if (allTelComponentsAreUntouched) {
801 // No need to verify it if none of related fields are modified after autofilling.
802 if (!address.untouchedFields.includes("tel")) {
803 address.untouchedFields.push("tel");
806 let strippedNumber = address.record.tel.replace(/[\s\(\)-]/g, "");
808 // Remove "tel" if it contains invalid characters or the length of its
809 // number part isn't between 5 and 15.
810 // (The maximum length of a valid number in E.164 format is 15 digits
811 // according to https://en.wikipedia.org/wiki/E.164 )
812 if (!/^(\+?)[\da-zA-Z]{5,15}$/.test(strippedNumber)) {
813 address.record.tel = "";
820 class FormAutofillCreditCardSection extends FormAutofillSection {
821 constructor(fieldDetails, winUtils) {
822 super(fieldDetails, winUtils);
826 let ccNumberReason = "";
827 let hasCCNumber = false;
828 let hasExpiryDate = false;
829 let hasCCName = false;
831 for (let detail of this.fieldDetails) {
832 switch (detail.fieldName) {
835 ccNumberReason = detail._reason;
838 case "cc-given-name":
839 case "cc-additional-name":
840 case "cc-family-name":
846 hasExpiryDate = true;
853 (ccNumberReason == "autocomplete" || hasExpiryDate || hasCCName)
858 return FormAutofill.isAutofillCreditCardsEnabled;
861 isRecordCreatable(record) {
863 record["cc-number"] && FormAutofillUtils.isCCNumber(record["cc-number"])
867 creditCardExpDateTransformer(profile) {
868 if (!profile["cc-exp"]) {
872 let detail = this.getFieldDetailByName("cc-exp");
877 let element = detail.elementWeakRef.get();
878 if (element.tagName != "INPUT" || !element.placeholder) {
883 ccExpMonth = profile["cc-exp-month"],
884 ccExpYear = profile["cc-exp-year"],
885 placeholder = element.placeholder;
887 result = /(?:[^m]|\b)(m{1,2})\s*([-/\\]*)\s*(y{2,4})(?!y)/i.exec(
892 String(ccExpMonth).padStart(result[1].length, "0") +
894 String(ccExpYear).substr(-1 * result[3].length);
898 result = /(?:[^y]|\b)(y{2,4})\s*([-/\\]*)\s*(m{1,2})(?!m)/i.exec(
903 String(ccExpYear).substr(-1 * result[1].length) +
905 String(ccExpMonth).padStart(result[3].length, "0");
909 async _decrypt(cipherText, reauth) {
910 return new Promise(resolve => {
911 Services.cpmm.addMessageListener(
912 "FormAutofill:DecryptedString",
913 function getResult(result) {
914 Services.cpmm.removeMessageListener(
915 "FormAutofill:DecryptedString",
918 resolve(result.data);
922 Services.cpmm.sendAsyncMessage("FormAutofill:GetDecryptedString", {
930 * Apply all credit card related transformers.
932 * @param {Object} profile
933 * A profile for adjusting credit card related value.
936 applyTransformers(profile) {
937 this.matchSelectOptions(profile);
938 this.creditCardExpDateTransformer(profile);
939 this.adaptFieldMaxLength(profile);
943 * Customize for previewing prorifle.
945 * @param {Object} profile
946 * A profile for pre-processing before previewing values.
949 preparePreviewProfile(profile) {
950 // Always show the decrypted credit card number when Master Password is
952 if (profile["cc-number-decrypted"]) {
953 profile["cc-number"] = profile["cc-number-decrypted"];
958 * Customize for filling prorifle.
960 * @param {Object} profile
961 * A profile for pre-processing before filling values.
962 * @returns {boolean} Whether the profile should be filled.
965 async prepareFillingProfile(profile) {
966 // Prompt the OS login dialog to get the decrypted credit
968 if (profile["cc-number-encrypted"]) {
969 let decrypted = await this._decrypt(
970 profile["cc-number-encrypted"],
971 reauthPasswordPromptMessage
975 // Early return if the decrypted is empty or undefined
979 profile["cc-number"] = decrypted;
986 * Handles profile autofill for a DOM Form element.
988 class FormAutofillHandler {
990 * Initialize the form from `FormLike` object to handle the section or form
992 * @param {FormLike} form Form that need to be auto filled
995 this._updateForm(form);
998 * A WindowUtils reference of which Window the form belongs
1000 this.winUtils = this.form.rootElement.ownerGlobal.windowUtils;
1003 * Time in milliseconds since epoch when a user started filling in the form.
1005 this.timeStartedFillingMS = null;
1008 set focusedInput(element) {
1009 let section = this._sectionCache.get(element);
1011 section = this.sections.find(s => s.getFieldDetailByElement(element));
1012 this._sectionCache.set(element, section);
1015 this._focusedSection = section;
1018 section.focusedInput = element;
1022 get activeSection() {
1023 return this._focusedSection;
1027 * Check the form is necessary to be updated. This function should be able to
1028 * detect any changes including all control elements in the form.
1029 * @param {HTMLElement} element The element supposed to be in the form.
1030 * @returns {boolean} FormAutofillHandler.form is updated or not.
1032 updateFormIfNeeded(element) {
1033 // When the following condition happens, FormAutofillHandler.form should be
1035 // * The count of form controls is changed.
1036 // * When the element can not be found in the current form.
1038 // However, we should improve the function to detect the element changes.
1039 // e.g. a tel field is changed from type="hidden" to type="tel".
1042 let getFormLike = () => {
1044 _formLike = FormLikeFactory.createFromField(element);
1049 let currentForm = element.form;
1051 currentForm = getFormLike();
1054 if (currentForm.elements.length != this.form.elements.length) {
1055 log.debug("The count of form elements is changed.");
1056 this._updateForm(getFormLike());
1060 if (!this.form.elements.includes(element)) {
1061 log.debug("The element can not be found in the current form.");
1062 this._updateForm(getFormLike());
1070 * Update the form with a new FormLike, and the related fields should be
1071 * updated or clear to ensure the data consistency.
1072 * @param {FormLike} form a new FormLike to replace the original one.
1076 * DOM Form element to which this object is attached.
1081 * Array of collected data about relevant form fields. Each item is an object
1082 * storing the identifying details of the field and a reference to the
1083 * originally associated element from the form.
1085 * The "section", "addressType", "contactType", and "fieldName" values are
1086 * used to identify the exact field when the serializable data is received
1087 * from the backend. There cannot be multiple fields which have
1088 * the same exact combination of these values.
1090 * A direct reference to the associated element cannot be sent to the user
1091 * interface because processing may be done in the parent process.
1093 this.fieldDetails = null;
1096 this._sectionCache = new WeakMap();
1100 * Set fieldDetails from the form about fields that can be autofilled.
1102 * @param {boolean} allowDuplicates
1103 * true to remain any duplicated field details otherwise to remove the
1105 * @returns {Array} The valid address and credit card details.
1107 collectFormFields(allowDuplicates = false) {
1108 let sections = FormAutofillHeuristics.getFormInfo(
1112 let allValidDetails = [];
1113 for (let { fieldDetails, type } of sections) {
1115 if (type == FormAutofillUtils.SECTION_TYPES.ADDRESS) {
1116 section = new FormAutofillAddressSection(fieldDetails, this.winUtils);
1117 } else if (type == FormAutofillUtils.SECTION_TYPES.CREDIT_CARD) {
1118 section = new FormAutofillCreditCardSection(
1123 throw new Error("Unknown field type.");
1125 this.sections.push(section);
1126 allValidDetails.push(...section.fieldDetails);
1129 for (let detail of allValidDetails) {
1130 let input = detail.elementWeakRef.get();
1134 input.addEventListener("input", this, { mozSystemGroup: true });
1137 this.fieldDetails = allValidDetails;
1138 return allValidDetails;
1141 _hasFilledSection() {
1142 return this.sections.some(section => section.isFilled());
1146 * Processes form fields that can be autofilled, and populates them with the
1147 * profile provided by backend.
1149 * @param {Object} profile
1150 * A profile to be filled in.
1152 async autofillFormFields(profile) {
1153 let noFilledSectionsPreviously = !this._hasFilledSection();
1154 await this.activeSection.autofillFields(profile);
1156 const onChangeHandler = e => {
1160 if (e.type == "reset") {
1161 for (let section of this.sections) {
1162 section.resetFieldStates();
1165 // Unregister listeners once no field is in AUTO_FILLED state.
1166 if (!this._hasFilledSection()) {
1167 this.form.rootElement.removeEventListener("input", onChangeHandler, {
1168 mozSystemGroup: true,
1170 this.form.rootElement.removeEventListener("reset", onChangeHandler, {
1171 mozSystemGroup: true,
1176 if (noFilledSectionsPreviously) {
1177 // Handle the highlight style resetting caused by user's correction afterward.
1178 log.debug("register change handler for filled form:", this.form);
1179 this.form.rootElement.addEventListener("input", onChangeHandler, {
1180 mozSystemGroup: true,
1182 this.form.rootElement.addEventListener("reset", onChangeHandler, {
1183 mozSystemGroup: true,
1188 handleEvent(event) {
1189 switch (event.type) {
1191 if (!event.isTrusted) {
1195 for (let detail of this.fieldDetails) {
1196 let input = detail.elementWeakRef.get();
1200 input.removeEventListener("input", this, { mozSystemGroup: true });
1202 this.timeStartedFillingMS = Date.now();
1208 * Collect the filled sections within submitted form and convert all the valid
1209 * field data into multiple records.
1211 * @returns {Object} records
1212 * {Array.<Object>} records.address
1213 * {Array.<Object>} records.creditCard
1221 for (const section of this.sections) {
1222 const secRecord = section.createRecord();
1226 if (section instanceof FormAutofillAddressSection) {
1227 records.address.push(secRecord);
1228 } else if (section instanceof FormAutofillCreditCardSection) {
1229 records.creditCard.push(secRecord);
1231 throw new Error("Unknown section type");
1234 log.debug("Create records:", records);