Source code

Revision control

Copy as Markdown

Other Tools

/* 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, {
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});
/**
* Listens for interactions in the child process and passes information to the
* parent.
*/
export class InteractionsChild extends JSWindowActorChild {
#progressListener;
#currentURL;
actorCreated() {
this.isContentWindowPrivate =
lazy.PrivateBrowsingUtils.isContentWindowPrivate(this.contentWindow);
if (this.isContentWindowPrivate) {
return;
}
this.#progressListener = {
onLocationChange: (webProgress, request, location, flags) => {
this.onLocationChange(webProgress, request, location, flags);
},
QueryInterface: ChromeUtils.generateQI([
"nsIWebProgressListener2",
"nsIWebProgressListener",
"nsISupportsWeakReference",
]),
};
let webProgress = this.docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(
this.#progressListener,
Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
Ci.nsIWebProgress.NOTIFY_LOCATION
);
}
didDestroy() {
// If the tab is closed then the docshell is no longer available.
if (!this.#progressListener || !this.docShell) {
return;
}
let webProgress = this.docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
webProgress.removeProgressListener(this.#progressListener);
}
onLocationChange(webProgress, request, location, flags) {
// We don't care about inner-frame navigations.
if (!webProgress.isTopLevel) {
return;
}
// If this is a new document then the DOMContentLoaded event will trigger
// the new interaction instead.
if (!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
return;
}
this.#recordNewPage();
}
#recordNewPage() {
if (!this.docShell.currentDocumentChannel) {
// If there is no document channel, then it is something we're not
// interested in, but we do need to know that the previous interaction
// has ended.
this.sendAsyncMessage("Interactions:PageHide");
return;
}
let docInfo = this.#getDocumentInfo();
// This may happen when the page calls replaceState or pushState with the
// same URL. We'll just consider this to not be a new page.
if (docInfo.url == this.#currentURL) {
return;
}
this.#currentURL = docInfo.url;
if (
this.docShell.currentDocumentChannel instanceof Ci.nsIHttpChannel &&
!this.docShell.currentDocumentChannel.requestSucceeded
) {
return;
}
this.sendAsyncMessage("Interactions:PageLoaded", docInfo);
}
async handleEvent(event) {
if (this.isContentWindowPrivate) {
// No recording in private browsing mode.
return;
}
switch (event.type) {
case "DOMContentLoaded": {
this.#recordNewPage();
break;
}
case "pagehide": {
if (!this.docShell.currentDocumentChannel) {
return;
}
if (!this.docShell.currentDocumentChannel.requestSucceeded) {
return;
}
this.sendAsyncMessage("Interactions:PageHide");
break;
}
}
}
/**
* Returns the current document information for sending to the parent process.
*
* @returns {{ isActive: boolean, url: string, referrer: * }?}
*/
#getDocumentInfo() {
let doc = this.document;
let referrer;
if (doc.referrer) {
referrer = Services.io.newURI(doc.referrer);
}
return {
isActive: this.manager.browsingContext.isActive,
url: doc.documentURIObject.specIgnoringRef,
referrer: referrer?.specIgnoringRef,
};
}
}