forked from mirrors/gecko-dev
FormHandler is a central place for heuristics that other components like the LoginManager and FormAutofill rely on. This patch moves the heuristics that detect page navigations to the FormHandler. It also applies two changes: - Heuristic to capture on page navigation no longer relies on the process' active element in FormAutofill - Capturing in cross-origin frames is introduced Introduced page navigation heuristic: When LoginManager/FormAutofill detect a form that they expect a submission for, a FormHandler actor pair is created in the current window context, which registers the web progress listener that listens for "page navigations", e.g. location changes of the observed window/document or history session changes. - If the form is in a same-orign frame, we register the listener only at the top level. - If the form is in a cross-origin frame, we additionally set up a listener with the root of the cross-origin process, so that we are aware of page navigations in both processes. When a page navigation is observed, all existing (same-origin and cross-origin) FormHandler parents in the browsing context subtree notify their children. (Note: We don't create any new actors in this step, because they won't have any form to submit anyway). When the corresponding FormHandlerChild(ren) are notified of the page navigation, they fire the "form-submission-detected" event. On "form-submission-detected" event: - The LoginManagerChild instance(s) kept track of all modified login forms and infers capturing them. - The FormAutofillChild instance(s) kept track of all identified formautofill forms and infers capturing them. Differential Revision: https://phabricator.services.mozilla.com/D204927
280 lines
9.7 KiB
JavaScript
280 lines
9.7 KiB
JavaScript
/* 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/. */
|
|
|
|
/**
|
|
* The FormHandler actor pair implements the logic for detecting and
|
|
* notifying of form submissions. Currently the components FormAutofill
|
|
* and LoginManager are listening for the "form-submission-detected"
|
|
* event that is dispatched by the FormHandlerChild.
|
|
*/
|
|
|
|
export const FORM_SUBMISSION_REASON = {
|
|
FORM_SUBMIT_EVENT: "form-submit-event",
|
|
FORM_REMOVAL_AFTER_FETCH: "form-removal-after-fetch",
|
|
IFRAME_PAGEHIDE: "iframe-pagehide",
|
|
PAGE_NAVIGATION: "page-navigation",
|
|
};
|
|
|
|
export class FormHandlerChild extends JSWindowActorChild {
|
|
actorCreated() {
|
|
// Whenever a FormHandlerChild is created it's because somebody has registered
|
|
// their interest in form submissions. This step might create FormHandler actors
|
|
// across multiple window contexts. Whenever a FormHandlerChild is created in a
|
|
// process root, we want to make sure that it registers the progress listener
|
|
// in order to listen for form submissions in that process.
|
|
if (this.manager.isProcessRoot) {
|
|
this.registerProgressListener();
|
|
}
|
|
}
|
|
/**
|
|
* Tracks whether an interest in form submissions was registered in this window
|
|
*/
|
|
#hasRegisteredFormSubmissionInterest = false;
|
|
|
|
handleEvent(event) {
|
|
if (!event.isTrusted) {
|
|
return;
|
|
}
|
|
|
|
if (!this.#hasRegisteredFormSubmissionInterest) {
|
|
return;
|
|
}
|
|
|
|
switch (event.type) {
|
|
case "DOMFormBeforeSubmit":
|
|
this.processDOMFormBeforeSubmitEvent(event);
|
|
break;
|
|
default:
|
|
throw new Error("Unexpected event type");
|
|
}
|
|
}
|
|
|
|
receiveMessage(message) {
|
|
switch (message.name) {
|
|
case "FormHandler:FormSubmissionByNavigation": {
|
|
this.processPageNavigation();
|
|
break;
|
|
}
|
|
case "FormHandler:EnsureChildExists": {
|
|
// This is just a dummy message to make sure that the
|
|
// FormHandlerChild is created because then the actor
|
|
// starts listening to page navigations
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process the DOMFormBeforeSubmit event that is dispatched
|
|
* after a form submit event.
|
|
*
|
|
* @param {Event} event DOMFormBeforeSubmit
|
|
*/
|
|
processDOMFormBeforeSubmitEvent(event) {
|
|
const form = event.target;
|
|
const formSubmissionReason = FORM_SUBMISSION_REASON.FORM_SUBMIT_EVENT;
|
|
|
|
this.#dispatchFormSubmissionEvent(form, formSubmissionReason);
|
|
}
|
|
|
|
// handle form-removal-after-fetch
|
|
processFormRemovalAfterFetch(_params) {}
|
|
|
|
/**
|
|
* This or the page of a parent browsing context was navigated,
|
|
* so process the page navigation, only when somebody in the current has
|
|
* registered interest for it
|
|
*/
|
|
processPageNavigation() {
|
|
if (!this.#hasRegisteredFormSubmissionInterest) {
|
|
// Nobody is interested in the current window
|
|
// so don't bother notifying anyone
|
|
return;
|
|
}
|
|
const formSubmissionReason = FORM_SUBMISSION_REASON.PAGE_NAVIGATION;
|
|
this.#dispatchFormSubmissionEvent(null, formSubmissionReason);
|
|
}
|
|
|
|
/**
|
|
* Dispatch the CustomEvent form-submission-detected and transfer
|
|
* the following information:
|
|
* detail.form - the form that is being submitted
|
|
* detail.reason - the heuristic that detected the form submission
|
|
* (see FORM_SUBMISSION_REASON)
|
|
*
|
|
* @param {HTMLFormElement} form
|
|
* @param {string} reason
|
|
*/
|
|
#dispatchFormSubmissionEvent(form, reason) {
|
|
const formSubmissionEvent = new CustomEvent("form-submission-detected", {
|
|
detail: { form, reason },
|
|
bubbles: true,
|
|
});
|
|
this.document.dispatchEvent(formSubmissionEvent);
|
|
}
|
|
|
|
/**
|
|
* A page navigation was observed in this window or in the subtree.
|
|
* If somebody in this window is interested in form submissions, process it here.
|
|
* Additionally, inform the parent of the navigation so that all FormHandler
|
|
* children in the subtree of the navigated browsing context are notified as well.
|
|
*
|
|
* @param {BrowsingContext} navigatedBrowingContext
|
|
*/
|
|
onNavigationObserved(navigatedBrowingContext) {
|
|
if (
|
|
this.#hasRegisteredFormSubmissionInterest &&
|
|
this.browsingContext == navigatedBrowingContext
|
|
) {
|
|
// This is the most probable case, that an interest in form submissions was registered
|
|
// in the navigated browing context, so we call processPageNavigation directly
|
|
// instead of letting the parent notify this actor again to process it.
|
|
this.processPageNavigation();
|
|
}
|
|
this.sendAsyncMessage(
|
|
"FormHandler:NotifyNavigatedSubtree",
|
|
navigatedBrowingContext
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create the corresponding parent of the current child, because the existence
|
|
* of the FormHandlerParent is the condition for being notified of a page navigation.
|
|
* If the current process is not the process root, we create the FormHandlerChild in
|
|
* the process root. The progress listener is registered after creating the child.
|
|
* If the current process is in a cross-origin frame, we notify the parent
|
|
* to register the progress listener also with the top level's process root.
|
|
*/
|
|
registerFormSubmissionInterest() {
|
|
if (this.#hasRegisteredFormSubmissionInterest) {
|
|
return;
|
|
}
|
|
|
|
// We use the existence of the FormHandlerParent on the parent side
|
|
// to determine whether to notify the corresponding FormHandleChild
|
|
// when a page is navigated. So we explicitly create the parent actor
|
|
// by sending a dummy message here
|
|
this.sendAsyncMessage("FormHandler:EnsureParentExists");
|
|
|
|
if (!this.manager.isProcessRoot) {
|
|
// The progress listener is registered after the
|
|
// FormHandlerChild is created in the process root
|
|
this.document.ownerGlobal.windowRoot.ownerGlobal.windowGlobalChild.getActor(
|
|
"FormHandler"
|
|
);
|
|
}
|
|
|
|
if (!this.manager.sameOriginWithTop) {
|
|
// If the top level is navigated, that also effects the current cross-origin frame.
|
|
// So we notify the parent to set up the progress listeners at the top as well.
|
|
this.sendAsyncMessage("FormHandler:RegisterProgressListenerAtTopLevel");
|
|
}
|
|
|
|
this.#hasRegisteredFormSubmissionInterest = true;
|
|
}
|
|
|
|
/**
|
|
* Set up a nsIWebProgressListener that notifies of certain request state
|
|
* changes such as changes of the location and the history stack for this docShell
|
|
* and for the children's same-orign docShells.
|
|
*
|
|
* Note: Registering the listener only in the process root (instead of for
|
|
* every window) is enough to receive notifications for the whole process,
|
|
* because the notifications bubble up
|
|
*/
|
|
registerProgressListener() {
|
|
const webProgress = this.docShell
|
|
.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIWebProgress);
|
|
|
|
const flags =
|
|
Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
|
|
Ci.nsIWebProgress.NOTIFY_LOCATION;
|
|
try {
|
|
webProgress.addProgressListener(observer, flags);
|
|
} catch (ex) {
|
|
// Ignore NS_ERROR_FAILURE if the progress listener was already added
|
|
}
|
|
}
|
|
}
|
|
|
|
const observer = {
|
|
QueryInterface: ChromeUtils.generateQI([
|
|
"nsIWebProgressListener",
|
|
"nsISupportsWeakReference",
|
|
]),
|
|
|
|
/**
|
|
* Handle history stack changes (history.replaceState(), history.pushState())
|
|
* on the same document as page navigation
|
|
*/
|
|
onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
|
|
if (
|
|
!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) ||
|
|
!(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE)
|
|
) {
|
|
return;
|
|
}
|
|
const navigatedWindow = aWebProgress.DOMWindow;
|
|
|
|
this.notifyProcessRootOfNavigation(navigatedWindow);
|
|
},
|
|
|
|
/*
|
|
* Handle certain state changes of requests as page navigation
|
|
* such as location changes (location.assign(), location.replace())
|
|
* See further comments for more details
|
|
*/
|
|
onStateChange(aWebProgress, aRequest, aStateFlags, _aStatus) {
|
|
if (
|
|
aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING &&
|
|
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP
|
|
) {
|
|
// a document is restored from bfcache
|
|
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 formautofill data.
|
|
let channel = aRequest.QueryInterface(Ci.nsIChannel);
|
|
let triggeringPrincipal = channel.loadInfo.triggeringPrincipal;
|
|
if (
|
|
triggeringPrincipal.isNullPrincipal ||
|
|
triggeringPrincipal.equals(
|
|
Services.scriptSecurityManager.getSystemPrincipal()
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// We don't handle history navigation, reloads (e.g. history.go(-1), history.back(), location.reload())
|
|
// Note: History state changes (e.g. history.replaceState(), history.pushState()) are handled in onLocationChange
|
|
if (!(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_NORMAL)) {
|
|
return;
|
|
}
|
|
|
|
const navigatedWindow = aWebProgress.DOMWindow;
|
|
this.notifyProcessRootOfNavigation(navigatedWindow);
|
|
},
|
|
|
|
/**
|
|
* Notify the current process root parent of the page navigation
|
|
* and pass on the navigated browsing context
|
|
*
|
|
* @param {Window} navigatedWindow
|
|
*/
|
|
notifyProcessRootOfNavigation(navigatedWindow) {
|
|
const processRootWindow = navigatedWindow.windowRoot.ownerGlobal;
|
|
const formHandlerChild =
|
|
processRootWindow.windowGlobalChild.getExistingActor("FormHandler");
|
|
const navigatedBrowsingContext = navigatedWindow.browsingContext;
|
|
|
|
formHandlerChild?.onNavigationObserved(navigatedBrowsingContext);
|
|
},
|
|
};
|