summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/WebNavigationContent.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/WebNavigationContent.js')
-rw-r--r--toolkit/components/extensions/WebNavigationContent.js397
1 files changed, 397 insertions, 0 deletions
diff --git a/toolkit/components/extensions/WebNavigationContent.js b/toolkit/components/extensions/WebNavigationContent.js
new file mode 100644
index 0000000000..08ada223d6
--- /dev/null
+++ b/toolkit/components/extensions/WebNavigationContent.js
@@ -0,0 +1,397 @@
+/* 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/. */
+
+"use strict";
+
+/* eslint-env mozilla/frame-script */
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "WebNavigationFrames",
+ "resource://gre/modules/WebNavigationFrames.jsm"
+);
+
+function loadListener(event) {
+ let document = event.target;
+ let window = document.defaultView;
+ let url = document.documentURI;
+ let frameId = WebNavigationFrames.getFrameId(window);
+ let parentFrameId = WebNavigationFrames.getParentFrameId(window);
+ sendAsyncMessage("Extension:DOMContentLoaded", {
+ frameId,
+ parentFrameId,
+ url,
+ });
+}
+
+addEventListener("DOMContentLoaded", loadListener);
+addMessageListener("Extension:DisableWebNavigation", () => {
+ removeEventListener("DOMContentLoaded", loadListener);
+});
+
+var CreatedNavigationTargetListener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+
+ init() {
+ Services.obs.addObserver(
+ this,
+ "webNavigation-createdNavigationTarget-from-js"
+ );
+ },
+ uninit() {
+ Services.obs.removeObserver(
+ this,
+ "webNavigation-createdNavigationTarget-from-js"
+ );
+ },
+
+ observe(subject, topic, data) {
+ if (!(subject instanceof Ci.nsIPropertyBag2)) {
+ return;
+ }
+
+ let props = subject.QueryInterface(Ci.nsIPropertyBag2);
+
+ const createdDocShell = props.getPropertyAsInterface(
+ "createdTabDocShell",
+ Ci.nsIDocShell
+ );
+ const sourceDocShell = props.getPropertyAsInterface(
+ "sourceTabDocShell",
+ Ci.nsIDocShell
+ );
+
+ const isSourceTabDescendant =
+ sourceDocShell.sameTypeRootTreeItem === docShell;
+
+ if (
+ docShell !== createdDocShell &&
+ docShell !== sourceDocShell &&
+ !isSourceTabDescendant
+ ) {
+ // if the createdNavigationTarget is not related to this docShell
+ // (this docShell is not the newly created docShell, it is not the source docShell,
+ // and the source docShell is not a descendant of it)
+ // there is nothing to do here and return early.
+ return;
+ }
+
+ const isSourceTab = docShell === sourceDocShell || isSourceTabDescendant;
+
+ const sourceFrameId = WebNavigationFrames.getFrameId(
+ sourceDocShell.browsingContext
+ );
+ const createdOuterWindowId = sourceDocShell?.outerWindowID;
+
+ let url;
+ if (props.hasKey("url")) {
+ url = props.getPropertyAsACString("url");
+ }
+
+ sendAsyncMessage("Extension:CreatedNavigationTarget", {
+ url,
+ sourceFrameId,
+ createdOuterWindowId,
+ isSourceTab,
+ });
+ },
+};
+
+var FormSubmitListener = {
+ init() {
+ this.formSubmitWindows = new WeakSet();
+ addEventListener("DOMFormBeforeSubmit", this);
+ },
+
+ uninit() {
+ removeEventListener("DOMFormBeforeSubmit", this);
+ this.formSubmitWindows = new WeakSet();
+ },
+
+ handleEvent({ target: form }) {
+ this.formSubmitWindows.add(form.ownerGlobal);
+ },
+
+ hasAndForget: function(window) {
+ let has = this.formSubmitWindows.has(window);
+ this.formSubmitWindows.delete(window);
+ return has;
+ },
+};
+
+var WebProgressListener = {
+ init: function() {
+ // This WeakMap (DOMWindow -> nsIURI) keeps track of the pathname and hash
+ // of the previous location for all the existent docShells.
+ this.previousURIMap = new WeakMap();
+
+ // Populate the above previousURIMap by iterating over the docShells tree.
+ for (let currentDocShell of WebNavigationFrames.iterateDocShellTree(
+ docShell
+ )) {
+ let win = currentDocShell.domWindow;
+ let { currentURI } = currentDocShell.QueryInterface(Ci.nsIWebNavigation);
+
+ this.previousURIMap.set(win, currentURI);
+ }
+
+ // This WeakSet of DOMWindows keeps track of the attempted refresh.
+ this.refreshAttemptedDOMWindows = new WeakSet();
+
+ let webProgress = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ webProgress.addProgressListener(
+ this,
+ Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
+ Ci.nsIWebProgress.NOTIFY_REFRESH |
+ Ci.nsIWebProgress.NOTIFY_LOCATION
+ );
+ },
+
+ uninit() {
+ if (!docShell) {
+ return;
+ }
+ let webProgress = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ webProgress.removeProgressListener(this);
+ },
+
+ onRefreshAttempted: function onRefreshAttempted(
+ webProgress,
+ URI,
+ delay,
+ sameURI
+ ) {
+ this.refreshAttemptedDOMWindows.add(webProgress.DOMWindow);
+
+ // If this function doesn't return true, the attempted refresh will be blocked.
+ return true;
+ },
+
+ onStateChange: function onStateChange(
+ webProgress,
+ request,
+ stateFlags,
+ status
+ ) {
+ let { originalURI, URI: locationURI } = request.QueryInterface(
+ Ci.nsIChannel
+ );
+
+ // Prevents "about", "chrome", "resource" and "moz-extension" URI schemes to be
+ // reported with the resolved "file" or "jar" URIs. (see Bug 1246125 for rationale)
+ if (locationURI.schemeIs("file") || locationURI.schemeIs("jar")) {
+ let shouldUseOriginalURI =
+ originalURI.schemeIs("about") ||
+ originalURI.schemeIs("chrome") ||
+ originalURI.schemeIs("resource") ||
+ originalURI.schemeIs("moz-extension");
+
+ locationURI = shouldUseOriginalURI ? originalURI : locationURI;
+ }
+
+ this.sendStateChange({ webProgress, locationURI, stateFlags, status });
+
+ // Based on the docs of the webNavigation.onCommitted event, it should be raised when:
+ // "The document might still be downloading, but at least part of
+ // the document has been received"
+ // and for some reason we don't fire onLocationChange for the
+ // initial navigation of a sub-frame.
+ // For the above two reasons, when the navigation event is related to
+ // a sub-frame we process the document change here and
+ // then send an "Extension:DocumentChange" message to the main process,
+ // where it will be turned into a webNavigation.onCommitted event.
+ // (see Bug 1264936 and Bug 125662 for rationale)
+ if (
+ webProgress.DOMWindow.top != webProgress.DOMWindow &&
+ stateFlags & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT
+ ) {
+ this.sendDocumentChange({ webProgress, locationURI, request });
+ }
+ },
+
+ onLocationChange: function onLocationChange(
+ webProgress,
+ request,
+ locationURI,
+ flags
+ ) {
+ let { DOMWindow } = webProgress;
+
+ // Get the previous URI loaded in the DOMWindow.
+ let previousURI = this.previousURIMap.get(DOMWindow);
+
+ // Update the URI in the map with the new locationURI.
+ this.previousURIMap.set(DOMWindow, locationURI);
+
+ let isSameDocument =
+ flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT;
+
+ // When a frame navigation doesn't change the current loaded document
+ // (which can be due to history.pushState/replaceState or to a changed hash in the url),
+ // it is reported only to the onLocationChange, for this reason
+ // we process the history change here and then we are going to send
+ // an "Extension:HistoryChange" to the main process, where it will be turned
+ // into a webNavigation.onHistoryStateUpdated/onReferenceFragmentUpdated event.
+ if (isSameDocument) {
+ this.sendHistoryChange({
+ webProgress,
+ previousURI,
+ locationURI,
+ request,
+ });
+ } else if (webProgress.DOMWindow.top == webProgress.DOMWindow) {
+ // We have to catch the document changes from top level frames here,
+ // where we can detect the "server redirect" transition.
+ // (see Bug 1264936 and Bug 125662 for rationale)
+ this.sendDocumentChange({ webProgress, locationURI, request });
+ }
+ },
+
+ sendStateChange({ webProgress, locationURI, stateFlags, status }) {
+ let data = {
+ requestURL: locationURI.spec,
+ frameId: WebNavigationFrames.getFrameId(webProgress.DOMWindow),
+ parentFrameId: WebNavigationFrames.getParentFrameId(
+ webProgress.DOMWindow
+ ),
+ status,
+ stateFlags,
+ };
+
+ sendAsyncMessage("Extension:StateChange", data);
+ },
+
+ sendDocumentChange({ webProgress, locationURI, request }) {
+ let { loadType, DOMWindow } = webProgress;
+ let frameTransitionData = this.getFrameTransitionData({
+ loadType,
+ request,
+ DOMWindow,
+ });
+
+ let data = {
+ frameTransitionData,
+ location: locationURI ? locationURI.spec : "",
+ frameId: WebNavigationFrames.getFrameId(webProgress.DOMWindow),
+ parentFrameId: WebNavigationFrames.getParentFrameId(
+ webProgress.DOMWindow
+ ),
+ };
+
+ sendAsyncMessage("Extension:DocumentChange", data);
+ },
+
+ sendHistoryChange({ webProgress, previousURI, locationURI, request }) {
+ let { loadType, DOMWindow } = webProgress;
+
+ let isHistoryStateUpdated = false;
+ let isReferenceFragmentUpdated = false;
+
+ let pathChanged = !(
+ previousURI && locationURI.equalsExceptRef(previousURI)
+ );
+ let hashChanged = !(previousURI && previousURI.ref == locationURI.ref);
+
+ // When the location changes but the document is the same:
+ // - path not changed and hash changed -> |onReferenceFragmentUpdated|
+ // (even if it changed using |history.pushState|)
+ // - path not changed and hash not changed -> |onHistoryStateUpdated|
+ // (only if it changes using |history.pushState|)
+ // - path changed -> |onHistoryStateUpdated|
+
+ if (!pathChanged && hashChanged) {
+ isReferenceFragmentUpdated = true;
+ } else if (loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE) {
+ isHistoryStateUpdated = true;
+ } else if (loadType & Ci.nsIDocShell.LOAD_CMD_HISTORY) {
+ isHistoryStateUpdated = true;
+ }
+
+ if (isHistoryStateUpdated || isReferenceFragmentUpdated) {
+ let frameTransitionData = this.getFrameTransitionData({
+ loadType,
+ request,
+ DOMWindow,
+ });
+
+ let data = {
+ frameTransitionData,
+ isHistoryStateUpdated,
+ isReferenceFragmentUpdated,
+ location: locationURI ? locationURI.spec : "",
+ frameId: WebNavigationFrames.getFrameId(webProgress.DOMWindow),
+ parentFrameId: WebNavigationFrames.getParentFrameId(
+ webProgress.DOMWindow
+ ),
+ };
+
+ sendAsyncMessage("Extension:HistoryChange", data);
+ }
+ },
+
+ getFrameTransitionData({ loadType, request, DOMWindow }) {
+ let frameTransitionData = {};
+
+ if (loadType & Ci.nsIDocShell.LOAD_CMD_HISTORY) {
+ frameTransitionData.forward_back = true;
+ }
+
+ if (loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD) {
+ frameTransitionData.reload = true;
+ }
+
+ if (request instanceof Ci.nsIChannel) {
+ if (request.loadInfo.redirectChain.length) {
+ frameTransitionData.server_redirect = true;
+ }
+ }
+
+ if (FormSubmitListener.hasAndForget(DOMWindow)) {
+ frameTransitionData.form_submit = true;
+ }
+
+ if (this.refreshAttemptedDOMWindows.has(DOMWindow)) {
+ this.refreshAttemptedDOMWindows.delete(DOMWindow);
+ frameTransitionData.client_redirect = true;
+ }
+
+ return frameTransitionData;
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsIWebProgressListener2",
+ "nsISupportsWeakReference",
+ ]),
+};
+
+var disabled = false;
+WebProgressListener.init();
+FormSubmitListener.init();
+CreatedNavigationTargetListener.init();
+addEventListener("unload", () => {
+ if (!disabled) {
+ disabled = true;
+ WebProgressListener.uninit();
+ FormSubmitListener.uninit();
+ CreatedNavigationTargetListener.uninit();
+ }
+});
+addMessageListener("Extension:DisableWebNavigation", () => {
+ if (!disabled) {
+ disabled = true;
+ WebProgressListener.uninit();
+ FormSubmitListener.uninit();
+ CreatedNavigationTargetListener.uninit();
+ }
+});