Bug 1564761 [wpt PR 17620] - Document::CheckComplete should be nop when called from...
[gecko.git] / browser / extensions / formautofill / FormAutofillHandler.jsm
blobd350578e1d65f35d37064eb98c66ae47f7f617d5
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/. */
5 /*
6  * Defines a handler object to represent forms that autofill can handle.
7  */
9 "use strict";
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(
25   this,
26   "FormAutofillUtils",
27   "resource://formautofill/FormAutofillUtils.jsm"
29 ChromeUtils.defineModuleGetter(
30   this,
31   "FormAutofillHeuristics",
32   "resource://formautofill/FormAutofillHeuristics.jsm"
34 ChromeUtils.defineModuleGetter(
35   this,
36   "FormLikeFactory",
37   "resource://gre/modules/FormLikeFactory.jsm"
40 XPCOMUtils.defineLazyGetter(this, "reauthPasswordPromptMessage", () => {
41   const brandShortName = FormAutofillUtils.brandBundle.GetStringFromName(
42     "brandShortName"
43   );
44   return FormAutofillUtils.stringBundle.formatStringFromName(
45     `useCreditCardPasswordPrompt.${AppConstants.platform}`,
46     [brandShortName]
47   );
48 });
50 this.log = null;
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;
61     /**
62      * Enum for form autofill MANUALLY_MANAGED_STATES values
63      */
64     this._FIELD_STATE_ENUM = {
65       // not themed
66       [FIELD_STATES.NORMAL]: null,
67       // highlighted
68       [FIELD_STATES.AUTO_FILLED]: "-moz-autofill",
69       // highlighted && grey color text
70       [FIELD_STATES.PREVIEW]: "-moz-autofill-preview",
71     };
73     if (!this.isValidSection()) {
74       this.fieldDetails = [];
75       log.debug(
76         `Ignoring ${
77           this.constructor.name
78         } related fields since it is an invalid section`
79       );
80     }
82     this._cacheValue = {
83       allFieldNames: null,
84       matchingSelectOption: null,
85     };
86   }
88   /*
89    * Examine the section is a valid section or not based on its fieldDetails or
90    * other information. This method must be overrided.
91    *
92    * @returns {boolean} True for a valid section, otherwise false
93    *
94    */
95   isValidSection() {
96     throw new TypeError("isValidSection method must be overrided");
97   }
99   /*
100    * Examine the section is an enabled section type or not based on its
101    * preferences. This method must be overrided.
102    *
103    * @returns {boolean} True for an enabled section type, otherwise false
104    *
105    */
106   isEnabled() {
107     throw new TypeError("isEnabled method must be overrided");
108   }
110   /*
111    * Examine the section is createable for storing the profile. This method
112    * must be overrided.
113    *
114    * @param {Object} record The record for examining createable
115    * @returns {boolean} True for the record is createable, otherwise false
116    *
117    */
118   isRecordCreatable(record) {
119     throw new TypeError("isRecordCreatable method must be overrided");
120   }
122   /**
123    * Override this method if the profile is needed to apply some transformers.
124    *
125    * @param {Object} profile
126    *        A profile should be converted based on the specific requirement.
127    */
128   applyTransformers(profile) {}
130   /**
131    * Override this method if the profile is needed to be customized for
132    * previewing values.
133    *
134    * @param {Object} profile
135    *        A profile for pre-processing before previewing values.
136    */
137   preparePreviewProfile(profile) {}
139   /**
140    * Override this method if the profile is needed to be customized for filling
141    * values.
142    *
143    * @param {Object} profile
144    *        A profile for pre-processing before filling values.
145    * @returns {boolean} Whether the profile should be filled.
146    */
147   async prepareFillingProfile(profile) {
148     return true;
149   }
151   /*
152    * Override this methid if any data for `createRecord` is needed to be
153    * normailized before submitting the record.
154    *
155    * @param {Object} profile
156    *        A record for normalization.
157    */
158   normalizeCreatingRecord(data) {}
160   /*
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.
169    *
170    * @returns {String}
171    *          A string of the converted value.
172    */
173   computeFillingValue(value, fieldName, element) {
174     return value;
175   }
177   set focusedInput(element) {
178     this._focusedDetail = this.getFieldDetailByElement(element);
179   }
181   getFieldDetailByElement(element) {
182     return this.fieldDetails.find(
183       detail => detail.elementWeakRef.get() == element
184     );
185   }
187   get allFieldNames() {
188     if (!this._cacheValue.allFieldNames) {
189       this._cacheValue.allFieldNames = this.fieldDetails.map(
190         record => record.fieldName
191       );
192     }
193     return this._cacheValue.allFieldNames;
194   }
196   getFieldDetailByName(fieldName) {
197     return this.fieldDetails.find(detail => detail.fieldName == fieldName);
198   }
200   matchSelectOptions(profile) {
201     if (!this._cacheValue.matchingSelectOption) {
202       this._cacheValue.matchingSelectOption = new WeakMap();
203     }
205     for (let fieldName in profile) {
206       let fieldDetail = this.getFieldDetailByName(fieldName);
207       if (!fieldDetail) {
208         continue;
209       }
211       let element = fieldDetail.elementWeakRef.get();
212       if (ChromeUtils.getClassName(element) !== "HTMLSelectElement") {
213         continue;
214       }
216       let cache = this._cacheValue.matchingSelectOption.get(element) || {};
217       let value = profile[fieldName];
218       if (cache[value] && cache[value].get()) {
219         continue;
220       }
222       let option = FormAutofillUtils.findSelectOption(
223         element,
224         profile,
225         fieldName
226       );
227       if (option) {
228         cache[value] = Cu.getWeakReference(option);
229         this._cacheValue.matchingSelectOption.set(element, cache);
230       } else {
231         if (cache[value]) {
232           delete cache[value];
233           this._cacheValue.matchingSelectOption.set(element, cache);
234         }
235         // Delete the field so the phishing hint won't treat it as a "also fill"
236         // field.
237         delete profile[fieldName];
238       }
239     }
240   }
242   adaptFieldMaxLength(profile) {
243     for (let key in profile) {
244       let detail = this.getFieldDetailByName(key);
245       if (!detail) {
246         continue;
247       }
249       let element = detail.elementWeakRef.get();
250       if (!element) {
251         continue;
252       }
254       let maxLength = element.maxLength;
255       if (
256         maxLength === undefined ||
257         maxLength < 0 ||
258         profile[key].length <= maxLength
259       ) {
260         continue;
261       }
263       if (maxLength) {
264         profile[key] = profile[key].substr(0, maxLength);
265       } else {
266         delete profile[key];
267       }
268     }
269   }
271   getAdaptedProfiles(originalProfiles) {
272     for (let profile of originalProfiles) {
273       this.applyTransformers(profile);
274     }
275     return originalProfiles;
276   }
278   /**
279    * Processes form fields that can be autofilled, and populates them with the
280    * profile provided by backend.
281    *
282    * @param {Object} profile
283    *        A profile to be filled in.
284    */
285   async autofillFields(profile) {
286     let focusedDetail = this._focusedDetail;
287     if (!focusedDetail) {
288       throw new Error("No fieldDetail for the focused input.");
289     }
291     if (!(await this.prepareFillingProfile(profile))) {
292       log.debug("profile cannot be filled", profile);
293       return;
294     }
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();
305       if (!element) {
306         continue;
307       }
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
314         // anyway.
315         // For the others, the fields should be only filled when their values
316         // are empty.
317         let focusedInput = focusedDetail.elementWeakRef.get();
318         if (
319           element == focusedInput ||
320           (element != focusedInput && !element.value)
321         ) {
322           element.setUserInput(value);
323           this._changeFieldState(fieldDetail, FIELD_STATES.AUTO_FILLED);
324         }
325       } else if (ChromeUtils.getClassName(element) === "HTMLSelectElement") {
326         let cache = this._cacheValue.matchingSelectOption.get(element) || {};
327         let option = cache[value] && cache[value].get();
328         if (!option) {
329           continue;
330         }
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 })
337           );
338           element.dispatchEvent(
339             new element.ownerGlobal.Event("change", { bubbles: true })
340           );
341         }
342         // Autofill highlight appears regardless if value is changed or not
343         this._changeFieldState(fieldDetail, FIELD_STATES.AUTO_FILLED);
344       }
345     }
346   }
348   /**
349    * Populates result to the preview layers with given profile.
350    *
351    * @param {Object} profile
352    *        A profile to be previewed with
353    */
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
364       if (!element) {
365         continue;
366       }
368       if (ChromeUtils.getClassName(element) === "HTMLSelectElement") {
369         // Unlike text input, select element is always previewed even if
370         // the option is already selected.
371         if (value) {
372           let cache = this._cacheValue.matchingSelectOption.get(element) || {};
373           let option = cache[value] && cache[value].get();
374           if (option) {
375             value = option.text || "";
376           } else {
377             value = "";
378           }
379         }
380       } else if (element.value) {
381         // Skip the field if it already has text entered.
382         continue;
383       }
384       element.previewValue = value;
385       this._changeFieldState(
386         fieldDetail,
387         value ? FIELD_STATES.PREVIEW : FIELD_STATES.NORMAL
388       );
389     }
390   }
392   /**
393    * Clear preview text and background highlight of all fields.
394    */
395   clearPreviewedFormFields() {
396     log.debug("clear previewed fields in:", this.form);
398     for (let fieldDetail of this.fieldDetails) {
399       let element = fieldDetail.elementWeakRef.get();
400       if (!element) {
401         log.warn(fieldDetail.fieldName, "is unreachable");
402         continue;
403       }
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) {
410         continue;
411       }
413       this._changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
414     }
415   }
417   /**
418    * Clear value and highlight style of all filled fields.
419    */
420   clearPopulatedForm() {
421     for (let fieldDetail of this.fieldDetails) {
422       let element = fieldDetail.elementWeakRef.get();
423       if (!element) {
424         log.warn(fieldDetail.fieldName, "is unreachable");
425         continue;
426       }
428       // Only reset value for input element.
429       if (
430         fieldDetail.state == FIELD_STATES.AUTO_FILLED &&
431         ChromeUtils.getClassName(element) === "HTMLInputElement"
432       ) {
433         element.setUserInput("");
434       }
435     }
436   }
438   /**
439    * Change the state of a field to correspond with different presentations.
440    *
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
445    */
446   _changeFieldState(fieldDetail, nextState) {
447     let element = fieldDetail.elementWeakRef.get();
449     if (!element) {
450       log.warn(fieldDetail.fieldName, "is unreachable while changing state");
451       return;
452     }
453     if (!(nextState in this._FIELD_STATE_ENUM)) {
454       log.warn(
455         fieldDetail.fieldName,
456         "is trying to change to an invalid state"
457       );
458       return;
459     }
460     if (fieldDetail.state == nextState) {
461       return;
462     }
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.
467       if (!mmStateValue) {
468         continue;
469       }
471       if (state == nextState) {
472         this.winUtils.addManuallyManagedState(element, mmStateValue);
473       } else {
474         this.winUtils.removeManuallyManagedState(element, mmStateValue);
475       }
476     }
478     switch (nextState) {
479       case FIELD_STATES.NORMAL: {
480         if (fieldDetail.state == FIELD_STATES.AUTO_FILLED) {
481           element.removeEventListener("input", this, { mozSystemGroup: true });
482         }
483         break;
484       }
485       case FIELD_STATES.AUTO_FILLED: {
486         element.addEventListener("input", this, { mozSystemGroup: true });
487         break;
488       }
489     }
491     fieldDetail.state = nextState;
492   }
494   resetFieldStates() {
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);
499     }
500     this.filledRecordGUID = null;
501   }
503   isFilled() {
504     return !!this.filledRecordGUID;
505   }
507   /**
508    * Return the record that is converted from `fieldDetails` and only valid
509    * form record is included.
510    *
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.
517    */
518   createRecord() {
519     let details = this.fieldDetails;
520     if (!this.isEnabled() || !details || details.length == 0) {
521       return null;
522     }
524     let data = {
525       guid: this.filledRecordGUID,
526       record: {},
527       untouchedFields: [],
528     };
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] = "";
539         return;
540       }
542       data.record[detail.fieldName] = value;
544       if (detail.state == FIELD_STATES.AUTO_FILLED) {
545         data.untouchedFields.push(detail.fieldName);
546       }
547     });
549     this.normalizeCreatingRecord(data);
551     if (!this.isRecordCreatable(data.record)) {
552       return null;
553     }
555     return data;
556   }
558   handleEvent(event) {
559     switch (event.type) {
560       case "input": {
561         if (!event.isTrusted) {
562           return;
563         }
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);
578           } else {
579             isAutofilled |= fieldDetail.state == FIELD_STATES.AUTO_FILLED;
580           }
581         }
582         if (!isAutofilled) {
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);
587           }
588           this.filledRecordGUID = null;
589         }
590         break;
591       }
592     }
593   }
596 class FormAutofillAddressSection extends FormAutofillSection {
597   constructor(fieldDetails, winUtils) {
598     super(fieldDetails, winUtils);
600     this._cacheValue.oneLineStreetAddress = null;
601   }
603   isValidSection() {
604     return (
605       this.fieldDetails.length >= FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD
606     );
607   }
609   isEnabled() {
610     return FormAutofill.isAutofillAddressesEnabled;
611   }
613   isRecordCreatable(record) {
614     if (
615       record.country &&
616       !FormAutofill.supportedCountries.includes(record.country)
617     ) {
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);
621       return false;
622     }
624     let hasName = 0;
625     let length = 0;
626     for (let key of Object.keys(record)) {
627       if (!record[key]) {
628         continue;
629       }
630       if (FormAutofillUtils.getCategoryFromFieldName(key) == "name") {
631         hasName = 1;
632         continue;
633       }
634       length++;
635     }
636     return length + hasName >= FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD;
637   }
639   _getOneLineStreetAddress(address) {
640     if (!this._cacheValue.oneLineStreetAddress) {
641       this._cacheValue.oneLineStreetAddress = {};
642     }
643     if (!this._cacheValue.oneLineStreetAddress[address]) {
644       this._cacheValue.oneLineStreetAddress[
645         address
646       ] = FormAutofillUtils.toOneLineAddress(address);
647     }
648     return this._cacheValue.oneLineStreetAddress[address];
649   }
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"]
657       );
658       let streetAddressDetail = this.getFieldDetailByName("street-address");
659       if (
660         streetAddressDetail &&
661         ChromeUtils.getClassName(streetAddressDetail.elementWeakRef.get()) ===
662           "HTMLInputElement"
663       ) {
664         profile["street-address"] = profile["-moz-street-address-one-line"];
665       }
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);
673           }
674           waitForConcat = [];
675         }
676       }
677     }
678   }
680   /**
681    * Replace tel with tel-national if tel violates the input element's
682    * restriction.
683    * @param {Object} profile
684    *        A profile to be converted.
685    */
686   telTransformer(profile) {
687     if (!profile.tel || !profile["tel-national"]) {
688       return;
689     }
691     let detail = this.getFieldDetailByName("tel");
692     if (!detail) {
693       return;
694     }
696     let element = detail.elementWeakRef.get();
697     let _pattern;
698     let testPattern = str => {
699       if (!_pattern) {
700         // The pattern has to match the entire value.
701         _pattern = new RegExp("^(?:" + element.pattern + ")$", "u");
702       }
703       return _pattern.test(str);
704     };
705     if (element.pattern) {
706       if (testPattern(profile.tel)) {
707         return;
708       }
709     } else if (element.maxLength) {
710       if (
711         detail._reason == "autocomplete" &&
712         profile.tel.length <= element.maxLength
713       ) {
714         return;
715       }
716     }
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
724       // supported.
725       profile.tel = profile["tel-national"];
726     } else if (element.pattern) {
727       if (testPattern(profile["tel-national"])) {
728         profile.tel = profile["tel-national"];
729       }
730     } else if (element.maxLength) {
731       if (profile["tel-national"].length <= element.maxLength) {
732         profile.tel = profile["tel-national"];
733       }
734     }
735   }
737   /*
738    * Apply all address related transformers.
739    *
740    * @param {Object} profile
741    *        A profile for adjusting address related value.
742    * @override
743    */
744   applyTransformers(profile) {
745     this.addressTransformer(profile);
746     this.telTransformer(profile);
747     this.matchSelectOptions(profile);
748     this.adaptFieldMaxLength(profile);
749   }
751   computeFillingValue(value, fieldDetail, element) {
752     // Try to abbreviate the value of select element.
753     if (
754       fieldDetail.fieldName == "address-level1" &&
755       ChromeUtils.getClassName(element) === "HTMLSelectElement"
756     ) {
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
762         value = "";
763       } else {
764         let text = element.selectedOptions[0].text.trim();
765         value =
766           FormAutofillUtils.getAbbreviatedSubregionName([value, text]) || text;
767       }
768     }
769     return value;
770   }
772   normalizeCreatingRecord(address) {
773     if (!address) {
774       return;
775     }
777     // Normalize Country
778     if (address.record.country) {
779       let detail = this.getFieldDetailByName("country");
780       // Try identifying country field aggressively if it doesn't come from
781       // @autocomplete.
782       if (detail._reason != "autocomplete") {
783         let countryCode = FormAutofillUtils.identifyCountryCode(
784           address.record.country
785         );
786         if (countryCode) {
787           address.record.country = countryCode;
788         }
789       }
790     }
792     // Normalize Tel
793     FormAutofillUtils.compressTel(address.record);
794     if (address.record.tel) {
795       let allTelComponentsAreUntouched = Object.keys(address.record)
796         .filter(
797           field => FormAutofillUtils.getCategoryFromFieldName(field) == "tel"
798         )
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");
804         }
805       } else {
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 = "";
814         }
815       }
816     }
817   }
820 class FormAutofillCreditCardSection extends FormAutofillSection {
821   constructor(fieldDetails, winUtils) {
822     super(fieldDetails, winUtils);
823   }
825   isValidSection() {
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) {
833         case "cc-number":
834           hasCCNumber = true;
835           ccNumberReason = detail._reason;
836           break;
837         case "cc-name":
838         case "cc-given-name":
839         case "cc-additional-name":
840         case "cc-family-name":
841           hasCCName = true;
842           break;
843         case "cc-exp":
844         case "cc-exp-month":
845         case "cc-exp-year":
846           hasExpiryDate = true;
847           break;
848       }
849     }
851     return (
852       hasCCNumber &&
853       (ccNumberReason == "autocomplete" || hasExpiryDate || hasCCName)
854     );
855   }
857   isEnabled() {
858     return FormAutofill.isAutofillCreditCardsEnabled;
859   }
861   isRecordCreatable(record) {
862     return (
863       record["cc-number"] && FormAutofillUtils.isCCNumber(record["cc-number"])
864     );
865   }
867   creditCardExpDateTransformer(profile) {
868     if (!profile["cc-exp"]) {
869       return;
870     }
872     let detail = this.getFieldDetailByName("cc-exp");
873     if (!detail) {
874       return;
875     }
877     let element = detail.elementWeakRef.get();
878     if (element.tagName != "INPUT" || !element.placeholder) {
879       return;
880     }
882     let result,
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(
888       placeholder
889     );
890     if (result) {
891       profile["cc-exp"] =
892         String(ccExpMonth).padStart(result[1].length, "0") +
893         result[2] +
894         String(ccExpYear).substr(-1 * result[3].length);
895       return;
896     }
898     result = /(?:[^y]|\b)(y{2,4})\s*([-/\\]*)\s*(m{1,2})(?!m)/i.exec(
899       placeholder
900     );
901     if (result) {
902       profile["cc-exp"] =
903         String(ccExpYear).substr(-1 * result[1].length) +
904         result[2] +
905         String(ccExpMonth).padStart(result[3].length, "0");
906     }
907   }
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",
916             getResult
917           );
918           resolve(result.data);
919         }
920       );
922       Services.cpmm.sendAsyncMessage("FormAutofill:GetDecryptedString", {
923         cipherText,
924         reauth,
925       });
926     });
927   }
929   /*
930    * Apply all credit card related transformers.
931    *
932    * @param {Object} profile
933    *        A profile for adjusting credit card related value.
934    * @override
935    */
936   applyTransformers(profile) {
937     this.matchSelectOptions(profile);
938     this.creditCardExpDateTransformer(profile);
939     this.adaptFieldMaxLength(profile);
940   }
942   /**
943    * Customize for previewing prorifle.
944    *
945    * @param {Object} profile
946    *        A profile for pre-processing before previewing values.
947    * @override
948    */
949   preparePreviewProfile(profile) {
950     // Always show the decrypted credit card number when Master Password is
951     // disabled.
952     if (profile["cc-number-decrypted"]) {
953       profile["cc-number"] = profile["cc-number-decrypted"];
954     }
955   }
957   /**
958    * Customize for filling prorifle.
959    *
960    * @param {Object} profile
961    *        A profile for pre-processing before filling values.
962    * @returns {boolean} Whether the profile should be filled.
963    * @override
964    */
965   async prepareFillingProfile(profile) {
966     // Prompt the OS login dialog to get the decrypted credit
967     // card number.
968     if (profile["cc-number-encrypted"]) {
969       let decrypted = await this._decrypt(
970         profile["cc-number-encrypted"],
971         reauthPasswordPromptMessage
972       );
974       if (!decrypted) {
975         // Early return if the decrypted is empty or undefined
976         return false;
977       }
979       profile["cc-number"] = decrypted;
980     }
981     return true;
982   }
986  * Handles profile autofill for a DOM Form element.
987  */
988 class FormAutofillHandler {
989   /**
990    * Initialize the form from `FormLike` object to handle the section or form
991    * operations.
992    * @param {FormLike} form Form that need to be auto filled
993    */
994   constructor(form) {
995     this._updateForm(form);
997     /**
998      * A WindowUtils reference of which Window the form belongs
999      */
1000     this.winUtils = this.form.rootElement.ownerGlobal.windowUtils;
1002     /**
1003      * Time in milliseconds since epoch when a user started filling in the form.
1004      */
1005     this.timeStartedFillingMS = null;
1006   }
1008   set focusedInput(element) {
1009     let section = this._sectionCache.get(element);
1010     if (!section) {
1011       section = this.sections.find(s => s.getFieldDetailByElement(element));
1012       this._sectionCache.set(element, section);
1013     }
1015     this._focusedSection = section;
1017     if (section) {
1018       section.focusedInput = element;
1019     }
1020   }
1022   get activeSection() {
1023     return this._focusedSection;
1024   }
1026   /**
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.
1031    */
1032   updateFormIfNeeded(element) {
1033     // When the following condition happens, FormAutofillHandler.form should be
1034     // updated:
1035     // * The count of form controls is changed.
1036     // * When the element can not be found in the current form.
1037     //
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".
1041     let _formLike;
1042     let getFormLike = () => {
1043       if (!_formLike) {
1044         _formLike = FormLikeFactory.createFromField(element);
1045       }
1046       return _formLike;
1047     };
1049     let currentForm = element.form;
1050     if (!currentForm) {
1051       currentForm = getFormLike();
1052     }
1054     if (currentForm.elements.length != this.form.elements.length) {
1055       log.debug("The count of form elements is changed.");
1056       this._updateForm(getFormLike());
1057       return true;
1058     }
1060     if (!this.form.elements.includes(element)) {
1061       log.debug("The element can not be found in the current form.");
1062       this._updateForm(getFormLike());
1063       return true;
1064     }
1066     return false;
1067   }
1069   /**
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.
1073    */
1074   _updateForm(form) {
1075     /**
1076      * DOM Form element to which this object is attached.
1077      */
1078     this.form = form;
1080     /**
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.
1084      *
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.
1089      *
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.
1092      */
1093     this.fieldDetails = null;
1095     this.sections = [];
1096     this._sectionCache = new WeakMap();
1097   }
1099   /**
1100    * Set fieldDetails from the form about fields that can be autofilled.
1101    *
1102    * @param {boolean} allowDuplicates
1103    *        true to remain any duplicated field details otherwise to remove the
1104    *        duplicated ones.
1105    * @returns {Array} The valid address and credit card details.
1106    */
1107   collectFormFields(allowDuplicates = false) {
1108     let sections = FormAutofillHeuristics.getFormInfo(
1109       this.form,
1110       allowDuplicates
1111     );
1112     let allValidDetails = [];
1113     for (let { fieldDetails, type } of sections) {
1114       let section;
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(
1119           fieldDetails,
1120           this.winUtils
1121         );
1122       } else {
1123         throw new Error("Unknown field type.");
1124       }
1125       this.sections.push(section);
1126       allValidDetails.push(...section.fieldDetails);
1127     }
1129     for (let detail of allValidDetails) {
1130       let input = detail.elementWeakRef.get();
1131       if (!input) {
1132         continue;
1133       }
1134       input.addEventListener("input", this, { mozSystemGroup: true });
1135     }
1137     this.fieldDetails = allValidDetails;
1138     return allValidDetails;
1139   }
1141   _hasFilledSection() {
1142     return this.sections.some(section => section.isFilled());
1143   }
1145   /**
1146    * Processes form fields that can be autofilled, and populates them with the
1147    * profile provided by backend.
1148    *
1149    * @param {Object} profile
1150    *        A profile to be filled in.
1151    */
1152   async autofillFormFields(profile) {
1153     let noFilledSectionsPreviously = !this._hasFilledSection();
1154     await this.activeSection.autofillFields(profile);
1156     const onChangeHandler = e => {
1157       if (!e.isTrusted) {
1158         return;
1159       }
1160       if (e.type == "reset") {
1161         for (let section of this.sections) {
1162           section.resetFieldStates();
1163         }
1164       }
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,
1169         });
1170         this.form.rootElement.removeEventListener("reset", onChangeHandler, {
1171           mozSystemGroup: true,
1172         });
1173       }
1174     };
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,
1181       });
1182       this.form.rootElement.addEventListener("reset", onChangeHandler, {
1183         mozSystemGroup: true,
1184       });
1185     }
1186   }
1188   handleEvent(event) {
1189     switch (event.type) {
1190       case "input":
1191         if (!event.isTrusted) {
1192           return;
1193         }
1195         for (let detail of this.fieldDetails) {
1196           let input = detail.elementWeakRef.get();
1197           if (!input) {
1198             continue;
1199           }
1200           input.removeEventListener("input", this, { mozSystemGroup: true });
1201         }
1202         this.timeStartedFillingMS = Date.now();
1203         break;
1204     }
1205   }
1207   /**
1208    * Collect the filled sections within submitted form and convert all the valid
1209    * field data into multiple records.
1210    *
1211    * @returns {Object} records
1212    *          {Array.<Object>} records.address
1213    *          {Array.<Object>} records.creditCard
1214    */
1215   createRecords() {
1216     const records = {
1217       address: [],
1218       creditCard: [],
1219     };
1221     for (const section of this.sections) {
1222       const secRecord = section.createRecord();
1223       if (!secRecord) {
1224         continue;
1225       }
1226       if (section instanceof FormAutofillAddressSection) {
1227         records.address.push(secRecord);
1228       } else if (section instanceof FormAutofillCreditCardSection) {
1229         records.creditCard.push(secRecord);
1230       } else {
1231         throw new Error("Unknown section type");
1232       }
1233     }
1234     log.debug("Create records:", records);
1235     return records;
1236   }