summaryrefslogtreecommitdiffstats
path: root/toolkit/components/formautofill/FormAutofillContent.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/formautofill/FormAutofillContent.sys.mjs')
-rw-r--r--toolkit/components/formautofill/FormAutofillContent.sys.mjs442
1 files changed, 442 insertions, 0 deletions
diff --git a/toolkit/components/formautofill/FormAutofillContent.sys.mjs b/toolkit/components/formautofill/FormAutofillContent.sys.mjs
new file mode 100644
index 0000000000..133e5e1d0a
--- /dev/null
+++ b/toolkit/components/formautofill/FormAutofillContent.sys.mjs
@@ -0,0 +1,442 @@
+/* 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 */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
+ FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+ FormStateManager: "resource://gre/modules/shared/FormStateManager.sys.mjs",
+ ProfileAutocomplete:
+ "resource://autofill/AutofillProfileAutoComplete.sys.mjs",
+ AutofillTelemetry: "resource://autofill/AutofillTelemetry.sys.mjs",
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "DELEGATE_AUTOCOMPLETE",
+ "toolkit.autocomplete.delegate",
+ false
+);
+
+const formFillController = Cc[
+ "@mozilla.org/satchel/form-fill-controller;1"
+].getService(Ci.nsIFormFillController);
+
+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;
+}
+
+/**
+ * Handles content's interactions for the process.
+ *
+ */
+export var FormAutofillContent = {
+ /**
+ * @type {Set} Set of the fields with usable values in any saved profile.
+ */
+ get savedFieldNames() {
+ return Services.cpmm.sharedData.get("FormAutofill:savedFieldNames");
+ },
+
+ /**
+ * @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) {
+ lazy.ProfileAutocomplete.ensureRegistered();
+ }
+
+ /**
+ * @type {FormAutofillFieldDetailsManager} handling state management of current forms and handlers.
+ */
+ this._fieldDetailsManager = new lazy.FormStateManager(
+ this.formSubmitted.bind(this),
+ this._showPopup.bind(this)
+ );
+ },
+
+ get activeFieldDetail() {
+ return this._fieldDetailsManager.activeFieldDetail;
+ },
+
+ get activeFormDetails() {
+ return this._fieldDetailsManager.activeFormDetails;
+ },
+
+ get activeInput() {
+ return this._fieldDetailsManager.activeInput;
+ },
+
+ get activeHandler() {
+ return this._fieldDetailsManager.activeHandler;
+ },
+
+ get activeSection() {
+ return this._fieldDetailsManager.activeSection;
+ },
+
+ /**
+ * 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 {string} formSubmissionReason Reason for invoking the form submission
+ * (see options for FORM_SUBMISSION_REASON in FormAutofillUtils))
+ * @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,
+ formSubmissionReason,
+ domWin = formElement.ownerGlobal,
+ handler = undefined
+ ) {
+ this.debug(`Handling form submission - infered by ${formSubmissionReason}`);
+
+ // Unregister the progress listener since we detected a form submission
+ // (domWin is null in unit tests)
+ getActorFromWindow(domWin)?.unregisterProgressListener();
+
+ 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.
+ if (domWin && lazy.PrivateBrowsingUtils.isContentWindowPrivate(domWin)) {
+ this.debug("Ignoring submission in a private window");
+ return;
+ }
+
+ handler = handler || this._fieldDetailsManager._getFormHandler(formElement);
+ const records = this._fieldDetailsManager.getRecords(formElement, handler);
+
+ if (!records || !handler) {
+ this.debug("Form element could not map to an existing handler");
+ 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);
+ },
+
+ _showPopup() {
+ formFillController.showPopup();
+ },
+
+ handleEvent(evt) {
+ switch (evt.type) {
+ case "change": {
+ if (!evt.changedKeys.includes("FormAutofill:enabled")) {
+ return;
+ }
+ if (Services.cpmm.sharedData.get("FormAutofill:enabled")) {
+ lazy.ProfileAutocomplete.ensureRegistered();
+ if (this._popupPending) {
+ this._popupPending = false;
+ this.debug("handleEvent: Opening deferred popup");
+ this._showPopup();
+ }
+ } else {
+ lazy.ProfileAutocomplete.ensureUnregistered();
+ }
+ break;
+ }
+ }
+ },
+
+ /**
+ * 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");
+ return;
+ }
+ this._fieldDetailsManager.updateActiveInput(element);
+ 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");
+ this._showPopup();
+ } else {
+ this.debug(
+ "updateActiveElement: Deferring pop-up until Autofill is ready"
+ );
+ this._popupPending = true;
+ }
+ }
+ }
+ },
+
+ set autofillPending(flag) {
+ this.debug("Setting autofillPending to", flag);
+ this._autofillPending = flag;
+ },
+
+ /**
+ * Identifies and marks each autofill field
+ *
+ * @param {HTMLElement} element
+ * Element that serves as an anchor for the formautofill heuristics to retrieve
+ * the root form and run the formautofill heuristics on the form elements
+ * @returns {boolean}
+ * whether any autofill fields were identified
+ */
+ 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");
+ }
+ }
+
+ const validDetails =
+ this._fieldDetailsManager.identifyAutofillFields(element);
+
+ validDetails?.forEach(detail => this._markAsAutofillField(detail.element));
+
+ return !!validDetails.length;
+ },
+
+ clearForm() {
+ let focusedInput =
+ this.activeInput ||
+ lazy.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 = lazy.ProfileAutocomplete._getSelectedIndex(docWin);
+ let lastAutoCompleteResult =
+ lazy.ProfileAutocomplete.lastProfileAutoCompleteResult;
+ let focusedInput = this.activeInput;
+ let actor = getActorFromWindow(docWin);
+
+ if (
+ selectedIndex === -1 ||
+ !focusedInput ||
+ !lastAutoCompleteResult ||
+ lastAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile"
+ ) {
+ actor.sendAsyncMessage("FormAutofill:UpdateWarningMessage", {});
+
+ lazy.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,
+ });
+
+ lazy.ProfileAutocomplete._previewSelectedProfile(selectedIndex);
+ }
+ },
+
+ onPopupClosed(selectedRowStyle) {
+ this.debug("Popup has closed.");
+ lazy.ProfileAutocomplete._clearProfilePreview();
+
+ let lastAutoCompleteResult =
+ lazy.ProfileAutocomplete.lastProfileAutoCompleteResult;
+ let focusedInput = FormAutofillContent.activeInput;
+ if (
+ lastAutoCompleteResult &&
+ FormAutofillContent._keyDownEnterForInput &&
+ focusedInput === FormAutofillContent._keyDownEnterForInput &&
+ focusedInput ===
+ lazy.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;
+ if (fieldName && this.activeSection) {
+ 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 =
+ lazy.ProfileAutocomplete.lastProfileAutoCompleteResult;
+ let focusedInput = FormAutofillContent.activeInput;
+ if (
+ e.keyCode != e.DOM_VK_RETURN ||
+ !lastAutoCompleteResult ||
+ !focusedInput ||
+ focusedInput !=
+ lazy.ProfileAutocomplete.lastProfileAutoCompleteFocusedInput
+ ) {
+ return;
+ }
+ FormAutofillContent._keyDownEnterForInput = focusedInput;
+ },
+
+ didDestroy() {
+ this._fieldDetailsManager.didDestroy();
+ },
+};
+
+FormAutofillContent.init();