/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  AddressResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs",
  AutofillTelemetry: "resource://gre/modules/shared/AutofillTelemetry.sys.mjs",
  CreditCardResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs",
  GenericAutocompleteItem: "resource://gre/modules/FillHelpers.sys.mjs",
  InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.sys.mjs",
  FieldDetail: "resource://gre/modules/shared/FieldScanner.sys.mjs",
  FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
  FormAutofillContent: "resource://autofill/FormAutofillContent.sys.mjs",
  FormAutofillHandler:
    "resource://gre/modules/shared/FormAutofillHandler.sys.mjs",
  FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
  FormLikeFactory: "resource://gre/modules/FormLikeFactory.sys.mjs",
  FormScenarios: "resource://gre/modules/FormScenarios.sys.mjs",
  FormStateManager: "resource://gre/modules/shared/FormStateManager.sys.mjs",
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
  FORM_SUBMISSION_REASON: "resource://gre/actors/FormHandlerChild.sys.mjs",
});

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "DELEGATE_AUTOCOMPLETE",
  "toolkit.autocomplete.delegate",
  false
);

/**
 * Handles content's interactions for the frame.
 */
export class FormAutofillChild extends JSWindowActorChild {
  // Flag to indicate whethere there is an ongoing autofilling process.
  #autofillInProgress = false;

  /**
   * Keep track of autofill handlers that are waiting for the parent process
   * to send back the identified result.
   */
  #handlerWaitingForDetectedComplete = new Set();

  constructor() {
    super();

    this.log = lazy.FormAutofill.defineLogGetter(this, "FormAutofillChild");
    this.debug("init");

    this._hasDOMContentLoadedHandler = false;

    this._hasRegisteredPageHide = new Set();

    /**
     * @type {FormAutofillFieldDetailsManager} handling state management of current forms and handlers.
     */
    this._fieldDetailsManager = new lazy.FormStateManager(
      this.onFilledModified.bind(this)
    );

    /**
     * Tracks whether the last form submission was triggered by a form submit event,
     * if so we'll ignore the page navigation that follows
     */
    this.isFollowingSubmitEvent = false;
  }

  /**
   * After the parent process finishes classifying the fields, the parent process
   * informs all the child process of the classified field result. The child process
   * then sets the updated result to the corresponding AutofillHandler
   *
   * @param {Array<FieldDetail>} fieldDetails
   *        An array of the identified fields.
   */
  onFieldsDetectedComplete(fieldDetails) {
    if (!fieldDetails.length) {
      return;
    }

    const handler = this._fieldDetailsManager.getFormHandlerByRootElementId(
      fieldDetails[0].rootElementId
    );
    this.#handlerWaitingForDetectedComplete.delete(handler);

    handler.setIdentifiedFieldDetails(fieldDetails);

    let addressFields = [];
    let creditcardFields = [];

    handler.fieldDetails.forEach(fd => {
      if (lazy.FormAutofillUtils.isAddressField(fd.fieldName)) {
        addressFields.push(fd);
      } else if (lazy.FormAutofillUtils.isCreditCardField(fd.fieldName)) {
        creditcardFields.push(fd);
      }
    });

    // Bug 1905040. This is only a temporarily workaround for now to skip marking address fields
    // autocompletable whenever we detect an address field. We only mark address field when
    // it is a valid address section (This is done in the parent)
    const addressFieldSet = new Set(addressFields.map(fd => fd.fieldName));
    if (
      addressFieldSet.size < lazy.FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD
    ) {
      addressFields = [];
    }

    // Inform the autocomplete controller these fields are autofillable
    [...addressFields, ...creditcardFields].forEach(fieldDetail => {
      this.#markAsAutofillField(fieldDetail);

      if (fieldDetail.element == lazy.FormAutofillContent.focusedInput) {
        this.showPopupIfEmpty(fieldDetail.element, fieldDetail.fieldName);
      }
    });

    // Do not need to listen to form submission event because if the address fields do not contain
    // 'street-address' or `address-linx`, we will not save the address.
    if (
      creditcardFields.length ||
      (addressFields.length &&
        [
          "street-address",
          "address-line1",
          "address-line2",
          "address-line3",
        ].some(fieldName => addressFieldSet.has(fieldName)))
    ) {
      this.manager
        .getActor("FormHandler")
        .registerFormSubmissionInterest(this, {
          includesFormRemoval: lazy.FormAutofill.captureOnFormRemoval,
          includesPageNavigation: lazy.FormAutofill.captureOnPageNavigation,
        });

      // TODO (Bug 1901486): Integrate pagehide to FormHandler.
      if (!this._hasRegisteredPageHide.has(handler)) {
        this.registerPageHide(handler);
        this._hasRegisteredPageHide.add(true);
      }
    }
  }

  /**
   * Identifies elements that are in the associated form of the passed element.
   *
   * @param {Element} element
   *        The element to be identified.
   *
   * @returns {FormAutofillHandler}
   *        The autofill handler instance for the form that is associated with the
   *        passed element.
   */
  identifyFieldsWhenFocused(element) {
    this.debug(
      `identifyFieldsWhenFocused: ${element.ownerDocument.location?.hostname}`
    );

    const handler = this._fieldDetailsManager.getOrCreateFormHandler(element);

    // If the child process is still waiting for the parent to send to
    // `onFieldsDetectedComplete` message, bail out.
    if (this.#handlerWaitingForDetectedComplete.has(handler)) {
      return;
    }

    // Bail out if there is nothing changed since last time we identified this element
    // or there is no interested fields.
    if (handler.hasIdentifiedFields() && !handler.updateFormIfNeeded(element)) {
      // This is for testing purposes only. It sends a notification to indicate that the
      // form has been identified and is ready to open the popup.
      // If new fields are detected, the message will be sent to the parent
      // once the parent finishes collecting information from sub-frames if they exist.
      this.sendAsyncMessage("FormAutofill:FieldsIdentified");

      const fieldName =
        handler.getFieldDetailByElement(element)?.fieldName ?? "";
      this.showPopupIfEmpty(element, fieldName);
    } else {
      const detectedFields = lazy.FormAutofillHandler.collectFormFields(
        handler.form
      );

      // If none of the detected fields are credit card or address fields,
      // there's no need to notify the parent because nothing will change.
      if (
        !detectedFields.some(
          fd =>
            lazy.FormAutofillUtils.isCreditCardField(fd.fieldName) ||
            lazy.FormAutofillUtils.isAddressField(fd.fieldName)
        )
      ) {
        handler.setIdentifiedFieldDetails(detectedFields);
        return;
      }

      this.sendAsyncMessage(
        "FormAutofill:OnFieldsDetected",
        detectedFields.map(field => field.toVanillaObject())
      );

      // Notify the parent about the newly identified fields because
      // the autofill section information is maintained on the parent side.
      this.#handlerWaitingForDetectedComplete.add(handler);
    }
  }

  /**
   * This function is called by the parent when a field is detected in another
   * frame. The parent uses this function to collect field information from frames
   * that are part of the same form as the detected field.
   *
   * @param {string} focusedBCId
   *        The browsing context ID of the top-level iframe
   *        that contains the detected field.
   *        Note that this value is set only when the current frame is the top-level.
   *
   * @returns {Array}
   *        Array of FieldDetail objects of identified fields (including iframes).
   */
  identifyFields(focusedBCId) {
    const isTop = this.browsingContext == this.browsingContext.top;

    let element;
    if (isTop) {
      // Find the focused iframe
      element = BrowsingContext.get(focusedBCId).embedderElement;
    } else {
      // Ignore form as long as the frame is not the top-level, which means
      // we can just pick any of the eligible elements to identify.
      element = this.document.querySelector("input, select, iframe");
    }

    if (!element) {
      return [];
    }

    const handler = this._fieldDetailsManager.getOrCreateFormHandler(element);

    // We don't have to call 'updateFormIfNeeded' like we do in
    // 'identifyFieldsWhenFocused' because 'collectFormFields' doesn't use cached
    // result.
    const detectedFields = lazy.FormAutofillHandler.collectFormFields(
      handler.form
    );

    if (detectedFields.length) {
      // This actor should receive `onFieldsDetectedComplete`message after
      // `idenitfyFields` is called
      this.#handlerWaitingForDetectedComplete.add(handler);
    }
    return detectedFields;
  }

  showPopupIfEmpty(element, fieldName) {
    if (element?.value?.length !== 0) {
      this.debug(`Not opening popup because field is not empty.`);
      return;
    }

    if (fieldName.startsWith("cc-") || AppConstants.platform === "android") {
      lazy.FormAutofillContent.showPopup();
    }
  }

  /**
   * We received a form-submission-detected event because
   * the page was navigated.
   */
  onPageNavigation() {
    if (!lazy.FormAutofill.captureOnPageNavigation) {
      return;
    }

    if (this.isFollowingSubmitEvent) {
      // The next page navigation should be handled as form submission again
      this.isFollowingSubmitEvent = false;
      return;
    }

    const formSubmissionReason = lazy.FORM_SUBMISSION_REASON.PAGE_NAVIGATION;
    const weakIdentifiedForms =
      this._fieldDetailsManager.getWeakIdentifiedForms();

    for (const form of weakIdentifiedForms) {
      // Disconnected forms are captured by the form removal heuristic
      if (!form.isConnected) {
        continue;
      }
      this.formSubmitted(form, formSubmissionReason);
    }
  }

  /**
   * We received a form-submission-detected event because
   * a form was removed from the DOM after a successful
   * xhr/fetch request
   *
   * @param {Event} form form to be submitted
   */
  onFormRemoval(form) {
    if (!lazy.FormAutofill.captureOnFormRemoval) {
      return;
    }

    const formSubmissionReason =
      lazy.FORM_SUBMISSION_REASON.FORM_REMOVAL_AFTER_FETCH;
    this.formSubmitted(form, formSubmissionReason);
    this.manager.getActor("FormHandler").unregisterFormRemovalInterest(this);
  }

  registerPageHide(handler) {
    // Check whether the section is in an <iframe>; and, if so,
    // watch for the <iframe> to pagehide.
    if (this.browsingContext != this.browsingContext.top) {
      this.debug(
        "Address/Credit card form is in an iframe -- watching for pagehide"
      );
      handler.window.addEventListener(
        "pagehide",
        () => {
          this.debug("Credit card subframe is pagehiding", handler.form);

          const reason = lazy.FORM_SUBMISSION_REASON.IFRAME_PAGEHIDE;
          this.formSubmitted(handler.form.rootElement, reason, handler);
          this._hasRegisteredPageHide.delete(handler);
        },
        { once: true }
      );
    }
  }

  shouldIgnoreFormAutofillEvent(event) {
    if (!event.isTrusted) {
      return true;
    }

    if (
      !lazy.FormAutofill.isAutofillCreditCardsAvailable &&
      !lazy.FormAutofill.isAutofillAddressesAvailable
    ) {
      return true;
    }

    const nodePrincipal = event.target.nodePrincipal;
    return nodePrincipal.isSystemPrincipal || nodePrincipal.schemeIs("about");
  }

  handleEvent(evt) {
    if (
      !lazy.FormAutofill.isAutofillEnabled ||
      this.shouldIgnoreFormAutofillEvent(evt)
    ) {
      return;
    }

    if (!this.windowContext) {
      // !this.windowContext must not be null, because we need the
      // windowContext and/or docShell to (un)register form submission listeners
      return;
    }

    switch (evt.type) {
      case "focusin": {
        this.onFocusIn(evt.target);
        break;
      }
      case "form-submission-detected": {
        const formElement = evt.detail.form;
        const formSubmissionReason = evt.detail.reason;
        this.onFormSubmission(formElement, formSubmissionReason);
        break;
      }

      default: {
        throw new Error("Unexpected event type");
      }
    }
  }

  onFocusIn(element) {
    // When autofilling, we focus on the element before setting the autofill value
    // (See FormAutofillHandler.fillFieldValue). We ignore the focus event for this
    // case to avoid showing popup while autofilling.
    if (
      !lazy.FormAutofillUtils.isCreditCardOrAddressFieldType(element) ||
      this.#autofillInProgress
    ) {
      return;
    }

    const doc = element.ownerDocument;
    if (doc.readyState === "loading") {
      // For auto-focused input, we might receive focus event before document becomes ready.
      // When this happens, run field identification after receiving `DOMContentLoaded` event
      if (!this._hasDOMContentLoadedHandler) {
        this._hasDOMContentLoadedHandler = true;
        doc.addEventListener(
          "DOMContentLoaded",
          () => this.onFocusIn(lazy.FormAutofillContent.focusedInput),
          { once: true }
        );
      }
      return;
    }

    if (
      lazy.DELEGATE_AUTOCOMPLETE ||
      !lazy.FormAutofillContent.savedFieldNames
    ) {
      this.debug("onFocusIn: savedFieldNames are not known yet");

      // Init can be asynchronous because we don't need anything from the parent
      // at this point.
      this.sendAsyncMessage("FormAutofill:InitStorage");
    }

    this.identifyFieldsWhenFocused(element);
  }

  /**
   * Handle form-submission-detected event (dispatched by FormHandlerChild)
   *
   * Depending on the heuristic that detected the form submission,
   * the form that is submitted is retrieved differently
   *
   * @param {HTMLFormElement} form that is being submitted
   * @param {string} reason heuristic that detected the form submission
   *                        (see FormHandlerChild.FORM_SUBMISSION_REASON)
   */
  onFormSubmission(form, reason) {
    switch (reason) {
      case lazy.FORM_SUBMISSION_REASON.PAGE_NAVIGATION:
        this.onPageNavigation();
        break;
      case lazy.FORM_SUBMISSION_REASON.FORM_SUBMIT_EVENT:
        this.formSubmitted(form, reason);
        break;
      case lazy.FORM_SUBMISSION_REASON.FORM_REMOVAL_AFTER_FETCH:
        this.onFormRemoval(form);
        break;
    }
  }

  async receiveMessage(message) {
    switch (message.name) {
      case "FormAutofill:FillFields": {
        const { focusedId, ids, profile } = message.data;
        const result = this.fillFields(focusedId, ids, profile);

        // Return the autofilled result to the parent. The result
        // is used by both tests and telemetry.
        return result;
      }
      case "FormAutofill:ClearFilledFields": {
        const { ids } = message.data;
        const handler = this.#getHandlerByElementId(ids[0]);
        handler?.clearFilledFields(ids);
        break;
      }
      case "FormAutofill:PreviewFields": {
        const { ids, profile } = message.data;
        const handler = this.#getHandlerByElementId(ids[0]);

        if (profile) {
          handler?.previewFields(ids, profile);
        } else {
          handler?.clearPreviewedFields(ids);
        }
        break;
      }
      case "FormAutofill:IdentifyFields": {
        const { focusedBCId } = message.data ?? {};
        return this.identifyFields(focusedBCId).map(fieldDetail =>
          fieldDetail.toVanillaObject()
        );
      }
      case "FormAutofill:GetFilledInfo": {
        const { rootElementId } = message.data;
        const handler =
          this._fieldDetailsManager.getFormHandlerByRootElementId(
            rootElementId
          );
        return handler?.collectFormFilledData();
      }
      case "FormAutofill:InspectFields": {
        const fieldDetails = this.inspectFields();
        return fieldDetails.map(field => field.toVanillaObject());
      }
      case "FormAutofill:onFieldsDetectedComplete": {
        const { fds } = message.data;
        const fieldDetails = fds.map(fd =>
          lazy.FieldDetail.fromVanillaObject(fd)
        );
        this.onFieldsDetectedComplete(fieldDetails);
        break;
      }
    }
    return true;
  }

  /**
   * Handle a form submission and early return when:
   * 1. In private browsing mode.
   * 2. Could not map any autofill handler by form element.
   * 3. Number of filled fields is less than autofill threshold
   *
   * @param {HTMLElement} formElement Root element which receives submit event.
   * @param {string} formSubmissionReason Reason for invoking the form submission
   *                 (see options for FORM_SUBMISSION_REASON in FormAutofillUtils))
   * @param {object} handler FormAutofillHander, if known by caller
   */
  formSubmitted(formElement, formSubmissionReason, handler = undefined) {
    this.debug(`Handling form submission - infered by ${formSubmissionReason}`);

    lazy.AutofillTelemetry.recordFormSubmissionHeuristicCount(
      formSubmissionReason
    );

    if (!lazy.FormAutofill.isAutofillEnabled) {
      this.debug("Form Autofill is disabled");
      return;
    }

    // The `domWin` truthiness test is used by unit tests to bypass this check.
    const domWin = formElement.ownerGlobal;
    if (!domWin) {
      return;
    }

    if (lazy.PrivateBrowsingUtils.isContentWindowPrivate(domWin)) {
      this.debug("Ignoring submission in a private window");
      return;
    }

    handler = handler || this._fieldDetailsManager.getFormHandler(formElement);
    if (!handler) {
      this.debug("Form element could not map to an existing handler");
      return;
    }

    const formFilledData = handler.collectFormFilledData();
    if (!formFilledData) {
      this.debug("Form handler could not obtain filled data");
      return;
    }

    // After a form submit event follows (most likely) a page navigation, so we set this flag
    // to not handle the following one as form submission in order to avoid re-submitting the same form.
    // Ideally, we should keep a record of the last submitted form details and based on that we
    // should decide if we want to submit a form (bug 1895437)
    this.isFollowingSubmitEvent = true;

    this.sendAsyncMessage("FormAutofill:OnFormSubmit", {
      rootElementId: handler.rootElementId,
      formFilledData,
    });
  }

  /**
   * This is called by FormAutofillHandler
   */
  onFilledModified(fieldDetail, previousState, newState) {
    const element = fieldDetail.element;
    if (HTMLInputElement.isInstance(element)) {
      // If the user manually blanks a credit card field, then
      // we want the popup to be activated.
      if (
        lazy.FormAutofillUtils.isCreditCardField(fieldDetail.fieldName) &&
        element.value === ""
      ) {
        lazy.FormAutofillContent.showPopup();
      }
    }

    if (
      previousState == lazy.FormAutofillUtils.FIELD_STATES.AUTO_FILLED &&
      newState == lazy.FormAutofillUtils.FIELD_STATES.NORMAL
    ) {
      this.sendAsyncMessage(
        "FormAutofill:FieldFilledModified",
        fieldDetail.elementId
      );
    }
  }

  async fillFields(focusedId, elementIds, profile) {
    this.#autofillInProgress = true;
    let result = new Map();
    try {
      Services.obs.notifyObservers(null, "autofill-fill-starting");
      const handler = this.#getHandlerByElementId(elementIds[0]);
      handler.fillFields(focusedId, elementIds, profile);

      // Return the autofilled result to the parent. The result
      // is used by both tests and telemetry.
      result = handler.collectFormFilledData();

      Services.obs.notifyObservers(null, "autofill-fill-complete");
    } catch {}

    this.#autofillInProgress = false;
    return result;
  }

  /**
   * Returns all the identified fields for this document.
   * This function is only used by about:autofill extension.
   */
  inspectFields() {
    const elements = Array.from(
      this.document.querySelectorAll("input, select")
    );

    const roots = new Set();
    const fieldDetails = [];
    for (const element of elements) {
      const formLike = lazy.FormLikeFactory.createFromField(element);
      if (roots.has(formLike.rootElement)) {
        continue;
      }
      roots.add(formLike.rootElement);
      const handler = new lazy.FormAutofillHandler(formLike);

      // Fields that cannot be recognized will still be reported with this API.
      const fields = lazy.FormAutofillHandler.collectFormFields(handler.form);
      fieldDetails.push(...fields);
    }

    // For inspection, we want to return the field according to their order
    return elements
      .map(element => fieldDetails.find(field => field.element == element))
      .filter(field => !!field);
  }

  #markAsAutofillField(fieldDetail) {
    const element = fieldDetail.element;

    // Since Form Autofill popup is only for input element, any non-Input
    // element should be excluded here.
    if (!HTMLInputElement.isInstance(element)) {
      return;
    }

    this.manager
      .getActor("AutoComplete")
      ?.markAsAutoCompletableField(element, this);
  }

  get actorName() {
    return "FormAutofill";
  }

  /**
   * Get the search options when searching for autocomplete entries in the parent
   *
   * @param {HTMLInputElement} input - The input element to search for autocomplete entries
   * @returns {object} the search options for the input
   */
  getAutoCompleteSearchOption(input) {
    const fieldDetail = this._fieldDetailsManager
      .getFormHandler(input)
      ?.getFieldDetailByElement(input);

    const scenarioName = lazy.FormScenarios.detect({ input }).signUpForm
      ? "SignUpFormScenario"
      : "";
    return {
      fieldName: fieldDetail?.fieldName,
      elementId: fieldDetail?.elementId,
      scenarioName,
    };
  }

  /**
   * Ask the provider whether it might have autocomplete entry to show
   * for the given input.
   *
   * @param {HTMLInputElement} input - The input element to search for autocomplete entries
   * @returns {boolean} true if we shold search for autocomplete entries
   */
  shouldSearchForAutoComplete(input) {
    const fieldDetail = this._fieldDetailsManager
      .getFormHandler(input)
      ?.getFieldDetailByElement(input);
    if (!fieldDetail) {
      return false;
    }
    const fieldName = fieldDetail.fieldName;
    const isAddressField = lazy.FormAutofillUtils.isAddressField(fieldName);
    const searchPermitted = isAddressField
      ? lazy.FormAutofill.isAutofillAddressesEnabled
      : lazy.FormAutofill.isAutofillCreditCardsEnabled;
    // If the specified autofill feature is pref off, do not search
    if (!searchPermitted) {
      return false;
    }

    // No profile can fill the currently-focused input.
    if (!lazy.FormAutofillContent.savedFieldNames.has(fieldName)) {
      return false;
    }

    return true;
  }

  /**
   * Convert the search result to autocomplete results
   *
   * @param {string} searchString - The string to search for
   * @param {HTMLInputElement} input - The input element to search for autocomplete entries
   * @param {Array<object>} records - autocomplete records
   * @returns {AutocompleteResult}
   */
  searchResultToAutoCompleteResult(searchString, input, records) {
    if (!records) {
      return null;
    }

    const handler = this._fieldDetailsManager.getFormHandler(input);
    const fieldDetail = handler?.getFieldDetailByElement(input);
    if (!fieldDetail) {
      return null;
    }

    const adaptedRecords = handler.getAdaptedProfiles(records.records);
    const isSecure = lazy.InsecurePasswordUtils.isFormSecure(handler.form);
    const isInputAutofilled =
      input.autofillState == lazy.FormAutofillUtils.FIELD_STATES.AUTO_FILLED;

    let AutocompleteResult;

    // TODO: This should be calculated in the parent
    // The field categories will be filled if the corresponding profile is
    // used for autofill. We don't display this information for credit
    // cards, so this is only calculated for address fields.
    let fillCategories;
    if (lazy.FormAutofillUtils.isAddressField(fieldDetail.fieldName)) {
      AutocompleteResult = lazy.AddressResult;
      fillCategories = adaptedRecords.map(profile => {
        const fields = Object.keys(profile).filter(fieldName => {
          const detail = handler.getFieldDetailByName(fieldName);
          return detail ? handler.isFieldAutofillable(detail, profile) : false;
        });
        return lazy.FormAutofillUtils.getCategoriesFromFieldNames(fields);
      });
    } else {
      AutocompleteResult = lazy.CreditCardResult;
    }

    const acResult = new AutocompleteResult(
      searchString,
      fieldDetail,
      records.allFieldNames,
      adaptedRecords,
      fillCategories,
      { isSecure, isInputAutofilled }
    );

    const externalEntries = records.externalEntries;

    acResult.externalEntries.push(
      ...externalEntries.map(
        entry =>
          new lazy.GenericAutocompleteItem(
            entry.image,
            entry.label,
            entry.secondary,
            entry.fillMessageName,
            entry.fillMessageData
          )
      )
    );

    return acResult;
  }

  #getHandlerByElementId(elementId) {
    const element = lazy.FormAutofillUtils.getElementByIdentifier(elementId);
    return this._fieldDetailsManager.getFormHandler(element);
  }
}
