diff options
Diffstat (limited to 'toolkit/modules/addons/WebNavigationContent.js')
-rw-r--r-- | toolkit/modules/addons/WebNavigationContent.js | 272 |
1 files changed, 272 insertions, 0 deletions
diff --git a/toolkit/modules/addons/WebNavigationContent.js b/toolkit/modules/addons/WebNavigationContent.js new file mode 100644 index 000000000..cea4a97b3 --- /dev/null +++ b/toolkit/modules/addons/WebNavigationContent.js @@ -0,0 +1,272 @@ +"use strict"; + +/* globals docShell */ + +var Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames", + "resource://gre/modules/WebNavigationFrames.jsm"); + +function loadListener(event) { + let document = event.target; + let window = document.defaultView; + let url = document.documentURI; + let windowId = WebNavigationFrames.getWindowId(window); + let parentWindowId = WebNavigationFrames.getParentWindowId(window); + sendAsyncMessage("Extension:DOMContentLoaded", {windowId, parentWindowId, url}); +} + +addEventListener("DOMContentLoaded", loadListener); +addMessageListener("Extension:DisableWebNavigation", () => { + removeEventListener("DOMContentLoaded", loadListener); +}); + +var FormSubmitListener = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsIFormSubmitObserver, + Ci.nsISupportsWeakReference]), + init() { + this.formSubmitWindows = new WeakSet(); + Services.obs.addObserver(FormSubmitListener, "earlyformsubmit", false); + }, + + uninit() { + Services.obs.removeObserver(FormSubmitListener, "earlyformsubmit", false); + this.formSubmitWindows = new WeakSet(); + }, + + notify: function(form, window, actionURI) { + try { + this.formSubmitWindows.add(window); + } catch (e) { + Cu.reportError("Error in FormSubmitListener.notify"); + } + }, + + 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.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + 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, + windowId: webProgress.DOMWindowID, + parentWindowId: WebNavigationFrames.getParentWindowId(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 : "", + windowId: webProgress.DOMWindowID, + parentWindowId: WebNavigationFrames.getParentWindowId(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 : "", + windowId: webProgress.DOMWindowID, + parentWindowId: WebNavigationFrames.getParentWindowId(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: XPCOMUtils.generateQI([ + Ci.nsIWebProgressListener, + Ci.nsIWebProgressListener2, + Ci.nsISupportsWeakReference, + ]), +}; + +var disabled = false; +WebProgressListener.init(); +FormSubmitListener.init(); +addEventListener("unload", () => { + if (!disabled) { + disabled = true; + WebProgressListener.uninit(); + FormSubmitListener.uninit(); + } +}); +addMessageListener("Extension:DisableWebNavigation", () => { + if (!disabled) { + disabled = true; + WebProgressListener.uninit(); + FormSubmitListener.uninit(); + } +}); |