Bug 1933479 - Add tab close button on hover to vertical tabs when sidebar is collapse...
[gecko.git] / toolkit / components / formautofill / FormAutofillChild.sys.mjs
blob9a29dd6394c97c6caa6cd680792d3c715b9de409
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 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
6 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
8 const lazy = {};
10 ChromeUtils.defineESModuleGetters(lazy, {
11   AddressResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs",
12   AutofillTelemetry: "resource://gre/modules/shared/AutofillTelemetry.sys.mjs",
13   CreditCardResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs",
14   GenericAutocompleteItem: "resource://gre/modules/FillHelpers.sys.mjs",
15   InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.sys.mjs",
16   FieldDetail: "resource://gre/modules/shared/FieldScanner.sys.mjs",
17   FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
18   FormAutofillContent: "resource://autofill/FormAutofillContent.sys.mjs",
19   FormAutofillHandler:
20     "resource://gre/modules/shared/FormAutofillHandler.sys.mjs",
21   FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
22   FormLikeFactory: "resource://gre/modules/FormLikeFactory.sys.mjs",
23   FormScenarios: "resource://gre/modules/FormScenarios.sys.mjs",
24   FormStateManager: "resource://gre/modules/shared/FormStateManager.sys.mjs",
25   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
26   FORM_SUBMISSION_REASON: "resource://gre/actors/FormHandlerChild.sys.mjs",
27 });
29 XPCOMUtils.defineLazyPreferenceGetter(
30   lazy,
31   "DELEGATE_AUTOCOMPLETE",
32   "toolkit.autocomplete.delegate",
33   false
36 /**
37  * Handles content's interactions for the frame.
38  */
39 export class FormAutofillChild extends JSWindowActorChild {
40   // Flag to indicate whethere there is an ongoing autofilling process.
41   #autofillInProgress = false;
43   /**
44    * Keep track of autofill handlers that are waiting for the parent process
45    * to send back the identified result.
46    */
47   #handlerWaitingForDetectedComplete = new Set();
49   constructor() {
50     super();
52     this.log = lazy.FormAutofill.defineLogGetter(this, "FormAutofillChild");
53     this.debug("init");
55     this._hasDOMContentLoadedHandler = false;
57     this._hasRegisteredPageHide = new Set();
59     /**
60      * @type {FormAutofillFieldDetailsManager} handling state management of current forms and handlers.
61      */
62     this._fieldDetailsManager = new lazy.FormStateManager(
63       this.onFilledModified.bind(this)
64     );
66     /**
67      * Tracks whether the last form submission was triggered by a form submit event,
68      * if so we'll ignore the page navigation that follows
69      */
70     this.isFollowingSubmitEvent = false;
71   }
73   /**
74    * After the parent process finishes classifying the fields, the parent process
75    * informs all the child process of the classified field result. The child process
76    * then sets the updated result to the corresponding AutofillHandler
77    *
78    * @param {Array<FieldDetail>} fieldDetails
79    *        An array of the identified fields.
80    */
81   onFieldsDetectedComplete(fieldDetails) {
82     if (!fieldDetails.length) {
83       return;
84     }
86     const handler = this._fieldDetailsManager.getFormHandlerByRootElementId(
87       fieldDetails[0].rootElementId
88     );
89     this.#handlerWaitingForDetectedComplete.delete(handler);
91     handler.setIdentifiedFieldDetails(fieldDetails);
93     let addressFields = [];
94     let creditcardFields = [];
96     handler.fieldDetails.forEach(fd => {
97       if (lazy.FormAutofillUtils.isAddressField(fd.fieldName)) {
98         addressFields.push(fd);
99       } else if (lazy.FormAutofillUtils.isCreditCardField(fd.fieldName)) {
100         creditcardFields.push(fd);
101       }
102     });
104     // Bug 1905040. This is only a temporarily workaround for now to skip marking address fields
105     // autocompletable whenever we detect an address field. We only mark address field when
106     // it is a valid address section (This is done in the parent)
107     const addressFieldSet = new Set(addressFields.map(fd => fd.fieldName));
108     if (
109       addressFieldSet.size < lazy.FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD
110     ) {
111       addressFields = [];
112     }
114     // Inform the autocomplete controller these fields are autofillable
115     [...addressFields, ...creditcardFields].forEach(fieldDetail => {
116       this.#markAsAutofillField(fieldDetail);
118       if (fieldDetail.element == lazy.FormAutofillContent.focusedInput) {
119         this.showPopupIfEmpty(fieldDetail.element, fieldDetail.fieldName);
120       }
121     });
123     // Do not need to listen to form submission event because if the address fields do not contain
124     // 'street-address' or `address-linx`, we will not save the address.
125     if (
126       creditcardFields.length ||
127       (addressFields.length &&
128         [
129           "street-address",
130           "address-line1",
131           "address-line2",
132           "address-line3",
133         ].some(fieldName => addressFieldSet.has(fieldName)))
134     ) {
135       this.manager
136         .getActor("FormHandler")
137         .registerFormSubmissionInterest(this, {
138           includesFormRemoval: lazy.FormAutofill.captureOnFormRemoval,
139           includesPageNavigation: lazy.FormAutofill.captureOnPageNavigation,
140         });
142       // TODO (Bug 1901486): Integrate pagehide to FormHandler.
143       if (!this._hasRegisteredPageHide.has(handler)) {
144         this.registerPageHide(handler);
145         this._hasRegisteredPageHide.add(true);
146       }
147     }
148   }
150   /**
151    * Identifies elements that are in the associated form of the passed element.
152    *
153    * @param {Element} element
154    *        The element to be identified.
155    *
156    * @returns {FormAutofillHandler}
157    *        The autofill handler instance for the form that is associated with the
158    *        passed element.
159    */
160   identifyFieldsWhenFocused(element) {
161     this.debug(
162       `identifyFieldsWhenFocused: ${element.ownerDocument.location?.hostname}`
163     );
165     const handler = this._fieldDetailsManager.getOrCreateFormHandler(element);
167     // If the child process is still waiting for the parent to send to
168     // `onFieldsDetectedComplete` message, bail out.
169     if (this.#handlerWaitingForDetectedComplete.has(handler)) {
170       return;
171     }
173     // Bail out if there is nothing changed since last time we identified this element
174     // or there is no interested fields.
175     if (handler.hasIdentifiedFields() && !handler.updateFormIfNeeded(element)) {
176       // This is for testing purposes only. It sends a notification to indicate that the
177       // form has been identified and is ready to open the popup.
178       // If new fields are detected, the message will be sent to the parent
179       // once the parent finishes collecting information from sub-frames if they exist.
180       this.sendAsyncMessage("FormAutofill:FieldsIdentified");
182       const fieldName =
183         handler.getFieldDetailByElement(element)?.fieldName ?? "";
184       this.showPopupIfEmpty(element, fieldName);
185     } else {
186       const includeIframe = this.browsingContext == this.browsingContext.top;
187       let detectedFields = lazy.FormAutofillHandler.collectFormFieldDetails(
188         handler.form,
189         includeIframe
190       );
192       // If none of the detected fields are credit card or address fields,
193       // there's no need to notify the parent because nothing will change.
194       if (
195         !detectedFields.some(
196           fd =>
197             lazy.FormAutofillUtils.isCreditCardField(fd.fieldName) ||
198             lazy.FormAutofillUtils.isAddressField(fd.fieldName)
199         )
200       ) {
201         handler.setIdentifiedFieldDetails(detectedFields);
202         return;
203       }
205       this.sendAsyncMessage(
206         "FormAutofill:OnFieldsDetected",
207         detectedFields.map(field => field.toVanillaObject())
208       );
210       // Notify the parent about the newly identified fields because
211       // the autofill section information is maintained on the parent side.
212       this.#handlerWaitingForDetectedComplete.add(handler);
213     }
214   }
216   /**
217    * This function is called by the parent when a field is detected in another
218    * frame. The parent uses this function to collect field information from frames
219    * that are part of the same form as the detected field.
220    *
221    * @param {string} focusedBCId
222    *        The browsing context ID of the top-level iframe
223    *        that contains the detected field.
224    *        Note that this value is set only when the current frame is the top-level.
225    *
226    * @returns {Array}
227    *        Array of FieldDetail objects of identified fields (including iframes).
228    */
229   identifyFields(focusedBCId) {
230     const isTop = this.browsingContext == this.browsingContext.top;
232     let element;
233     if (isTop) {
234       // Find the focused iframe
235       element = BrowsingContext.get(focusedBCId).embedderElement;
236     } else {
237       // Ignore form as long as the frame is not the top-level, which means
238       // we can just pick any of the eligible elements to identify.
239       element = this.document.querySelector("input, select, iframe");
240     }
242     if (!element) {
243       return [];
244     }
246     const handler = this._fieldDetailsManager.getOrCreateFormHandler(element);
248     // We don't have to call 'updateFormIfNeeded' like we do in
249     // 'identifyFieldsWhenFocused' because 'collectFormFieldDetails' doesn't use cached
250     // result.
251     const includeIframe = isTop;
252     const detectedFields = lazy.FormAutofillHandler.collectFormFieldDetails(
253       handler.form,
254       includeIframe
255     );
257     if (detectedFields.length) {
258       // This actor should receive `onFieldsDetectedComplete`message after
259       // `idenitfyFields` is called
260       this.#handlerWaitingForDetectedComplete.add(handler);
261     }
262     return detectedFields;
263   }
265   showPopupIfEmpty(element, fieldName) {
266     if (element?.value?.length !== 0) {
267       this.debug(`Not opening popup because field is not empty.`);
268       return;
269     }
271     if (fieldName.startsWith("cc-") || AppConstants.platform === "android") {
272       lazy.FormAutofillContent.showPopup();
273     }
274   }
276   /**
277    * We received a form-submission-detected event because
278    * the page was navigated.
279    */
280   onPageNavigation() {
281     if (!lazy.FormAutofill.captureOnPageNavigation) {
282       return;
283     }
285     if (this.isFollowingSubmitEvent) {
286       // The next page navigation should be handled as form submission again
287       this.isFollowingSubmitEvent = false;
288       return;
289     }
291     const formSubmissionReason = lazy.FORM_SUBMISSION_REASON.PAGE_NAVIGATION;
292     const weakIdentifiedForms =
293       this._fieldDetailsManager.getWeakIdentifiedForms();
295     for (const form of weakIdentifiedForms) {
296       // Disconnected forms are captured by the form removal heuristic
297       if (!form.isConnected) {
298         continue;
299       }
300       this.formSubmitted(form, formSubmissionReason);
301     }
302   }
304   /**
305    * We received a form-submission-detected event because
306    * a form was removed from the DOM after a successful
307    * xhr/fetch request
308    *
309    * @param {Event} form form to be submitted
310    */
311   onFormRemoval(form) {
312     if (!lazy.FormAutofill.captureOnFormRemoval) {
313       return;
314     }
316     const formSubmissionReason =
317       lazy.FORM_SUBMISSION_REASON.FORM_REMOVAL_AFTER_FETCH;
318     this.formSubmitted(form, formSubmissionReason);
319     this.manager.getActor("FormHandler").unregisterFormRemovalInterest(this);
320   }
322   registerPageHide(handler) {
323     // Check whether the section is in an <iframe>; and, if so,
324     // watch for the <iframe> to pagehide.
325     if (this.browsingContext != this.browsingContext.top) {
326       this.debug(
327         "Address/Credit card form is in an iframe -- watching for pagehide"
328       );
329       handler.window.addEventListener(
330         "pagehide",
331         () => {
332           this.debug("Credit card subframe is pagehiding", handler.form);
334           const reason = lazy.FORM_SUBMISSION_REASON.IFRAME_PAGEHIDE;
335           this.formSubmitted(handler.form.rootElement, reason, handler);
336           this._hasRegisteredPageHide.delete(handler);
337         },
338         { once: true }
339       );
340     }
341   }
343   shouldIgnoreFormAutofillEvent(event) {
344     if (!event.isTrusted) {
345       return true;
346     }
348     if (
349       !lazy.FormAutofill.isAutofillCreditCardsAvailable &&
350       !lazy.FormAutofill.isAutofillAddressesAvailable
351     ) {
352       return true;
353     }
355     const nodePrincipal = event.target.nodePrincipal;
356     return nodePrincipal.isSystemPrincipal || nodePrincipal.schemeIs("about");
357   }
359   handleEvent(evt) {
360     if (
361       !lazy.FormAutofill.isAutofillEnabled ||
362       this.shouldIgnoreFormAutofillEvent(evt)
363     ) {
364       return;
365     }
367     if (!this.windowContext) {
368       // !this.windowContext must not be null, because we need the
369       // windowContext and/or docShell to (un)register form submission listeners
370       return;
371     }
373     switch (evt.type) {
374       case "focusin": {
375         this.onFocusIn(evt.target);
376         break;
377       }
378       case "form-submission-detected": {
379         const formElement = evt.detail.form;
380         const formSubmissionReason = evt.detail.reason;
381         this.onFormSubmission(formElement, formSubmissionReason);
382         break;
383       }
385       default: {
386         throw new Error("Unexpected event type");
387       }
388     }
389   }
391   onFocusIn(element) {
392     // When autofilling, we focus on the element before setting the autofill value
393     // (See FormAutofillHandler.fillFieldValue). We ignore the focus event for this
394     // case to avoid showing popup while autofilling.
395     if (
396       !lazy.FormAutofillUtils.isCreditCardOrAddressFieldType(element) ||
397       this.#autofillInProgress
398     ) {
399       return;
400     }
402     const doc = element.ownerDocument;
403     if (doc.readyState === "loading") {
404       // For auto-focused input, we might receive focus event before document becomes ready.
405       // When this happens, run field identification after receiving `DOMContentLoaded` event
406       if (!this._hasDOMContentLoadedHandler) {
407         this._hasDOMContentLoadedHandler = true;
408         doc.addEventListener(
409           "DOMContentLoaded",
410           () => this.onFocusIn(lazy.FormAutofillContent.focusedInput),
411           { once: true }
412         );
413       }
414       return;
415     }
417     if (
418       lazy.DELEGATE_AUTOCOMPLETE ||
419       !lazy.FormAutofillContent.savedFieldNames
420     ) {
421       this.debug("onFocusIn: savedFieldNames are not known yet");
423       // Init can be asynchronous because we don't need anything from the parent
424       // at this point.
425       this.sendAsyncMessage("FormAutofill:InitStorage");
426     }
428     this.identifyFieldsWhenFocused(element);
429   }
431   /**
432    * Handle form-submission-detected event (dispatched by FormHandlerChild)
433    *
434    * Depending on the heuristic that detected the form submission,
435    * the form that is submitted is retrieved differently
436    *
437    * @param {HTMLFormElement} form that is being submitted
438    * @param {string} reason heuristic that detected the form submission
439    *                        (see FormHandlerChild.FORM_SUBMISSION_REASON)
440    */
441   onFormSubmission(form, reason) {
442     switch (reason) {
443       case lazy.FORM_SUBMISSION_REASON.PAGE_NAVIGATION:
444         this.onPageNavigation();
445         break;
446       case lazy.FORM_SUBMISSION_REASON.FORM_SUBMIT_EVENT:
447         this.formSubmitted(form, reason);
448         break;
449       case lazy.FORM_SUBMISSION_REASON.FORM_REMOVAL_AFTER_FETCH:
450         this.onFormRemoval(form);
451         break;
452     }
453   }
455   async receiveMessage(message) {
456     switch (message.name) {
457       case "FormAutofill:FillFields": {
458         const { focusedId, ids, profile } = message.data;
459         const result = this.fillFields(focusedId, ids, profile);
461         // Return the autofilled result to the parent. The result
462         // is used by both tests and telemetry.
463         return result;
464       }
465       case "FormAutofill:ClearFilledFields": {
466         const { ids } = message.data;
467         const handler = this.#getHandlerByElementId(ids[0]);
468         handler?.clearFilledFields(ids);
469         break;
470       }
471       case "FormAutofill:PreviewFields": {
472         const { ids, profile } = message.data;
473         const handler = this.#getHandlerByElementId(ids[0]);
475         if (profile) {
476           handler?.previewFields(ids, profile);
477         } else {
478           handler?.clearPreviewedFields(ids);
479         }
480         break;
481       }
482       case "FormAutofill:IdentifyFields": {
483         const { focusedBCId } = message.data ?? {};
484         return this.identifyFields(focusedBCId).map(fieldDetail =>
485           fieldDetail.toVanillaObject()
486         );
487       }
488       case "FormAutofill:GetFilledInfo": {
489         const { rootElementId } = message.data;
490         const handler =
491           this._fieldDetailsManager.getFormHandlerByRootElementId(
492             rootElementId
493           );
494         return handler?.collectFormFilledData();
495       }
496       case "FormAutofill:InspectFields": {
497         const fieldDetails = this.inspectFields();
498         return fieldDetails.map(field => field.toVanillaObject());
499       }
500       case "FormAutofill:onFieldsDetectedComplete": {
501         const { fds } = message.data;
502         const fieldDetails = fds.map(fd =>
503           lazy.FieldDetail.fromVanillaObject(fd)
504         );
505         this.onFieldsDetectedComplete(fieldDetails);
506         break;
507       }
508     }
509     return true;
510   }
512   /**
513    * Handle a form submission and early return when:
514    * 1. In private browsing mode.
515    * 2. Could not map any autofill handler by form element.
516    * 3. Number of filled fields is less than autofill threshold
517    *
518    * @param {HTMLElement} formElement Root element which receives submit event.
519    * @param {string} formSubmissionReason Reason for invoking the form submission
520    *                 (see options for FORM_SUBMISSION_REASON in FormAutofillUtils))
521    * @param {object} handler FormAutofillHander, if known by caller
522    */
523   formSubmitted(formElement, formSubmissionReason, handler = undefined) {
524     this.debug(`Handling form submission - infered by ${formSubmissionReason}`);
526     lazy.AutofillTelemetry.recordFormSubmissionHeuristicCount(
527       formSubmissionReason
528     );
530     if (!lazy.FormAutofill.isAutofillEnabled) {
531       this.debug("Form Autofill is disabled");
532       return;
533     }
535     // The `domWin` truthiness test is used by unit tests to bypass this check.
536     const domWin = formElement.ownerGlobal;
537     if (!domWin) {
538       return;
539     }
541     if (lazy.PrivateBrowsingUtils.isContentWindowPrivate(domWin)) {
542       this.debug("Ignoring submission in a private window");
543       return;
544     }
546     handler = handler || this._fieldDetailsManager.getFormHandler(formElement);
547     if (!handler) {
548       this.debug("Form element could not map to an existing handler");
549       return;
550     }
552     const formFilledData = handler.collectFormFilledData();
553     if (!formFilledData) {
554       this.debug("Form handler could not obtain filled data");
555       return;
556     }
558     // After a form submit event follows (most likely) a page navigation, so we set this flag
559     // to not handle the following one as form submission in order to avoid re-submitting the same form.
560     // Ideally, we should keep a record of the last submitted form details and based on that we
561     // should decide if we want to submit a form (bug 1895437)
562     this.isFollowingSubmitEvent = true;
564     this.sendAsyncMessage("FormAutofill:OnFormSubmit", {
565       rootElementId: handler.rootElementId,
566       formFilledData,
567     });
568   }
570   /**
571    * This is called by FormAutofillHandler
572    */
573   onFilledModified(fieldDetail, previousState, newState) {
574     const element = fieldDetail.element;
575     if (HTMLInputElement.isInstance(element)) {
576       // If the user manually blanks a credit card field, then
577       // we want the popup to be activated.
578       if (
579         lazy.FormAutofillUtils.isCreditCardField(fieldDetail.fieldName) &&
580         element.value === ""
581       ) {
582         lazy.FormAutofillContent.showPopup();
583       }
584     }
586     if (
587       previousState == lazy.FormAutofillUtils.FIELD_STATES.AUTO_FILLED &&
588       newState == lazy.FormAutofillUtils.FIELD_STATES.NORMAL
589     ) {
590       this.sendAsyncMessage(
591         "FormAutofill:FieldFilledModified",
592         fieldDetail.elementId
593       );
594     }
595   }
597   async fillFields(focusedId, elementIds, profile) {
598     this.#autofillInProgress = true;
599     let result = new Map();
600     try {
601       Services.obs.notifyObservers(null, "autofill-fill-starting");
602       const handler = this.#getHandlerByElementId(elementIds[0]);
603       handler.fillFields(focusedId, elementIds, profile);
605       // Return the autofilled result to the parent. The result
606       // is used by both tests and telemetry.
607       result = handler.collectFormFilledData();
609       Services.obs.notifyObservers(null, "autofill-fill-complete");
610     } catch {}
612     this.#autofillInProgress = false;
613     return result;
614   }
616   /**
617    * Returns all the identified fields for this document.
618    * This function is only used by the autofill developer tool extension.
619    */
620   inspectFields() {
621     const isTop = this.browsingContext == this.browsingContext.top;
622     const elements = isTop
623       ? Array.from(this.document.querySelectorAll("input, select, iframe"))
624       : Array.from(this.document.querySelectorAll("input, select"));
626     // Unlike the case when users click on a field and we only run our heuristic
627     // on fields within the same form as the focused field, for inspection,
628     // we want to inspect all the forms in this page.
629     const roots = new Set();
630     let fieldDetails = [];
631     for (const element of elements) {
632       const formLike = lazy.FormLikeFactory.createFromField(element);
633       if (roots.has(formLike.rootElement)) {
634         continue;
635       }
636       roots.add(formLike.rootElement);
637       const handler = new lazy.FormAutofillHandler(formLike);
639       // Fields that cannot be recognized will still be reported with this API.
640       const includeIframe = isTop;
641       const fields = lazy.FormAutofillHandler.collectFormFieldDetails(
642         handler.form,
643         includeIframe,
644         false
645       );
646       fieldDetails.push(...fields);
647     }
649     // The 'fieldDetails' array are grouped by form so might not follow their
650     // order in the DOM tree. We rebuild the array based on their order in
651     // the document.
652     fieldDetails = elements
653       .map(element => fieldDetails.find(field => field.element == element))
654       .filter(field => !!field && field.element);
656     // Add a data attribute with a unique identifier to allow the inspector
657     // to link the element with its associated 'FieldDetail' information.
658     for (const fd of fieldDetails) {
659       const INSPECT_ATTRIBUTE = "data-moz-autofill-inspect-id";
660       fd.inspectId = fd.element.getAttribute(INSPECT_ATTRIBUTE);
661     }
663     return fieldDetails;
664   }
666   #markAsAutofillField(fieldDetail) {
667     const element = fieldDetail.element;
669     // Since Form Autofill popup is only for input element, any non-Input
670     // element should be excluded here.
671     if (!HTMLInputElement.isInstance(element)) {
672       return;
673     }
675     this.manager
676       .getActor("AutoComplete")
677       ?.markAsAutoCompletableField(element, this);
678   }
680   get actorName() {
681     return "FormAutofill";
682   }
684   /**
685    * Get the search options when searching for autocomplete entries in the parent
686    *
687    * @param {HTMLInputElement} input - The input element to search for autocomplete entries
688    * @returns {object} the search options for the input
689    */
690   getAutoCompleteSearchOption(input) {
691     const fieldDetail = this._fieldDetailsManager
692       .getFormHandler(input)
693       ?.getFieldDetailByElement(input);
695     const scenarioName = lazy.FormScenarios.detect({ input }).signUpForm
696       ? "SignUpFormScenario"
697       : "";
698     return {
699       fieldName: fieldDetail?.fieldName,
700       elementId: fieldDetail?.elementId,
701       scenarioName,
702     };
703   }
705   /**
706    * Ask the provider whether it might have autocomplete entry to show
707    * for the given input.
708    *
709    * @param {HTMLInputElement} input - The input element to search for autocomplete entries
710    * @returns {boolean} true if we shold search for autocomplete entries
711    */
712   shouldSearchForAutoComplete(input) {
713     const fieldDetail = this._fieldDetailsManager
714       .getFormHandler(input)
715       ?.getFieldDetailByElement(input);
716     if (!fieldDetail) {
717       return false;
718     }
719     const fieldName = fieldDetail.fieldName;
720     const isAddressField = lazy.FormAutofillUtils.isAddressField(fieldName);
721     const searchPermitted = isAddressField
722       ? lazy.FormAutofill.isAutofillAddressesEnabled
723       : lazy.FormAutofill.isAutofillCreditCardsEnabled;
724     // If the specified autofill feature is pref off, do not search
725     if (!searchPermitted) {
726       return false;
727     }
729     // No profile can fill the currently-focused input.
730     if (!lazy.FormAutofillContent.savedFieldNames.has(fieldName)) {
731       return false;
732     }
734     return true;
735   }
737   /**
738    * Convert the search result to autocomplete results
739    *
740    * @param {string} searchString - The string to search for
741    * @param {HTMLInputElement} input - The input element to search for autocomplete entries
742    * @param {Array<object>} records - autocomplete records
743    * @returns {AutocompleteResult}
744    */
745   searchResultToAutoCompleteResult(searchString, input, records) {
746     if (!records) {
747       return null;
748     }
750     const handler = this._fieldDetailsManager.getFormHandler(input);
751     const fieldDetail = handler?.getFieldDetailByElement(input);
752     if (!fieldDetail) {
753       return null;
754     }
756     const adaptedRecords = handler.getAdaptedProfiles(records.records);
757     const isSecure = lazy.InsecurePasswordUtils.isFormSecure(handler.form);
758     const isInputAutofilled =
759       input.autofillState == lazy.FormAutofillUtils.FIELD_STATES.AUTO_FILLED;
761     let AutocompleteResult;
763     // TODO: This should be calculated in the parent
764     // The field categories will be filled if the corresponding profile is
765     // used for autofill. We don't display this information for credit
766     // cards, so this is only calculated for address fields.
767     let fillCategories;
768     if (lazy.FormAutofillUtils.isAddressField(fieldDetail.fieldName)) {
769       AutocompleteResult = lazy.AddressResult;
770       fillCategories = adaptedRecords.map(profile => {
771         const fields = Object.keys(profile).filter(fieldName => {
772           const detail = handler.getFieldDetailByName(fieldName);
773           return detail ? handler.isFieldAutofillable(detail, profile) : false;
774         });
775         return lazy.FormAutofillUtils.getCategoriesFromFieldNames(fields);
776       });
777     } else {
778       AutocompleteResult = lazy.CreditCardResult;
779     }
781     const acResult = new AutocompleteResult(
782       searchString,
783       fieldDetail,
784       records.allFieldNames,
785       adaptedRecords,
786       fillCategories,
787       { isSecure, isInputAutofilled }
788     );
790     const externalEntries = records.externalEntries;
792     acResult.externalEntries.push(
793       ...externalEntries.map(
794         entry =>
795           new lazy.GenericAutocompleteItem(
796             entry.image,
797             entry.label,
798             entry.secondary,
799             entry.fillMessageName,
800             entry.fillMessageData
801           )
802       )
803     );
805     return acResult;
806   }
808   #getHandlerByElementId(elementId) {
809     const element = lazy.FormAutofillUtils.getElementByIdentifier(elementId);
810     return this._fieldDetailsManager.getFormHandler(element);
811   }