summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/WebNavigation.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/WebNavigation.sys.mjs')
-rw-r--r--toolkit/components/extensions/WebNavigation.sys.mjs404
1 files changed, 404 insertions, 0 deletions
diff --git a/toolkit/components/extensions/WebNavigation.sys.mjs b/toolkit/components/extensions/WebNavigation.sys.mjs
new file mode 100644
index 0000000000..b040a6cbaa
--- /dev/null
+++ b/toolkit/components/extensions/WebNavigation.sys.mjs
@@ -0,0 +1,404 @@
+/* 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/. */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "BrowserWindowTracker",
+ "resource:///modules/BrowserWindowTracker.jsm"
+);
+ChromeUtils.defineESModuleGetters(lazy, {
+ ClickHandlerParent: "resource:///actors/ClickHandlerParent.sys.mjs",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
+ WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.sys.mjs",
+});
+
+// Maximum amount of time that can be passed and still consider
+// the data recent (similar to how is done in nsNavHistory,
+// e.g. nsNavHistory::CheckIsRecentEvent, but with a lower threshold value).
+const RECENT_DATA_THRESHOLD = 5 * 1000000;
+
+function getBrowser(bc) {
+ return bc.top.embedderElement;
+}
+
+export var WebNavigationManager = {
+ // Map[string -> Map[listener -> URLFilter]]
+ listeners: new Map(),
+
+ init() {
+ // Collect recent tab transition data in a WeakMap:
+ // browser -> tabTransitionData
+ this.recentTabTransitionData = new WeakMap();
+
+ Services.obs.addObserver(this, "urlbar-user-start-navigation", true);
+
+ Services.obs.addObserver(this, "webNavigation-createdNavigationTarget");
+
+ if (AppConstants.MOZ_BUILD_APP == "browser") {
+ lazy.ClickHandlerParent.addContentClickListener(this);
+ }
+ },
+
+ uninit() {
+ // Stop collecting recent tab transition data and reset the WeakMap.
+ Services.obs.removeObserver(this, "urlbar-user-start-navigation");
+ Services.obs.removeObserver(this, "webNavigation-createdNavigationTarget");
+
+ if (AppConstants.MOZ_BUILD_APP == "browser") {
+ lazy.ClickHandlerParent.removeContentClickListener(this);
+ }
+
+ this.recentTabTransitionData = new WeakMap();
+ },
+
+ addListener(type, listener) {
+ if (this.listeners.size == 0) {
+ this.init();
+ }
+
+ if (!this.listeners.has(type)) {
+ this.listeners.set(type, new Set());
+ }
+ let listeners = this.listeners.get(type);
+ listeners.add(listener);
+ },
+
+ removeListener(type, listener) {
+ let listeners = this.listeners.get(type);
+ if (!listeners) {
+ return;
+ }
+ listeners.delete(listener);
+ if (listeners.size == 0) {
+ this.listeners.delete(type);
+ }
+
+ if (this.listeners.size == 0) {
+ this.uninit();
+ }
+ },
+
+ /**
+ * Support nsIObserver interface to observe the urlbar autocomplete events used
+ * to keep track of the urlbar user interaction.
+ */
+ QueryInterface: ChromeUtils.generateQI([
+ "extIWebNavigation",
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+
+ /**
+ * Observe webNavigation-createdNavigationTarget (to fire the onCreatedNavigationTarget
+ * related to windows or tabs opened from the main process) topics.
+ *
+ * @param {nsIAutoCompleteInput | object} subject
+ * @param {string} topic
+ * @param {string | undefined} data
+ */
+ observe: function (subject, topic, data) {
+ if (topic == "urlbar-user-start-navigation") {
+ this.onURLBarUserStartNavigation(subject.wrappedJSObject);
+ } else if (topic == "webNavigation-createdNavigationTarget") {
+ // The observed notification is coming from privileged JavaScript components running
+ // in the main process (e.g. when a new tab or window is opened using the context menu
+ // or Ctrl/Shift + click on a link).
+ const { createdTabBrowser, url, sourceFrameID, sourceTabBrowser } =
+ subject.wrappedJSObject;
+
+ this.fire("onCreatedNavigationTarget", createdTabBrowser, null, {
+ sourceTabBrowser,
+ sourceFrameId: sourceFrameID,
+ url,
+ });
+ }
+ },
+
+ /**
+ * Recognize the type of urlbar user interaction (e.g. typing a new url,
+ * clicking on an url generated from a searchengine or a keyword, or a
+ * bookmark found by the urlbar autocompletion).
+ *
+ * @param {object} acData
+ * The data for the autocompleted item.
+ * @param {object} [acData.result]
+ * The result information associated with the navigation action.
+ * @param {UrlbarUtils.RESULT_TYPE} [acData.result.type]
+ * The result type associated with the navigation action.
+ * @param {UrlbarUtils.RESULT_SOURCE} [acData.result.source]
+ * The result source associated with the navigation action.
+ */
+ onURLBarUserStartNavigation(acData) {
+ let tabTransitionData = {
+ from_address_bar: true,
+ };
+
+ if (!acData.result) {
+ tabTransitionData.typed = true;
+ } else {
+ switch (acData.result.type) {
+ case lazy.UrlbarUtils.RESULT_TYPE.KEYWORD:
+ tabTransitionData.keyword = true;
+ break;
+ case lazy.UrlbarUtils.RESULT_TYPE.SEARCH:
+ tabTransitionData.generated = true;
+ break;
+ case lazy.UrlbarUtils.RESULT_TYPE.URL:
+ if (
+ acData.result.source == lazy.UrlbarUtils.RESULT_SOURCE.BOOKMARKS
+ ) {
+ tabTransitionData.auto_bookmark = true;
+ } else {
+ tabTransitionData.typed = true;
+ }
+ break;
+ case lazy.UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
+ // Remote tab are autocomplete results related to
+ // tab urls from a remote synchronized Firefox.
+ tabTransitionData.typed = true;
+ break;
+ case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
+ // This "switchtab" autocompletion should be ignored, because
+ // it is not related to a navigation.
+ // Fall through.
+ case lazy.UrlbarUtils.RESULT_TYPE.OMNIBOX:
+ // "Omnibox" should be ignored as the add-on may or may not initiate
+ // a navigation on the item being selected.
+ // Fall through.
+ case lazy.UrlbarUtils.RESULT_TYPE.TIP:
+ // "Tip" should be ignored since the tip will only initiate navigation
+ // if there is a valid buttonUrl property, which is optional.
+ throw new Error(
+ `Unexpectedly received notification for ${acData.result.type}`
+ );
+ default:
+ Cu.reportError(
+ `Received unexpected result type ${acData.result.type}, falling back to typed transition.`
+ );
+ // Fallback on "typed" if the type is unknown.
+ tabTransitionData.typed = true;
+ }
+ }
+
+ this.setRecentTabTransitionData(tabTransitionData);
+ },
+
+ /**
+ * Keep track of a recent user interaction and cache it in a
+ * map associated to the current selected tab.
+ *
+ * @param {object} tabTransitionData
+ * @param {boolean} [tabTransitionData.auto_bookmark]
+ * @param {boolean} [tabTransitionData.from_address_bar]
+ * @param {boolean} [tabTransitionData.generated]
+ * @param {boolean} [tabTransitionData.keyword]
+ * @param {boolean} [tabTransitionData.link]
+ * @param {boolean} [tabTransitionData.typed]
+ */
+ setRecentTabTransitionData(tabTransitionData) {
+ let window = lazy.BrowserWindowTracker.getTopWindow();
+ if (
+ window &&
+ window.gBrowser &&
+ window.gBrowser.selectedTab &&
+ window.gBrowser.selectedTab.linkedBrowser
+ ) {
+ let browser = window.gBrowser.selectedTab.linkedBrowser;
+
+ // Get recent tab transition data to update if any.
+ let prevData = this.getAndForgetRecentTabTransitionData(browser);
+
+ let newData = Object.assign(
+ { time: Date.now() },
+ prevData,
+ tabTransitionData
+ );
+ this.recentTabTransitionData.set(browser, newData);
+ }
+ },
+
+ /**
+ * Retrieve recent data related to a recent user interaction give a
+ * given tab's linkedBrowser (only if is is more recent than the
+ * `RECENT_DATA_THRESHOLD`).
+ *
+ * NOTE: this method is used to retrieve the tab transition data
+ * collected when one of the `onCommitted`, `onHistoryStateUpdated`
+ * or `onReferenceFragmentUpdated` events has been received.
+ *
+ * @param {XULBrowserElement} browser
+ * @returns {object}
+ */
+ getAndForgetRecentTabTransitionData(browser) {
+ let data = this.recentTabTransitionData.get(browser);
+ this.recentTabTransitionData.delete(browser);
+
+ // Return an empty object if there isn't any tab transition data
+ // or if it's less recent than RECENT_DATA_THRESHOLD.
+ if (!data || data.time - Date.now() > RECENT_DATA_THRESHOLD) {
+ return {};
+ }
+
+ return data;
+ },
+
+ onContentClick(target, data) {
+ // We are interested only on clicks to links which are not "add to bookmark" commands
+ if (data.href && !data.bookmark) {
+ let ownerWin = target.ownerGlobal;
+ let where = ownerWin.whereToOpenLink(data);
+ if (where == "current") {
+ this.setRecentTabTransitionData({ link: true });
+ }
+ }
+ },
+
+ onCreatedNavigationTarget(bc, sourceBC, url) {
+ if (!this.listeners.size) {
+ return;
+ }
+
+ let browser = getBrowser(bc);
+
+ this.fire("onCreatedNavigationTarget", browser, null, {
+ sourceTabBrowser: getBrowser(sourceBC),
+ sourceFrameId: lazy.WebNavigationFrames.getFrameId(sourceBC),
+ url,
+ });
+ },
+
+ onStateChange(bc, requestURI, status, stateFlags) {
+ if (!this.listeners.size) {
+ return;
+ }
+
+ let browser = getBrowser(bc);
+
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
+ let url = requestURI.spec;
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ this.fire("onBeforeNavigate", browser, bc, { url });
+ } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ if (Components.isSuccessCode(status)) {
+ this.fire("onCompleted", browser, bc, { url });
+ } else {
+ let error = `Error code ${status}`;
+ this.fire("onErrorOccurred", browser, bc, { error, url });
+ }
+ }
+ }
+ },
+
+ onDocumentChange(bc, frameTransitionData, location) {
+ if (!this.listeners.size) {
+ return;
+ }
+
+ let browser = getBrowser(bc);
+
+ let extra = {
+ url: location ? location.spec : "",
+ // Transition data which is coming from the content process.
+ frameTransitionData,
+ tabTransitionData: this.getAndForgetRecentTabTransitionData(browser),
+ };
+
+ this.fire("onCommitted", browser, bc, extra);
+ },
+
+ onHistoryChange(
+ bc,
+ frameTransitionData,
+ location,
+ isHistoryStateUpdated,
+ isReferenceFragmentUpdated
+ ) {
+ if (!this.listeners.size) {
+ return;
+ }
+
+ let browser = getBrowser(bc);
+
+ let extra = {
+ url: location ? location.spec : "",
+ // Transition data which is coming from the content process.
+ frameTransitionData,
+ tabTransitionData: this.getAndForgetRecentTabTransitionData(browser),
+ };
+
+ if (isReferenceFragmentUpdated) {
+ this.fire("onReferenceFragmentUpdated", browser, bc, extra);
+ } else if (isHistoryStateUpdated) {
+ this.fire("onHistoryStateUpdated", browser, bc, extra);
+ }
+ },
+
+ onDOMContentLoaded(bc, documentURI) {
+ if (!this.listeners.size) {
+ return;
+ }
+
+ let browser = getBrowser(bc);
+
+ this.fire("onDOMContentLoaded", browser, bc, { url: documentURI.spec });
+ },
+
+ fire(type, browser, bc, extra) {
+ if (!browser) {
+ return;
+ }
+
+ let listeners = this.listeners.get(type);
+ if (!listeners) {
+ return;
+ }
+
+ let details = {
+ browser,
+ };
+
+ if (bc) {
+ details.frameId = lazy.WebNavigationFrames.getFrameId(bc);
+ details.parentFrameId = lazy.WebNavigationFrames.getParentFrameId(bc);
+ }
+
+ for (let prop in extra) {
+ details[prop] = extra[prop];
+ }
+
+ for (let listener of listeners) {
+ listener(details);
+ }
+ },
+};
+
+const EVENTS = [
+ "onBeforeNavigate",
+ "onCommitted",
+ "onDOMContentLoaded",
+ "onCompleted",
+ "onErrorOccurred",
+ "onReferenceFragmentUpdated",
+ "onHistoryStateUpdated",
+ "onCreatedNavigationTarget",
+];
+
+export var WebNavigation = {};
+
+for (let event of EVENTS) {
+ WebNavigation[event] = {
+ addListener: WebNavigationManager.addListener.bind(
+ WebNavigationManager,
+ event
+ ),
+ removeListener: WebNavigationManager.removeListener.bind(
+ WebNavigationManager,
+ event
+ ),
+ };
+}