/* 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, }; } }