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