summaryrefslogtreecommitdiffstats
path: root/toolkit/components/formautofill/FormAutofillChild.sys.mjs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/formautofill/FormAutofillChild.sys.mjs
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/formautofill/FormAutofillChild.sys.mjs')
-rw-r--r--toolkit/components/formautofill/FormAutofillChild.sys.mjs472
1 files changed, 472 insertions, 0 deletions
diff --git a/toolkit/components/formautofill/FormAutofillChild.sys.mjs b/toolkit/components/formautofill/FormAutofillChild.sys.mjs
new file mode 100644
index 0000000000..c40bfddbce
--- /dev/null
+++ b/toolkit/components/formautofill/FormAutofillChild.sys.mjs
@@ -0,0 +1,472 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AutoCompleteChild: "resource://gre/actors/AutoCompleteChild.sys.mjs",
+ FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
+ FormAutofillContent: "resource://autofill/FormAutofillContent.sys.mjs",
+ FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+const observer = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ // Only handle pushState/replaceState here.
+ if (
+ !(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) ||
+ !(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE)
+ ) {
+ return;
+ }
+ const window = aWebProgress.DOMWindow;
+ const formAutofillChild = window.windowGlobalChild.getActor("FormAutofill");
+ formAutofillChild.onPageNavigation();
+ },
+
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (
+ // if restoring a previously-rendered presentation (bfcache)
+ aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP
+ ) {
+ return;
+ }
+
+ if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_START)) {
+ return;
+ }
+
+ // We only care about when a page triggered a load, not the user. For example:
+ // clicking refresh/back/forward, typing a URL and hitting enter, and loading a bookmark aren't
+ // likely to be when a user wants to save a formautofill data.
+ let channel = aRequest.QueryInterface(Ci.nsIChannel);
+ let triggeringPrincipal = channel.loadInfo.triggeringPrincipal;
+ if (
+ triggeringPrincipal.isNullPrincipal ||
+ triggeringPrincipal.equals(
+ Services.scriptSecurityManager.getSystemPrincipal()
+ )
+ ) {
+ return;
+ }
+
+ // Don't handle history navigation, reload, or pushState not triggered via chrome UI.
+ // e.g. history.go(-1), location.reload(), history.replaceState()
+ if (!(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_NORMAL)) {
+ return;
+ }
+
+ const window = aWebProgress.DOMWindow;
+ const formAutofillChild = window.windowGlobalChild.getActor("FormAutofill");
+ formAutofillChild.onPageNavigation();
+ },
+};
+
+/**
+ * Handles content's interactions for the frame.
+ */
+export class FormAutofillChild extends JSWindowActorChild {
+ constructor() {
+ super();
+
+ this._nextHandleElement = null;
+ this._alreadyDOMContentLoaded = false;
+ this._hasDOMContentLoadedHandler = false;
+ this._hasPendingTask = false;
+ this.testListener = null;
+
+ lazy.AutoCompleteChild.addPopupStateListener(this);
+ }
+
+ didDestroy() {
+ lazy.AutoCompleteChild.removePopupStateListener(this);
+ lazy.FormAutofillContent.didDestroy();
+ }
+
+ popupStateChanged(messageName, data, target) {
+ let docShell;
+ try {
+ docShell = this.docShell;
+ } catch (ex) {
+ lazy.AutoCompleteChild.removePopupStateListener(this);
+ return;
+ }
+
+ if (!lazy.FormAutofill.isAutofillEnabled) {
+ return;
+ }
+
+ const { chromeEventHandler } = docShell;
+
+ switch (messageName) {
+ case "FormAutoComplete:PopupClosed": {
+ lazy.FormAutofillContent.onPopupClosed(data.selectedRowStyle);
+ Services.tm.dispatchToMainThread(() => {
+ chromeEventHandler.removeEventListener(
+ "keydown",
+ lazy.FormAutofillContent._onKeyDown,
+ true
+ );
+ });
+
+ break;
+ }
+ case "FormAutoComplete:PopupOpened": {
+ lazy.FormAutofillContent.onPopupOpened();
+ chromeEventHandler.addEventListener(
+ "keydown",
+ lazy.FormAutofillContent._onKeyDown,
+ true
+ );
+ break;
+ }
+ }
+ }
+
+ /**
+ * Invokes the FormAutofillContent to identify the autofill fields
+ * and consider opening the dropdown menu for the focused field
+ *
+ */
+ _doIdentifyAutofillFields() {
+ if (this._hasPendingTask) {
+ return;
+ }
+ this._hasPendingTask = true;
+
+ lazy.setTimeout(() => {
+ const isAnyFieldIdentified =
+ lazy.FormAutofillContent.identifyAutofillFields(
+ this._nextHandleElement
+ );
+ if (isAnyFieldIdentified) {
+ if (lazy.FormAutofill.captureOnFormRemoval) {
+ this.registerDOMDocFetchSuccessEventListener(
+ this._nextHandleElement.ownerDocument
+ );
+ }
+ if (lazy.FormAutofill.captureOnPageNavigation) {
+ this.registerProgressListener();
+ }
+ }
+
+ this._hasPendingTask = false;
+ this._nextHandleElement = null;
+ // This is for testing purpose only which sends a notification to indicate that the
+ // form has been identified, and ready to open popup.
+ this.sendAsyncMessage("FormAutofill:FieldsIdentified");
+ lazy.FormAutofillContent.updateActiveInput();
+ });
+ }
+
+ /**
+ * Gets the highest accessible docShell
+ *
+ * @returns {DocShell} highest accessible docShell
+ */
+ getHighestDocShell() {
+ const window = this.document.defaultView;
+
+ let docShell;
+ for (
+ let browsingContext = BrowsingContext.getFromWindow(window);
+ browsingContext?.docShell;
+ browsingContext = browsingContext.parent
+ ) {
+ docShell = browsingContext.docShell;
+ }
+
+ return docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ }
+
+ /**
+ * After being notified of a page navigation, we check whether
+ * the navigated window is the active window or one of its parents
+ * (active window = FormAutofillContent.activeHandler.window)
+ *
+ * @returns {boolean} whether the navigation affects the active window
+ */
+ isActiveWindowNavigation() {
+ const activeWindow = lazy.FormAutofillContent.activeHandler.window;
+ const navigatedWindow = this.document.defaultView;
+ const navigatedBrowsingContext =
+ BrowsingContext.getFromWindow(navigatedWindow);
+
+ for (
+ let browsingContext = BrowsingContext.getFromWindow(activeWindow);
+ browsingContext?.docShell;
+ browsingContext = browsingContext.parent
+ ) {
+ if (navigatedBrowsingContext === browsingContext) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Infer a form submission after document is navigated
+ */
+ onPageNavigation() {
+ const activeElement =
+ lazy.FormAutofillContent.activeFieldDetail?.elementWeakRef.deref();
+
+ if (!this.isActiveWindowNavigation()) {
+ return;
+ }
+
+ const formSubmissionReason =
+ lazy.FormAutofillUtils.FORM_SUBMISSION_REASON.PAGE_NAVIGATION;
+
+ // We only capture the form of the active field right now,
+ // this means that we might miss some fields (see bug 1871356)
+ lazy.FormAutofillContent.formSubmitted(activeElement, formSubmissionReason);
+ }
+
+ /**
+ * After a form submission we unregister the
+ * nsIWebProgressListener from the top level doc shell
+ */
+ unregisterProgressListener() {
+ const docShell = this.getHighestDocShell();
+ try {
+ docShell.removeProgressListener(observer);
+ } catch (ex) {
+ // Ignore NS_ERROR_FAILURE if the progress listener was not registered
+ }
+ }
+
+ /**
+ * After a focusin event and after we identified formautofill fields,
+ * we set up a nsIWebProgressListener that notifies of a request state
+ * change or window location change in the top level doc shell
+ */
+ registerProgressListener() {
+ const docShell = this.getHighestDocShell();
+
+ const flags =
+ Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
+ Ci.nsIWebProgress.NOTIFY_LOCATION;
+ try {
+ docShell.addProgressListener(observer, flags);
+ } catch (ex) {
+ // Ignore NS_ERROR_FAILURE if the progress listener was already added
+ }
+ }
+
+ /**
+ * After a focusin event and after we identify formautofill fields,
+ * we set up an event listener for the DOMDocFetchSuccess event
+ *
+ * @param {Document} document The document we want to be notified by of a DOMDocFetchSuccess event
+ */
+ registerDOMDocFetchSuccessEventListener(document) {
+ document.setNotifyFetchSuccess(true);
+
+ // Is removed after a DOMDocFetchSuccess event (bug 1864855)
+ /* eslint-disable mozilla/balanced-listeners */
+ this.docShell.chromeEventHandler.addEventListener(
+ "DOMDocFetchSuccess",
+ this,
+ true
+ );
+ }
+
+ /**
+ * After a DOMDocFetchSuccess event, we register an event listener for the DOMFormRemoved event
+ *
+ * @param {Document} document The document we want to be notified by of a DOMFormRemoved event
+ */
+ registerDOMFormRemovedEventListener(document) {
+ document.setNotifyFormOrPasswordRemoved(true);
+
+ // Is removed after a DOMFormRemoved event (bug 1864855)
+ /* eslint-disable mozilla/balanced-listeners */
+ this.docShell.chromeEventHandler.addEventListener(
+ "DOMFormRemoved",
+ this,
+ true
+ );
+ }
+
+ /**
+ * After a DOMDocFetchSuccess event we remove the DOMDocFetchSuccess event listener
+ *
+ * @param {Document} document The document we are notified by of a DOMDocFetchSuccess event
+ */
+ unregisterDOMDocFetchSuccessEventListener(document) {
+ document.setNotifyFetchSuccess(false);
+ this.docShell.chromeEventHandler.removeEventListener(
+ "DOMDocFetchSuccess",
+ this
+ );
+ }
+
+ /**
+ * After a DOMFormRemoved event we remove the DOMFormRemoved event listener
+ *
+ * @param {Document} document The document we are notified by of a DOMFormRemoved event
+ */
+ unregisterDOMFormRemovedEventListener(document) {
+ document.setNotifyFormOrPasswordRemoved(false);
+ this.docShell.chromeEventHandler.removeEventListener(
+ "DOMFormRemoved",
+ this
+ );
+ }
+
+ shouldIgnoreFormAutofillEvent(event) {
+ let nodePrincipal = event.target.nodePrincipal;
+ return (
+ nodePrincipal.isSystemPrincipal ||
+ nodePrincipal.isNullPrincipal ||
+ nodePrincipal.schemeIs("about")
+ );
+ }
+
+ handleEvent(evt) {
+ if (!evt.isTrusted) {
+ return;
+ }
+ if (this.shouldIgnoreFormAutofillEvent(evt)) {
+ return;
+ }
+
+ switch (evt.type) {
+ case "focusin": {
+ if (lazy.FormAutofill.isAutofillEnabled) {
+ this.onFocusIn(evt);
+ }
+ break;
+ }
+ case "DOMFormBeforeSubmit": {
+ if (lazy.FormAutofill.isAutofillEnabled) {
+ this.onDOMFormBeforeSubmit(evt);
+ }
+ break;
+ }
+ case "DOMFormRemoved": {
+ this.onDOMFormRemoved(evt);
+ break;
+ }
+ case "DOMDocFetchSuccess": {
+ this.onDOMDocFetchSuccess(evt);
+ break;
+ }
+
+ default: {
+ throw new Error("Unexpected event type");
+ }
+ }
+ }
+
+ onFocusIn(evt) {
+ lazy.FormAutofillContent.updateActiveInput();
+
+ let element = evt.target;
+ if (!lazy.FormAutofillUtils.isCreditCardOrAddressFieldType(element)) {
+ return;
+ }
+ this._nextHandleElement = element;
+
+ if (!this._alreadyDOMContentLoaded) {
+ let doc = element.ownerDocument;
+ if (doc.readyState === "loading") {
+ if (!this._hasDOMContentLoadedHandler) {
+ this._hasDOMContentLoadedHandler = true;
+ doc.addEventListener(
+ "DOMContentLoaded",
+ () => this._doIdentifyAutofillFields(),
+ { once: true }
+ );
+ }
+ return;
+ }
+ this._alreadyDOMContentLoaded = true;
+ }
+
+ this._doIdentifyAutofillFields();
+ }
+
+ /**
+ * Handle the DOMFormBeforeSubmit event.
+ *
+ * @param {Event} evt
+ */
+ onDOMFormBeforeSubmit(evt) {
+ const formElement = evt.target;
+
+ const formSubmissionReason =
+ lazy.FormAutofillUtils.FORM_SUBMISSION_REASON.FORM_SUBMIT_EVENT;
+
+ lazy.FormAutofillContent.formSubmitted(formElement, formSubmissionReason);
+ }
+
+ /**
+ * Handle the DOMFormRemoved event.
+ *
+ * Infers a form submission when the form is removed
+ * after a successful fetch or XHR request.
+ *
+ * @param {Event} evt DOMFormRemoved
+ */
+ onDOMFormRemoved(evt) {
+ const document = evt.composedTarget.ownerDocument;
+
+ const formSubmissionReason =
+ lazy.FormAutofillUtils.FORM_SUBMISSION_REASON.FORM_REMOVAL_AFTER_FETCH;
+
+ lazy.FormAutofillContent.formSubmitted(evt.target, formSubmissionReason);
+
+ this.unregisterDOMFormRemovedEventListener(document);
+ }
+
+ /**
+ * Handle the DOMDocFetchSuccess event.
+ *
+ * Sets up an event listener for the DOMFormRemoved event
+ * and unregisters the event listener for DOMDocFetchSuccess event.
+ *
+ * @param {Event} evt DOMDocFetchSuccess
+ */
+ onDOMDocFetchSuccess(evt) {
+ const document = evt.target;
+
+ this.registerDOMFormRemovedEventListener(document);
+
+ this.unregisterDOMDocFetchSuccessEventListener(document);
+ }
+
+ receiveMessage(message) {
+ if (!lazy.FormAutofill.isAutofillEnabled) {
+ return;
+ }
+
+ const doc = this.document;
+
+ switch (message.name) {
+ case "FormAutofill:PreviewProfile": {
+ lazy.FormAutofillContent.previewProfile(doc);
+ break;
+ }
+ case "FormAutofill:ClearForm": {
+ lazy.FormAutofillContent.clearForm();
+ break;
+ }
+ case "FormAutofill:FillForm": {
+ lazy.FormAutofillContent.activeHandler.autofillFormFields(message.data);
+ break;
+ }
+ }
+ }
+}