summaryrefslogtreecommitdiffstats
path: root/toolkit/components/formautofill/FormAutofillContent.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/formautofill/FormAutofillContent.jsm')
-rw-r--r--toolkit/components/formautofill/FormAutofillContent.jsm890
1 files changed, 890 insertions, 0 deletions
diff --git a/toolkit/components/formautofill/FormAutofillContent.jsm b/toolkit/components/formautofill/FormAutofillContent.jsm
new file mode 100644
index 0000000000..646c30c3d6
--- /dev/null
+++ b/toolkit/components/formautofill/FormAutofillContent.jsm
@@ -0,0 +1,890 @@
+/* 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/. */
+
+/**
+ * Form Autofill content process module.
+ */
+
+/* eslint-disable no-use-before-define */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["FormAutofillContent"];
+
+const Cm = Components.manager;
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ FormLikeFactory: "resource://gre/modules/FormLikeFactory.sys.mjs",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ AddressResult: "resource://autofill/ProfileAutoCompleteResult.jsm",
+ ComponentUtils: "resource://gre/modules/ComponentUtils.jsm",
+ CreditCardResult: "resource://autofill/ProfileAutoCompleteResult.jsm",
+ AutofillTelemetry: "resource://autofill/AutofillTelemetry.jsm",
+ FormAutofill: "resource://autofill/FormAutofill.jsm",
+ FormAutofillHandler: "resource://autofill/FormAutofillHandler.jsm",
+ FormAutofillUtils: "resource://autofill/FormAutofillUtils.jsm",
+ InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.jsm",
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "DELEGATE_AUTOCOMPLETE",
+ "toolkit.autocomplete.delegate",
+ false
+);
+
+const formFillController = Cc[
+ "@mozilla.org/satchel/form-fill-controller;1"
+].getService(Ci.nsIFormFillController);
+const autocompleteController = Cc[
+ "@mozilla.org/autocomplete/controller;1"
+].getService(Ci.nsIAutoCompleteController);
+
+XPCOMUtils.defineLazyGetter(
+ lazy,
+ "ADDRESSES_COLLECTION_NAME",
+ () => lazy.FormAutofillUtils.ADDRESSES_COLLECTION_NAME
+);
+XPCOMUtils.defineLazyGetter(
+ lazy,
+ "CREDITCARDS_COLLECTION_NAME",
+ () => lazy.FormAutofillUtils.CREDITCARDS_COLLECTION_NAME
+);
+XPCOMUtils.defineLazyGetter(
+ lazy,
+ "FIELD_STATES",
+ () => lazy.FormAutofillUtils.FIELD_STATES
+);
+
+function getActorFromWindow(contentWindow, name = "FormAutofill") {
+ // In unit tests, contentWindow isn't a real window.
+ if (!contentWindow) {
+ return null;
+ }
+
+ return contentWindow.windowGlobalChild
+ ? contentWindow.windowGlobalChild.getActor(name)
+ : null;
+}
+
+// Register/unregister a constructor as a factory.
+function AutocompleteFactory() {}
+AutocompleteFactory.prototype = {
+ register(targetConstructor) {
+ let proto = targetConstructor.prototype;
+ this._classID = proto.classID;
+
+ let factory = lazy.ComponentUtils.generateSingletonFactory(
+ targetConstructor
+ );
+ this._factory = factory;
+
+ let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.registerFactory(
+ proto.classID,
+ proto.classDescription,
+ proto.contractID,
+ factory
+ );
+
+ if (proto.classID2) {
+ this._classID2 = proto.classID2;
+ registrar.registerFactory(
+ proto.classID2,
+ proto.classDescription,
+ proto.contractID2,
+ factory
+ );
+ }
+ },
+
+ unregister() {
+ let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.unregisterFactory(this._classID, this._factory);
+ if (this._classID2) {
+ registrar.unregisterFactory(this._classID2, this._factory);
+ }
+ this._factory = null;
+ },
+};
+
+/**
+ * @class
+ *
+ * @implements {nsIAutoCompleteSearch}
+ */
+function AutofillProfileAutoCompleteSearch() {
+ this.log = lazy.FormAutofill.defineLogGetter(
+ this,
+ "AutofillProfileAutoCompleteSearch"
+ );
+}
+AutofillProfileAutoCompleteSearch.prototype = {
+ classID: Components.ID("4f9f1e4c-7f2c-439e-9c9e-566b68bc187d"),
+ contractID: "@mozilla.org/autocomplete/search;1?name=autofill-profiles",
+ classDescription: "AutofillProfileAutoCompleteSearch",
+ QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteSearch"]),
+
+ // Begin nsIAutoCompleteSearch implementation
+
+ /**
+ * Searches for a given string and notifies a listener (either synchronously
+ * or asynchronously) of the result
+ *
+ * @param {string} searchString the string to search for
+ * @param {string} searchParam
+ * @param {object} previousResult a previous result to use for faster searchinig
+ * @param {object} listener the listener to notify when the search is complete
+ */
+ startSearch(searchString, searchParam, previousResult, listener) {
+ let {
+ activeInput,
+ activeSection,
+ activeFieldDetail,
+ savedFieldNames,
+ } = FormAutofillContent;
+ this.forceStop = false;
+
+ let isAddressField = lazy.FormAutofillUtils.isAddressField(
+ activeFieldDetail.fieldName
+ );
+ const isCreditCardField = lazy.FormAutofillUtils.isCreditCardField(
+ activeFieldDetail.fieldName
+ );
+ let isInputAutofilled =
+ activeFieldDetail.state == lazy.FIELD_STATES.AUTO_FILLED;
+ let allFieldNames = activeSection.allFieldNames;
+ let filledRecordGUID = activeSection.filledRecordGUID;
+
+ let searchPermitted = isAddressField
+ ? lazy.FormAutofill.isAutofillAddressesEnabled
+ : lazy.FormAutofill.isAutofillCreditCardsEnabled;
+ let AutocompleteResult = isAddressField
+ ? lazy.AddressResult
+ : lazy.CreditCardResult;
+ let isFormAutofillSearch = true;
+ let pendingSearchResult = null;
+
+ ProfileAutocomplete.lastProfileAutoCompleteFocusedInput = activeInput;
+ // Fallback to form-history if ...
+ // - specified autofill feature is pref off.
+ // - no profile can fill the currently-focused input.
+ // - the current form has already been populated and the field is not
+ // an empty credit card field.
+ // - (address only) less than 3 inputs are covered by all saved fields in the storage.
+ if (
+ !searchPermitted ||
+ !savedFieldNames.has(activeFieldDetail.fieldName) ||
+ (!isInputAutofilled &&
+ filledRecordGUID &&
+ !(isCreditCardField && activeInput.value === "")) ||
+ (isAddressField &&
+ allFieldNames.filter(field => savedFieldNames.has(field)).length <
+ lazy.FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD)
+ ) {
+ isFormAutofillSearch = false;
+ if (activeInput.autocomplete == "off") {
+ // Create a dummy result as an empty search result.
+ pendingSearchResult = new AutocompleteResult("", "", [], [], {});
+ } else {
+ pendingSearchResult = new Promise(resolve => {
+ let formHistory = Cc[
+ "@mozilla.org/autocomplete/search;1?name=form-history"
+ ].createInstance(Ci.nsIAutoCompleteSearch);
+ formHistory.startSearch(searchString, searchParam, previousResult, {
+ onSearchResult: (_, result) => resolve(result),
+ });
+ });
+ }
+ } else if (isInputAutofilled) {
+ pendingSearchResult = new AutocompleteResult(searchString, "", [], [], {
+ isInputAutofilled,
+ });
+ } else {
+ let infoWithoutElement = { ...activeFieldDetail };
+ delete infoWithoutElement.elementWeakRef;
+
+ let data = {
+ collectionName: isAddressField
+ ? lazy.ADDRESSES_COLLECTION_NAME
+ : lazy.CREDITCARDS_COLLECTION_NAME,
+ info: infoWithoutElement,
+ searchString,
+ };
+
+ pendingSearchResult = this._getRecords(activeInput, data).then(
+ records => {
+ if (this.forceStop) {
+ return null;
+ }
+ // Sort addresses by timeLastUsed for showing the lastest used address at top.
+ records.sort((a, b) => b.timeLastUsed - a.timeLastUsed);
+
+ let adaptedRecords = activeSection.getAdaptedProfiles(records);
+ let handler = FormAutofillContent.activeHandler;
+ let isSecure = lazy.InsecurePasswordUtils.isFormSecure(handler.form);
+
+ return new AutocompleteResult(
+ searchString,
+ activeFieldDetail.fieldName,
+ allFieldNames,
+ adaptedRecords,
+ { isSecure, isInputAutofilled }
+ );
+ }
+ );
+ }
+
+ Promise.resolve(pendingSearchResult).then(result => {
+ if (this.forceStop) {
+ // If we notify the listener the search result when the search is already
+ // cancelled, it corrupts the internal state of the listener. So we only
+ // reset the controller's state in this case.
+ if (isFormAutofillSearch) {
+ autocompleteController.resetInternalState();
+ }
+ return;
+ }
+
+ listener.onSearchResult(this, result);
+ // Don't save cache results or reset state when returning non-autofill results such as the
+ // form history fallback above.
+ if (isFormAutofillSearch) {
+ ProfileAutocomplete.lastProfileAutoCompleteResult = result;
+ // Reset AutoCompleteController's state at the end of startSearch to ensure that
+ // none of form autofill result will be cached in other places and make the
+ // result out of sync.
+ autocompleteController.resetInternalState();
+ } else {
+ // Clear the cache so that we don't try to autofill from it after falling
+ // back to form history.
+ ProfileAutocomplete.lastProfileAutoCompleteResult = null;
+ }
+ });
+ },
+
+ /**
+ * Stops an asynchronous search that is in progress
+ */
+ stopSearch() {
+ ProfileAutocomplete.lastProfileAutoCompleteResult = null;
+ this.forceStop = true;
+ },
+
+ /**
+ * Get the records from parent process for AutoComplete result.
+ *
+ * @private
+ * @param {object} input
+ * Input element for autocomplete.
+ * @param {object} data
+ * Parameters for querying the corresponding result.
+ * @param {string} data.collectionName
+ * The name used to specify which collection to retrieve records.
+ * @param {string} data.searchString
+ * The typed string for filtering out the matched records.
+ * @param {string} data.info
+ * The input autocomplete property's information.
+ * @returns {Promise}
+ * Promise that resolves when addresses returned from parent process.
+ */
+ _getRecords(input, data) {
+ if (!input) {
+ return [];
+ }
+
+ let actor = getActorFromWindow(input.ownerGlobal);
+ return actor.sendQuery("FormAutofill:GetRecords", data);
+ },
+};
+
+let ProfileAutocomplete = {
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+
+ lastProfileAutoCompleteResult: null,
+ lastProfileAutoCompleteFocusedInput: null,
+ _registered: false,
+ _factory: null,
+
+ ensureRegistered() {
+ if (this._registered) {
+ return;
+ }
+
+ this.log = lazy.FormAutofill.defineLogGetter(this, "ProfileAutocomplete");
+ this.debug("ensureRegistered");
+ this._factory = new AutocompleteFactory();
+ this._factory.register(AutofillProfileAutoCompleteSearch);
+ this._registered = true;
+
+ Services.obs.addObserver(this, "autocomplete-will-enter-text");
+
+ this.debug(
+ "ensureRegistered. Finished with _registered:",
+ this._registered
+ );
+ },
+
+ ensureUnregistered() {
+ if (!this._registered) {
+ return;
+ }
+
+ this.debug("ensureUnregistered");
+ this._factory.unregister();
+ this._factory = null;
+ this._registered = false;
+ this._lastAutoCompleteResult = null;
+
+ Services.obs.removeObserver(this, "autocomplete-will-enter-text");
+ },
+
+ async observe(subject, topic, data) {
+ switch (topic) {
+ case "autocomplete-will-enter-text": {
+ if (!FormAutofillContent.activeInput) {
+ // The observer notification is for autocomplete in a different process.
+ break;
+ }
+ FormAutofillContent.autofillPending = true;
+ Services.obs.notifyObservers(null, "autofill-fill-starting");
+ await this._fillFromAutocompleteRow(FormAutofillContent.activeInput);
+ Services.obs.notifyObservers(null, "autofill-fill-complete");
+ FormAutofillContent.autofillPending = false;
+ break;
+ }
+ }
+ },
+
+ _getSelectedIndex(contentWindow) {
+ let actor = getActorFromWindow(contentWindow, "AutoComplete");
+ if (!actor) {
+ throw new Error("Invalid autocomplete selectedIndex");
+ }
+
+ return actor.selectedIndex;
+ },
+
+ async _fillFromAutocompleteRow(focusedInput) {
+ this.debug("_fillFromAutocompleteRow:", focusedInput);
+ let formDetails = FormAutofillContent.activeFormDetails;
+ if (!formDetails) {
+ // The observer notification is for a different frame.
+ return;
+ }
+
+ let selectedIndex = this._getSelectedIndex(focusedInput.ownerGlobal);
+ if (
+ selectedIndex == -1 ||
+ !this.lastProfileAutoCompleteResult ||
+ this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) !=
+ "autofill-profile"
+ ) {
+ return;
+ }
+
+ let profile = JSON.parse(
+ this.lastProfileAutoCompleteResult.getCommentAt(selectedIndex)
+ );
+
+ await FormAutofillContent.activeHandler.autofillFormFields(profile);
+ },
+
+ _clearProfilePreview() {
+ if (
+ !this.lastProfileAutoCompleteFocusedInput ||
+ !FormAutofillContent.activeSection
+ ) {
+ return;
+ }
+
+ FormAutofillContent.activeSection.clearPreviewedFormFields();
+ },
+
+ _previewSelectedProfile(selectedIndex) {
+ if (
+ !FormAutofillContent.activeInput ||
+ !FormAutofillContent.activeFormDetails
+ ) {
+ // The observer notification is for a different process/frame.
+ return;
+ }
+
+ if (
+ !this.lastProfileAutoCompleteResult ||
+ this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) !=
+ "autofill-profile"
+ ) {
+ return;
+ }
+
+ let profile = JSON.parse(
+ this.lastProfileAutoCompleteResult.getCommentAt(selectedIndex)
+ );
+ FormAutofillContent.activeSection.previewFormFields(profile);
+ },
+};
+
+/**
+ * Handles content's interactions for the process.
+ *
+ * NOTE: Declares it by "var" to make it accessible in unit tests.
+ */
+var FormAutofillContent = {
+ /**
+ * @type {WeakMap} mapping FormLike root HTML elements to FormAutofillHandler objects.
+ */
+ _formsDetails: new WeakMap(),
+
+ /**
+ * @type {Set} Set of the fields with usable values in any saved profile.
+ */
+ get savedFieldNames() {
+ return Services.cpmm.sharedData.get("FormAutofill:savedFieldNames");
+ },
+
+ /**
+ * @type {object} The object where to store the active items, e.g. element,
+ * handler, section, and field detail.
+ */
+ _activeItems: {},
+
+ /**
+ * @type {boolean} Flag indicating whether a focus action requiring
+ * the popup to be active is pending.
+ */
+ _popupPending: false,
+
+ /**
+ * @type {boolean} Flag indicating whether the form is waiting to be
+ * filled by Autofill.
+ */
+ _autofillPending: false,
+
+ init() {
+ this.log = lazy.FormAutofill.defineLogGetter(this, "FormAutofillContent");
+ this.debug("init");
+
+ // eslint-disable-next-line mozilla/balanced-listeners
+ Services.cpmm.sharedData.addEventListener("change", this);
+
+ let autofillEnabled = Services.cpmm.sharedData.get("FormAutofill:enabled");
+ // If storage hasn't be initialized yet autofillEnabled is undefined but we need to ensure
+ // autocomplete is registered before the focusin so register it in this case as long as the
+ // pref is true.
+ let shouldEnableAutofill =
+ autofillEnabled === undefined &&
+ (lazy.FormAutofill.isAutofillAddressesEnabled ||
+ lazy.FormAutofill.isAutofillCreditCardsEnabled);
+ if (autofillEnabled || shouldEnableAutofill) {
+ ProfileAutocomplete.ensureRegistered();
+ }
+ },
+
+ /**
+ * Send the profile to parent for doorhanger and storage saving/updating.
+ *
+ * @param {object} profile Submitted form's address/creditcard guid and record.
+ * @param {object} domWin Current content window.
+ */
+ _onFormSubmit(profile, domWin) {
+ let actor = getActorFromWindow(domWin);
+ actor.sendAsyncMessage("FormAutofill:OnFormSubmit", profile);
+ },
+
+ /**
+ * 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 {Window} domWin Content window; passed for unit tests and when
+ * invoked by the FormAutofillSection
+ * @param {object} handler FormAutofillHander, if known by caller
+ */
+ formSubmitted(
+ formElement,
+ domWin = formElement.ownerGlobal,
+ handler = undefined
+ ) {
+ this.debug("Handling form submission");
+
+ if (!lazy.FormAutofill.isAutofillEnabled) {
+ this.debug("Form Autofill is disabled");
+ return;
+ }
+
+ // The `domWin` truthiness test is used by unit tests to bypass this check.
+ if (domWin && lazy.PrivateBrowsingUtils.isContentWindowPrivate(domWin)) {
+ this.debug("Ignoring submission in a private window");
+ return;
+ }
+
+ handler = handler ?? this._formsDetails.get(formElement);
+ if (!handler) {
+ this.debug("Form element could not map to an existing handler");
+ return;
+ }
+
+ let records = handler.createRecords();
+ if (!Object.values(records).some(typeRecords => typeRecords.length)) {
+ return;
+ }
+
+ [records.address, records.creditCard].forEach((rs, idx) => {
+ lazy.AutofillTelemetry.recordSubmittedSectionCount(
+ idx == 0
+ ? lazy.AutofillTelemetry.ADDRESS
+ : lazy.AutofillTelemetry.CREDIT_CARD,
+ rs?.length
+ );
+
+ rs?.forEach(r => {
+ lazy.AutofillTelemetry.recordFormInteractionEvent(
+ "submitted",
+ r.section,
+ {
+ record: r,
+ form: handler.form,
+ }
+ );
+ delete r.section;
+ });
+ });
+
+ this._onFormSubmit(records, domWin);
+ },
+
+ handleEvent(evt) {
+ switch (evt.type) {
+ case "change": {
+ if (!evt.changedKeys.includes("FormAutofill:enabled")) {
+ return;
+ }
+ if (Services.cpmm.sharedData.get("FormAutofill:enabled")) {
+ ProfileAutocomplete.ensureRegistered();
+ if (this._popupPending) {
+ this._popupPending = false;
+ this.debug("handleEvent: Opening deferred popup");
+ formFillController.showPopup();
+ }
+ } else {
+ ProfileAutocomplete.ensureUnregistered();
+ }
+ break;
+ }
+ }
+ },
+
+ /**
+ * Get the form's handler from cache which is created after page identified.
+ *
+ * @param {HTMLInputElement} element Focused input which triggered profile searching
+ * @returns {Array<object> | null}
+ * Return target form's handler from content cache
+ * (or return null if the information is not found in the cache).
+ *
+ */
+ _getFormHandler(element) {
+ if (!element) {
+ return null;
+ }
+ let rootElement = lazy.FormLikeFactory.findRootForField(element);
+ return this._formsDetails.get(rootElement);
+ },
+
+ /**
+ * Get the active form's information from cache which is created after page
+ * identified.
+ *
+ * @returns {Array<object> | null}
+ * Return target form's information from content cache
+ * (or return null if the information is not found in the cache).
+ *
+ */
+ get activeFormDetails() {
+ let formHandler = this.activeHandler;
+ return formHandler ? formHandler.fieldDetails : null;
+ },
+
+ /**
+ * All active items should be updated according the active element of
+ * `formFillController.focusedInput`. All of them including element,
+ * handler, section, and field detail, can be retrieved by their own getters.
+ *
+ * @param {HTMLElement|null} element The active item should be updated based
+ * on this or `formFillController.focusedInput` will be taken.
+ */
+ updateActiveInput(element) {
+ element = element || formFillController.focusedInput;
+ if (!element) {
+ this.debug("updateActiveElement: no element selected");
+ this._activeItems = {};
+ return;
+ }
+ this._activeItems = {
+ elementWeakRef: Cu.getWeakReference(element),
+ fieldDetail: null,
+ };
+
+ this.debug("updateActiveElement: checking for popup-on-focus");
+ // We know this element just received focus. If it's a credit card field,
+ // open its popup.
+ if (this._autofillPending) {
+ this.debug("updateActiveElement: skipping check; autofill is imminent");
+ } else if (element.value?.length !== 0) {
+ this.debug(
+ `updateActiveElement: Not opening popup because field is not empty.`
+ );
+ } else {
+ this.debug(
+ "updateActiveElement: checking if empty field is cc-*: ",
+ this.activeFieldDetail?.fieldName
+ );
+
+ if (
+ this.activeFieldDetail?.fieldName?.startsWith("cc-") ||
+ AppConstants.platform === "android"
+ ) {
+ if (Services.cpmm.sharedData.get("FormAutofill:enabled")) {
+ this.debug("updateActiveElement: opening pop up");
+ formFillController.showPopup();
+ } else {
+ this.debug(
+ "updateActiveElement: Deferring pop-up until Autofill is ready"
+ );
+ this._popupPending = true;
+ }
+ }
+ }
+ },
+
+ get activeInput() {
+ let elementWeakRef = this._activeItems.elementWeakRef;
+ return elementWeakRef ? elementWeakRef.get() : null;
+ },
+
+ get activeHandler() {
+ const activeInput = this.activeInput;
+ if (!activeInput) {
+ return null;
+ }
+
+ // XXX: We are recomputing the activeHandler every time to avoid keeping a
+ // reference on the active element. This might be called quite frequently
+ // so if _getFormHandler/findRootForField become more costly, we should
+ // look into caching this result (eg by adding a weakmap).
+ let handler = this._getFormHandler(activeInput);
+ if (handler) {
+ handler.focusedInput = activeInput;
+ }
+ return handler;
+ },
+
+ get activeSection() {
+ let formHandler = this.activeHandler;
+ return formHandler ? formHandler.activeSection : null;
+ },
+
+ /**
+ * Get the active input's information from cache which is created after page
+ * identified.
+ *
+ * @returns {object | null}
+ * Return the active input's information that cloned from content cache
+ * (or return null if the information is not found in the cache).
+ */
+ get activeFieldDetail() {
+ if (!this._activeItems.fieldDetail) {
+ let formDetails = this.activeFormDetails;
+ if (!formDetails) {
+ return null;
+ }
+ for (let detail of formDetails) {
+ let detailElement = detail.elementWeakRef.get();
+ if (detailElement && this.activeInput == detailElement) {
+ this._activeItems.fieldDetail = detail;
+ break;
+ }
+ }
+ }
+ return this._activeItems.fieldDetail;
+ },
+
+ set autofillPending(flag) {
+ this.debug("Setting autofillPending to", flag);
+ this._autofillPending = flag;
+ },
+
+ identifyAutofillFields(element) {
+ this.debug(
+ `identifyAutofillFields: ${element.ownerDocument.location?.hostname}`
+ );
+
+ if (lazy.DELEGATE_AUTOCOMPLETE || !this.savedFieldNames) {
+ this.debug("identifyAutofillFields: savedFieldNames are not known yet");
+ let actor = getActorFromWindow(element.ownerGlobal);
+ if (actor) {
+ actor.sendAsyncMessage("FormAutofill:InitStorage");
+ }
+ }
+
+ let formHandler = this._getFormHandler(element);
+ if (!formHandler) {
+ let formLike = lazy.FormLikeFactory.createFromField(element);
+ formHandler = new lazy.FormAutofillHandler(
+ formLike,
+ this.formSubmitted.bind(this)
+ );
+ } else if (!formHandler.updateFormIfNeeded(element)) {
+ this.debug("No control is removed or inserted since last collection.");
+ return;
+ }
+ let validDetails = formHandler.collectFormFields();
+
+ this._formsDetails.set(formHandler.form.rootElement, formHandler);
+
+ validDetails.forEach(detail =>
+ this._markAsAutofillField(detail.elementWeakRef.get())
+ );
+ },
+
+ clearForm() {
+ let focusedInput =
+ this.activeInput || ProfileAutocomplete._lastAutoCompleteFocusedInput;
+ if (!focusedInput) {
+ return;
+ }
+
+ this.activeSection.clearPopulatedForm();
+
+ let fieldName = FormAutofillContent.activeFieldDetail?.fieldName;
+ if (lazy.FormAutofillUtils.isCreditCardField(fieldName)) {
+ lazy.AutofillTelemetry.recordFormInteractionEvent(
+ "cleared",
+ this.activeSection,
+ { fieldName }
+ );
+ }
+ },
+
+ previewProfile(doc) {
+ let docWin = doc.ownerGlobal;
+ let selectedIndex = ProfileAutocomplete._getSelectedIndex(docWin);
+ let lastAutoCompleteResult =
+ ProfileAutocomplete.lastProfileAutoCompleteResult;
+ let focusedInput = this.activeInput;
+ let actor = getActorFromWindow(docWin);
+
+ if (
+ selectedIndex === -1 ||
+ !focusedInput ||
+ !lastAutoCompleteResult ||
+ lastAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile"
+ ) {
+ actor.sendAsyncMessage("FormAutofill:UpdateWarningMessage", {});
+
+ ProfileAutocomplete._clearProfilePreview();
+ } else {
+ let focusedInputDetails = this.activeFieldDetail;
+ let profile = JSON.parse(
+ lastAutoCompleteResult.getCommentAt(selectedIndex)
+ );
+ let allFieldNames = FormAutofillContent.activeSection.allFieldNames;
+ let profileFields = allFieldNames.filter(
+ fieldName => !!profile[fieldName]
+ );
+
+ let focusedCategory = lazy.FormAutofillUtils.getCategoryFromFieldName(
+ focusedInputDetails.fieldName
+ );
+ let categories = lazy.FormAutofillUtils.getCategoriesFromFieldNames(
+ profileFields
+ );
+ actor.sendAsyncMessage("FormAutofill:UpdateWarningMessage", {
+ focusedCategory,
+ categories,
+ });
+
+ ProfileAutocomplete._previewSelectedProfile(selectedIndex);
+ }
+ },
+
+ onPopupClosed(selectedRowStyle) {
+ this.debug("Popup has closed.");
+ ProfileAutocomplete._clearProfilePreview();
+
+ let lastAutoCompleteResult =
+ ProfileAutocomplete.lastProfileAutoCompleteResult;
+ let focusedInput = FormAutofillContent.activeInput;
+ if (
+ lastAutoCompleteResult &&
+ FormAutofillContent._keyDownEnterForInput &&
+ focusedInput === FormAutofillContent._keyDownEnterForInput &&
+ focusedInput === ProfileAutocomplete.lastProfileAutoCompleteFocusedInput
+ ) {
+ if (selectedRowStyle == "autofill-footer") {
+ let actor = getActorFromWindow(focusedInput.ownerGlobal);
+ actor.sendAsyncMessage("FormAutofill:OpenPreferences");
+ } else if (selectedRowStyle == "autofill-clear-button") {
+ FormAutofillContent.clearForm();
+ }
+ }
+ },
+
+ onPopupOpened() {
+ this.debug(
+ "Popup has opened, automatic =",
+ formFillController.passwordPopupAutomaticallyOpened
+ );
+
+ let fieldName = FormAutofillContent.activeFieldDetail?.fieldName;
+ lazy.AutofillTelemetry.recordFormInteractionEvent(
+ "popup_shown",
+ this.activeSection,
+ { fieldName }
+ );
+ },
+
+ _markAsAutofillField(field) {
+ // Since Form Autofill popup is only for input element, any non-Input
+ // element should be excluded here.
+ if (!HTMLInputElement.isInstance(field)) {
+ return;
+ }
+
+ formFillController.markAsAutofillField(field);
+ },
+
+ _onKeyDown(e) {
+ delete FormAutofillContent._keyDownEnterForInput;
+ let lastAutoCompleteResult =
+ ProfileAutocomplete.lastProfileAutoCompleteResult;
+ let focusedInput = FormAutofillContent.activeInput;
+ if (
+ e.keyCode != e.DOM_VK_RETURN ||
+ !lastAutoCompleteResult ||
+ !focusedInput ||
+ focusedInput != ProfileAutocomplete.lastProfileAutoCompleteFocusedInput
+ ) {
+ return;
+ }
+ FormAutofillContent._keyDownEnterForInput = focusedInput;
+ },
+};
+
+FormAutofillContent.init();