summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/addons/WebNavigation.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/addons/WebNavigation.jsm')
-rw-r--r--toolkit/modules/addons/WebNavigation.jsm370
1 files changed, 370 insertions, 0 deletions
diff --git a/toolkit/modules/addons/WebNavigation.jsm b/toolkit/modules/addons/WebNavigation.jsm
new file mode 100644
index 000000000..6302a9d79
--- /dev/null
+++ b/toolkit/modules/addons/WebNavigation.jsm
@@ -0,0 +1,370 @@
+/* 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";
+
+const EXPORTED_SYMBOLS = ["WebNavigation"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
+ "resource:///modules/RecentWindow.jsm");
+
+// 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;
+
+// TODO:
+// onCreatedNavigationTarget
+
+var Manager = {
+ // 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, "autocomplete-did-enter-text", true);
+
+ Services.mm.addMessageListener("Content:Click", this);
+ Services.mm.addMessageListener("Extension:DOMContentLoaded", this);
+ Services.mm.addMessageListener("Extension:StateChange", this);
+ Services.mm.addMessageListener("Extension:DocumentChange", this);
+ Services.mm.addMessageListener("Extension:HistoryChange", this);
+
+ Services.mm.loadFrameScript("resource://gre/modules/WebNavigationContent.js", true);
+ },
+
+ uninit() {
+ // Stop collecting recent tab transition data and reset the WeakMap.
+ Services.obs.removeObserver(this, "autocomplete-did-enter-text", true);
+ this.recentTabTransitionData = new WeakMap();
+
+ Services.mm.removeMessageListener("Content:Click", this);
+ Services.mm.removeMessageListener("Extension:StateChange", this);
+ Services.mm.removeMessageListener("Extension:DocumentChange", this);
+ Services.mm.removeMessageListener("Extension:HistoryChange", this);
+ Services.mm.removeMessageListener("Extension:DOMContentLoaded", this);
+
+ Services.mm.removeDelayedFrameScript("resource://gre/modules/WebNavigationContent.js");
+ Services.mm.broadcastAsyncMessage("Extension:DisableWebNavigation");
+ },
+
+ addListener(type, listener, filters) {
+ if (this.listeners.size == 0) {
+ this.init();
+ }
+
+ if (!this.listeners.has(type)) {
+ this.listeners.set(type, new Map());
+ }
+ let listeners = this.listeners.get(type);
+ listeners.set(listener, filters);
+ },
+
+ 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: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+ /**
+ * Observe autocomplete-did-enter-text topic to track the user interaction with
+ * the awesome bar.
+ *
+ * @param {nsIAutoCompleteInput} subject
+ * @param {string} topic
+ * @param {string} data
+ */
+ observe: function(subject, topic, data) {
+ if (topic == "autocomplete-did-enter-text") {
+ this.onURLBarAutoCompletion(subject);
+ }
+ },
+
+ /**
+ * 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 {nsIAutoCompleteInput} input
+ */
+ onURLBarAutoCompletion(input) {
+ if (input && input instanceof Ci.nsIAutoCompleteInput) {
+ // We are only interested in urlbar autocompletion events
+ if (input.id !== "urlbar") {
+ return;
+ }
+
+ let controller = input.popup.view.QueryInterface(Ci.nsIAutoCompleteController);
+ let idx = input.popup.selectedIndex;
+
+ let tabTransistionData = {
+ from_address_bar: true,
+ };
+
+ if (idx < 0 || idx >= controller.matchCount) {
+ // Recognize when no valid autocomplete results has been selected.
+ tabTransistionData.typed = true;
+ } else {
+ let value = controller.getValueAt(idx);
+ let action = input._parseActionUrl(value);
+
+ if (action) {
+ // Detect keywork and generated and more typed scenarios.
+ switch (action.type) {
+ case "keyword":
+ tabTransistionData.keyword = true;
+ break;
+ case "searchengine":
+ case "searchsuggestion":
+ tabTransistionData.generated = true;
+ break;
+ case "visiturl":
+ // Visiturl are autocompletion results related to
+ // history suggestions.
+ tabTransistionData.typed = true;
+ break;
+ case "remotetab":
+ // Remote tab are autocomplete results related to
+ // tab urls from a remote synchronized Firefox.
+ tabTransistionData.typed = true;
+ break;
+ case "switchtab":
+ // This "switchtab" autocompletion should be ignored, because
+ // it is not related to a navigation.
+ return;
+ default:
+ // Fallback on "typed" if unable to detect a known moz-action type.
+ tabTransistionData.typed = true;
+ }
+ } else {
+ // Special handling for bookmark urlbar autocompletion
+ // (which happens when we got a null action and a valid selectedIndex)
+ let styles = new Set(controller.getStyleAt(idx).split(/\s+/));
+
+ if (styles.has("bookmark")) {
+ tabTransistionData.auto_bookmark = true;
+ } else {
+ // Fallback on "typed" if unable to detect a specific actionType
+ // (and when in the styles there are "autofill" or "history").
+ tabTransistionData.typed = true;
+ }
+ }
+ }
+
+ this.setRecentTabTransitionData(tabTransistionData);
+ }
+ },
+
+ /**
+ * 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 = RecentWindow.getMostRecentBrowserWindow();
+ 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;
+ },
+
+ /**
+ * Receive messages from the WebNavigationContent.js framescript
+ * over message manager events.
+ */
+ receiveMessage({name, data, target}) {
+ switch (name) {
+ case "Extension:StateChange":
+ this.onStateChange(target, data);
+ break;
+
+ case "Extension:DocumentChange":
+ this.onDocumentChange(target, data);
+ break;
+
+ case "Extension:HistoryChange":
+ this.onHistoryChange(target, data);
+ break;
+
+ case "Extension:DOMContentLoaded":
+ this.onLoad(target, data);
+ break;
+
+ case "Content:Click":
+ this.onContentClick(target, data);
+ break;
+ }
+ },
+
+ 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.ownerDocument.defaultView;
+ let where = ownerWin.whereToOpenLink(data);
+ if (where == "current") {
+ this.setRecentTabTransitionData({link: true});
+ }
+ }
+ },
+
+ onStateChange(browser, data) {
+ let stateFlags = data.stateFlags;
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
+ let url = data.requestURL;
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ this.fire("onBeforeNavigate", browser, data, {url});
+ } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ if (Components.isSuccessCode(data.status)) {
+ this.fire("onCompleted", browser, data, {url});
+ } else {
+ let error = `Error code ${data.status}`;
+ this.fire("onErrorOccurred", browser, data, {error, url});
+ }
+ }
+ }
+ },
+
+ onDocumentChange(browser, data) {
+ let extra = {
+ url: data.location,
+ // Transition data which is coming from the content process.
+ frameTransitionData: data.frameTransitionData,
+ tabTransitionData: this.getAndForgetRecentTabTransitionData(browser),
+ };
+
+ this.fire("onCommitted", browser, data, extra);
+ },
+
+ onHistoryChange(browser, data) {
+ let extra = {
+ url: data.location,
+ // Transition data which is coming from the content process.
+ frameTransitionData: data.frameTransitionData,
+ tabTransitionData: this.getAndForgetRecentTabTransitionData(browser),
+ };
+
+ if (data.isReferenceFragmentUpdated) {
+ this.fire("onReferenceFragmentUpdated", browser, data, extra);
+ } else if (data.isHistoryStateUpdated) {
+ this.fire("onHistoryStateUpdated", browser, data, extra);
+ }
+ },
+
+ onLoad(browser, data) {
+ this.fire("onDOMContentLoaded", browser, data, {url: data.url});
+ },
+
+ fire(type, browser, data, extra) {
+ let listeners = this.listeners.get(type);
+ if (!listeners) {
+ return;
+ }
+
+ let details = {
+ browser,
+ windowId: data.windowId,
+ };
+
+ if (data.parentWindowId) {
+ details.parentWindowId = data.parentWindowId;
+ }
+
+ for (let prop in extra) {
+ details[prop] = extra[prop];
+ }
+
+ for (let [listener, filters] of listeners) {
+ // Call the listener if the listener has no filter or if its filter matches.
+ if (!filters || filters.matches(extra.url)) {
+ listener(details);
+ }
+ }
+ },
+};
+
+const EVENTS = [
+ "onBeforeNavigate",
+ "onCommitted",
+ "onDOMContentLoaded",
+ "onCompleted",
+ "onErrorOccurred",
+ "onReferenceFragmentUpdated",
+ "onHistoryStateUpdated",
+ // "onCreatedNavigationTarget",
+];
+
+var WebNavigation = {};
+
+for (let event of EVENTS) {
+ WebNavigation[event] = {
+ addListener: Manager.addListener.bind(Manager, event),
+ removeListener: Manager.removeListener.bind(Manager, event),
+ };
+}