Mypal/devtools/server/actors/webbrowser.js

2537 lines
81 KiB
JavaScript

/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* 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";
/* global XPCNativeWrapper */
var { Ci, Cu, Cr } = require("chrome");
var Services = require("Services");
var { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
var promise = require("promise");
var {
ActorPool, createExtraActors, appendExtraActors, GeneratedLocation
} = require("devtools/server/actors/common");
var { DebuggerServer } = require("devtools/server/main");
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
var { assert } = DevToolsUtils;
var { TabSources } = require("./utils/TabSources");
var makeDebugger = require("./utils/make-debugger");
loader.lazyRequireGetter(this, "RootActor", "devtools/server/actors/root", true);
loader.lazyRequireGetter(this, "ThreadActor", "devtools/server/actors/script", true);
loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true);
loader.lazyRequireGetter(this, "BrowserAddonActor", "devtools/server/actors/addon", true);
loader.lazyRequireGetter(this, "WebExtensionActor", "devtools/server/actors/webextension", true);
loader.lazyRequireGetter(this, "WorkerActorList", "devtools/server/actors/worker", true);
loader.lazyRequireGetter(this, "ServiceWorkerRegistrationActorList", "devtools/server/actors/worker", true);
loader.lazyRequireGetter(this, "ProcessActorList", "devtools/server/actors/process", true);
loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
#ifdef MOZ_WEBEXTENSIONS
loader.lazyImporter(this, "ExtensionContent", "resource://gre/modules/ExtensionContent.jsm");
#endif
// Assumptions on events module:
// events needs to be dispatched synchronously,
// by calling the listeners in the order or registration.
loader.lazyRequireGetter(this, "events", "sdk/event/core");
loader.lazyRequireGetter(this, "StyleSheetActor", "devtools/server/actors/stylesheets", true);
function getWindowID(window) {
return window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.currentInnerWindowID;
}
function getDocShellChromeEventHandler(docShell) {
let handler = docShell.chromeEventHandler;
if (!handler) {
try {
// Toplevel xul window's docshell doesn't have chromeEventHandler
// attribute. The chrome event handler is just the global window object.
handler = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
} catch (e) {
// ignore
}
}
return handler;
}
function getChildDocShells(parentDocShell) {
let docShellsEnum = parentDocShell.getDocShellEnumerator(
Ci.nsIDocShellTreeItem.typeAll,
Ci.nsIDocShell.ENUMERATE_FORWARDS
);
let docShells = [];
while (docShellsEnum.hasMoreElements()) {
let docShell = docShellsEnum.getNext();
docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
docShells.push(docShell);
}
return docShells;
}
exports.getChildDocShells = getChildDocShells;
/**
* Browser-specific actors.
*/
function getInnerId(window) {
return window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
}
/**
* Yield all windows of type |windowType|, from the oldest window to the
* youngest, using nsIWindowMediator::getEnumerator. We're usually
* interested in "navigator:browser" windows.
*/
function* allAppShellDOMWindows(windowType) {
let e = Services.wm.getEnumerator(windowType);
while (e.hasMoreElements()) {
yield e.getNext();
}
}
exports.allAppShellDOMWindows = allAppShellDOMWindows;
/**
* Retrieve the window type of the top-level window |window|.
*/
function appShellDOMWindowType(window) {
/* This is what nsIWindowMediator's enumerator checks. */
return window.document.documentElement.getAttribute("windowtype");
}
/**
* Send Debugger:Shutdown events to all "navigator:browser" windows.
*/
function sendShutdownEvent() {
for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
let evt = win.document.createEvent("Event");
evt.initEvent("Debugger:Shutdown", true, false);
win.document.documentElement.dispatchEvent(evt);
}
}
exports.sendShutdownEvent = sendShutdownEvent;
/**
* Construct a root actor appropriate for use in a server running in a
* browser. The returned root actor:
* - respects the factories registered with DebuggerServer.addGlobalActor,
* - uses a BrowserTabList to supply tab actors,
* - sends all navigator:browser window documents a Debugger:Shutdown event
* when it exits.
*
* * @param connection DebuggerServerConnection
* The conection to the client.
*/
function createRootActor(connection) {
return new RootActor(connection, {
tabList: new BrowserTabList(connection),
addonList: new BrowserAddonList(connection),
workerList: new WorkerActorList(connection, {}),
serviceWorkerRegistrationList:
new ServiceWorkerRegistrationActorList(connection),
processList: new ProcessActorList(),
globalActorFactories: DebuggerServer.globalActorFactories,
onShutdown: sendShutdownEvent
});
}
/**
* A live list of BrowserTabActors representing the current browser tabs,
* to be provided to the root actor to answer 'listTabs' requests.
*
* This object also takes care of listening for TabClose events and
* onCloseWindow notifications, and exiting the BrowserTabActors concerned.
*
* (See the documentation for RootActor for the definition of the "live
* list" interface.)
*
* @param connection DebuggerServerConnection
* The connection in which this list's tab actors may participate.
*
* Some notes:
*
* This constructor is specific to the desktop browser environment; it
* maintains the tab list by tracking XUL windows and their XUL documents'
* "tabbrowser", "tab", and "browser" elements. What's entailed in maintaining
* an accurate list of open tabs in this context?
*
* - Opening and closing XUL windows:
*
* An nsIWindowMediatorListener is notified when new XUL windows (i.e., desktop
* windows) are opened and closed. It is not notified of individual content
* browser tabs coming and going within such a XUL window. That seems
* reasonable enough; it's concerned with XUL windows, not tab elements in the
* window's XUL document.
*
* However, even if we attach TabOpen and TabClose event listeners to each XUL
* window as soon as it is created:
*
* - we do not receive a TabOpen event for the initial empty tab of a new XUL
* window; and
*
* - we do not receive TabClose events for the tabs of a XUL window that has
* been closed.
*
* This means that TabOpen and TabClose events alone are not sufficient to
* maintain an accurate list of live tabs and mark tab actors as closed
* promptly. Our nsIWindowMediatorListener onCloseWindow handler must find and
* exit all actors for tabs that were in the closing window.
*
* Since this is a bit hairy, we don't make each individual attached tab actor
* responsible for noticing when it has been closed; we watch for that, and
* promise to call each actor's 'exit' method when it's closed, regardless of
* how we learn the news.
*
* - nsIWindowMediator locks
*
* nsIWindowMediator holds a lock protecting its list of top-level windows
* while it calls nsIWindowMediatorListener methods. nsIWindowMediator's
* GetEnumerator method also tries to acquire that lock. Thus, enumerating
* windows from within a listener method deadlocks (bug 873589). Rah. One
* can sometimes work around this by leaving the enumeration for a later
* tick.
*
* - Dragging tabs between windows:
*
* When a tab is dragged from one desktop window to another, we receive a
* TabOpen event for the new tab, and a TabClose event for the old tab; tab XUL
* elements do not really move from one document to the other (although their
* linked browser's content window objects do).
*
* However, while we could thus assume that each tab stays with the XUL window
* it belonged to when it was created, I'm not sure this is behavior one should
* rely upon. When a XUL window is closed, we take the less efficient, more
* conservative approach of simply searching the entire table for actors that
* belong to the closing XUL window, rather than trying to somehow track which
* XUL window each tab belongs to.
*
* - Title changes:
*
* For tabs living in the child process, we listen for DOMTitleChange message
* via the top-level window's message manager. Doing this also allows listening
* for title changes on Fennec.
* But as these messages aren't sent for tabs loaded in the parent process,
* we also listen for TabAttrModified event, which is fired only on Firefox
* desktop.
*/
function BrowserTabList(connection) {
this._connection = connection;
/*
* The XUL document of a tabbed browser window has "tab" elements, whose
* 'linkedBrowser' JavaScript properties are "browser" elements; those
* browsers' 'contentWindow' properties are wrappers on the tabs' content
* window objects.
*
* This map's keys are "browser" XUL elements; it maps each browser element
* to the tab actor we've created for its content window, if we've created
* one. This map serves several roles:
*
* - During iteration, we use it to find actors we've created previously.
*
* - On a TabClose event, we use it to find the tab's actor and exit it.
*
* - When the onCloseWindow handler is called, we iterate over it to find all
* tabs belonging to the closing XUL window, and exit them.
*
* - When it's empty, and the onListChanged hook is null, we know we can
* stop listening for events and notifications.
*
* We listen for TabClose events and onCloseWindow notifications in order to
* send onListChanged notifications, but also to tell actors when their
* referent has gone away and remove entries for dead browsers from this map.
* If that code is working properly, neither this map nor the actors in it
* should ever hold dead tabs alive.
*/
this._actorByBrowser = new Map();
/* The current onListChanged handler, or null. */
this._onListChanged = null;
/*
* True if we've been iterated over since we last called our onListChanged
* hook.
*/
this._mustNotify = false;
/* True if we're testing, and should throw if consistency checks fail. */
this._testing = false;
}
BrowserTabList.prototype.constructor = BrowserTabList;
/**
* Get the selected browser for the given navigator:browser window.
* @private
* @param window nsIChromeWindow
* The navigator:browser window for which you want the selected browser.
* @return nsIDOMElement|null
* The currently selected xul:browser element, if any. Note that the
* browser window might not be loaded yet - the function will return
* |null| in such cases.
*/
BrowserTabList.prototype._getSelectedBrowser = function (window) {
return window.gBrowser ? window.gBrowser.selectedBrowser : null;
};
/**
* Produces an iterable (in this case a generator) to enumerate all available
* browser tabs.
*/
BrowserTabList.prototype._getBrowsers = function* () {
// Iterate over all navigator:browser XUL windows.
for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
// For each tab in this XUL window, ensure that we have an actor for
// it, reusing existing actors where possible. We actually iterate
// over 'browser' XUL elements, and BrowserTabActor uses
// browser.contentWindow as the debuggee global.
for (let browser of this._getChildren(win)) {
yield browser;
}
}
};
BrowserTabList.prototype._getChildren = function (window) {
if (!window.gBrowser) {
return [];
}
let { gBrowser } = window;
if (!gBrowser.browsers) {
return [];
}
return gBrowser.browsers.filter(browser => {
// Filter tabs that are closing. listTabs calls made right after TabClose
// events still list tabs in process of being closed.
let tab = gBrowser.getTabForBrowser(browser);
return !tab.closing;
});
};
BrowserTabList.prototype.getList = function () {
let topXULWindow = Services.wm.getMostRecentWindow(
DebuggerServer.chromeWindowType);
let selectedBrowser = null;
if (topXULWindow) {
selectedBrowser = this._getSelectedBrowser(topXULWindow);
}
// As a sanity check, make sure all the actors presently in our map get
// picked up when we iterate over all windows' tabs.
let initialMapSize = this._actorByBrowser.size;
this._foundCount = 0;
// To avoid mysterious behavior if tabs are closed or opened mid-iteration,
// we update the map first, and then make a second pass over it to yield
// the actors. Thus, the sequence yielded is always a snapshot of the
// actors that were live when we began the iteration.
let actorPromises = [];
for (let browser of this._getBrowsers()) {
let selected = browser === selectedBrowser;
actorPromises.push(
this._getActorForBrowser(browser)
.then(actor => {
// Set the 'selected' properties on all actors correctly.
actor.selected = selected;
return actor;
})
);
}
if (this._testing && initialMapSize !== this._foundCount) {
throw new Error("_actorByBrowser map contained actors for dead tabs");
}
this._mustNotify = true;
this._checkListening();
return promise.all(actorPromises);
};
BrowserTabList.prototype._getActorForBrowser = function (browser) {
// Do we have an existing actor for this browser? If not, create one.
let actor = this._actorByBrowser.get(browser);
if (actor) {
this._foundCount++;
return actor.update();
}
actor = new BrowserTabActor(this._connection, browser);
this._actorByBrowser.set(browser, actor);
this._checkListening();
return actor.connect();
};
BrowserTabList.prototype.getTab = function ({ outerWindowID, tabId }) {
if (typeof outerWindowID == "number") {
// First look for in-process frames with this ID
let window = Services.wm.getOuterWindowWithId(outerWindowID);
// Safety check to prevent debugging top level window via getTab
if (window instanceof Ci.nsIDOMChromeWindow) {
return promise.reject({
error: "forbidden",
message: "Window with outerWindowID '" + outerWindowID + "' is chrome"
});
}
if (window) {
let iframe = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.containerElement;
if (iframe) {
return this._getActorForBrowser(iframe);
}
}
// Then also look on registered <xul:browsers> when using outerWindowID for
// OOP tabs
for (let browser of this._getBrowsers()) {
if (browser.outerWindowID == outerWindowID) {
return this._getActorForBrowser(browser);
}
}
return promise.reject({
error: "noTab",
message: "Unable to find tab with outerWindowID '" + outerWindowID + "'"
});
} else if (typeof tabId == "number") {
// Tabs OOP
for (let browser of this._getBrowsers()) {
if (browser.frameLoader.tabParent &&
browser.frameLoader.tabParent.tabId === tabId) {
return this._getActorForBrowser(browser);
}
}
return promise.reject({
error: "noTab",
message: "Unable to find tab with tabId '" + tabId + "'"
});
}
let topXULWindow = Services.wm.getMostRecentWindow(
DebuggerServer.chromeWindowType);
if (topXULWindow) {
let selectedBrowser = this._getSelectedBrowser(topXULWindow);
return this._getActorForBrowser(selectedBrowser);
}
return promise.reject({
error: "noTab",
message: "Unable to find any selected browser"
});
};
Object.defineProperty(BrowserTabList.prototype, "onListChanged", {
enumerable: true,
configurable: true,
get() {
return this._onListChanged;
},
set(v) {
if (v !== null && typeof v !== "function") {
throw new Error(
"onListChanged property may only be set to 'null' or a function");
}
this._onListChanged = v;
this._checkListening();
}
});
/**
* The set of tabs has changed somehow. Call our onListChanged handler, if
* one is set, and if we haven't already called it since the last iteration.
*/
BrowserTabList.prototype._notifyListChanged = function () {
if (!this._onListChanged) {
return;
}
if (this._mustNotify) {
this._onListChanged();
this._mustNotify = false;
}
};
/**
* Exit |actor|, belonging to |browser|, and notify the onListChanged
* handle if needed.
*/
BrowserTabList.prototype._handleActorClose = function (actor, browser) {
if (this._testing) {
if (this._actorByBrowser.get(browser) !== actor) {
throw new Error("BrowserTabActor not stored in map under given browser");
}
if (actor.browser !== browser) {
throw new Error("actor's browser and map key don't match");
}
}
this._actorByBrowser.delete(browser);
actor.exit();
this._notifyListChanged();
this._checkListening();
};
/**
* Make sure we are listening or not listening for activity elsewhere in
* the browser, as appropriate. Other than setting up newly created XUL
* windows, all listener / observer connection and disconnection should
* happen here.
*/
BrowserTabList.prototype._checkListening = function () {
/*
* If we have an onListChanged handler that we haven't sent an announcement
* to since the last iteration, we need to watch for tab creation as well as
* change of the currently selected tab and tab title changes of tabs in
* parent process via TabAttrModified (tabs oop uses DOMTitleChanges).
*
* Oddly, we don't need to watch for 'close' events here. If our actor list
* is empty, then either it was empty the last time we iterated, and no
* close events are possible, or it was not empty the last time we
* iterated, but all the actors have since been closed, and we must have
* sent a notification already when they closed.
*/
this._listenForEventsIf(this._onListChanged && this._mustNotify,
"_listeningForTabOpen",
["TabOpen", "TabSelect", "TabAttrModified"]);
/* If we have live actors, we need to be ready to mark them dead. */
this._listenForEventsIf(this._actorByBrowser.size > 0,
"_listeningForTabClose",
["TabClose", "TabRemotenessChange"]);
/*
* We must listen to the window mediator in either case, since that's the
* only way to find out about tabs that come and go when top-level windows
* are opened and closed.
*/
this._listenToMediatorIf((this._onListChanged && this._mustNotify) ||
(this._actorByBrowser.size > 0));
/*
* We also listen for title changed from the child process.
* This allows listening for title changes from Fennec and OOP tabs in Fx.
*/
this._listenForMessagesIf(this._onListChanged && this._mustNotify,
"_listeningForTitleChange",
["DOMTitleChanged"]);
};
/*
* Add or remove event listeners for all XUL windows.
*
* @param shouldListen boolean
* True if we should add event handlers; false if we should remove them.
* @param guard string
* The name of a guard property of 'this', indicating whether we're
* already listening for those events.
* @param eventNames array of strings
* An array of event names.
*/
BrowserTabList.prototype._listenForEventsIf =
function (shouldListen, guard, eventNames) {
if (!shouldListen !== !this[guard]) {
let op = shouldListen ? "addEventListener" : "removeEventListener";
for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
for (let name of eventNames) {
win[op](name, this, false);
}
}
this[guard] = shouldListen;
}
};
/*
* Add or remove message listeners for all XUL windows.
*
* @param aShouldListen boolean
* True if we should add message listeners; false if we should remove them.
* @param aGuard string
* The name of a guard property of 'this', indicating whether we're
* already listening for those messages.
* @param aMessageNames array of strings
* An array of message names.
*/
BrowserTabList.prototype._listenForMessagesIf =
function (shouldListen, guard, messageNames) {
if (!shouldListen !== !this[guard]) {
let op = shouldListen ? "addMessageListener" : "removeMessageListener";
for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
for (let name of messageNames) {
win.messageManager[op](name, this);
}
}
this[guard] = shouldListen;
}
};
/**
* Implement nsIMessageListener.
*/
BrowserTabList.prototype.receiveMessage = DevToolsUtils.makeInfallible(
function (message) {
let browser = message.target;
switch (message.name) {
case "DOMTitleChanged": {
let actor = this._actorByBrowser.get(browser);
if (actor) {
this._notifyListChanged();
this._checkListening();
}
break;
}
}
});
/**
* Implement nsIDOMEventListener.
*/
BrowserTabList.prototype.handleEvent =
DevToolsUtils.makeInfallible(function (event) {
let browser = event.target.linkedBrowser;
switch (event.type) {
case "TabOpen":
case "TabSelect": {
/* Don't create a new actor; iterate will take care of that. Just notify. */
this._notifyListChanged();
this._checkListening();
break;
}
case "TabClose": {
let actor = this._actorByBrowser.get(browser);
if (actor) {
this._handleActorClose(actor, browser);
}
break;
}
case "TabRemotenessChange": {
// We have to remove the cached actor as we have to create a new instance.
let actor = this._actorByBrowser.get(browser);
if (actor) {
this._actorByBrowser.delete(browser);
// Don't create a new actor; iterate will take care of that. Just notify.
this._notifyListChanged();
this._checkListening();
}
break;
}
case "TabAttrModified": {
// Remote <browser> title changes are handled via DOMTitleChange message
// TabAttrModified is only here for browsers in parent process which
// don't send this message.
if (browser.isRemoteBrowser) {
break;
}
let actor = this._actorByBrowser.get(browser);
if (actor) {
// TabAttrModified is fired in various cases, here only care about title
// changes
if (event.detail.changed.includes("label")) {
this._notifyListChanged();
this._checkListening();
}
}
break;
}
}
}, "BrowserTabList.prototype.handleEvent");
/*
* If |shouldListen| is true, ensure we've registered a listener with the
* window mediator. Otherwise, ensure we haven't registered a listener.
*/
BrowserTabList.prototype._listenToMediatorIf = function (shouldListen) {
if (!shouldListen !== !this._listeningToMediator) {
let op = shouldListen ? "addListener" : "removeListener";
Services.wm[op](this);
this._listeningToMediator = shouldListen;
}
};
/**
* nsIWindowMediatorListener implementation.
*
* See _onTabClosed for explanation of why we needn't actually tweak any
* actors or tables here.
*
* An nsIWindowMediatorListener's methods get passed all sorts of windows; we
* only care about the tab containers. Those have 'getBrowser' methods.
*/
BrowserTabList.prototype.onWindowTitleChange = () => { };
BrowserTabList.prototype.onOpenWindow =
DevToolsUtils.makeInfallible(function (window) {
let handleLoad = DevToolsUtils.makeInfallible(() => {
/* We don't want any further load events from this window. */
window.removeEventListener("load", handleLoad, false);
if (appShellDOMWindowType(window) !== DebuggerServer.chromeWindowType) {
return;
}
// Listen for future tab activity.
if (this._listeningForTabOpen) {
window.addEventListener("TabOpen", this, false);
window.addEventListener("TabSelect", this, false);
window.addEventListener("TabAttrModified", this, false);
}
if (this._listeningForTabClose) {
window.addEventListener("TabClose", this, false);
window.addEventListener("TabRemotenessChange", this, false);
}
if (this._listeningForTitleChange) {
window.messageManager.addMessageListener("DOMTitleChanged", this);
}
// As explained above, we will not receive a TabOpen event for this
// document's initial tab, so we must notify our client of the new tab
// this will have.
this._notifyListChanged();
});
/*
* You can hardly do anything at all with a XUL window at this point; it
* doesn't even have its document yet. Wait until its document has
* loaded, and then see what we've got. This also avoids
* nsIWindowMediator enumeration from within listeners (bug 873589).
*/
window = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
window.addEventListener("load", handleLoad, false);
}, "BrowserTabList.prototype.onOpenWindow");
BrowserTabList.prototype.onCloseWindow =
DevToolsUtils.makeInfallible(function (window) {
window = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
if (appShellDOMWindowType(window) !== DebuggerServer.chromeWindowType) {
return;
}
/*
* nsIWindowMediator deadlocks if you call its GetEnumerator method from
* a nsIWindowMediatorListener's onCloseWindow hook (bug 873589), so
* handle the close in a different tick.
*/
Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(() => {
/*
* Scan the entire map for actors representing tabs that were in this
* top-level window, and exit them.
*/
for (let [browser, actor] of this._actorByBrowser) {
/* The browser document of a closed window has no default view. */
if (!browser.ownerDocument.defaultView) {
this._handleActorClose(actor, browser);
}
}
}, "BrowserTabList.prototype.onCloseWindow's delayed body"), 0);
}, "BrowserTabList.prototype.onCloseWindow");
exports.BrowserTabList = BrowserTabList;
/**
* Creates a TabActor whose main goal is to manage lifetime and
* expose the tab actors being registered via DebuggerServer.registerModule.
* But also track the lifetime of the document being tracked.
*
* ### Main requests:
*
* `attach`/`detach` requests:
* - start/stop document watching:
* Starts watching for new documents and emits `tabNavigated` and
* `frameUpdate` over RDP.
* - retrieve the thread actor:
* Instantiates a ThreadActor that can be later attached to in order to
* debug JS sources in the document.
* `switchToFrame`:
* Change the targeted document of the whole TabActor, and its child tab actors
* to an iframe or back to its original document.
*
* Most of the TabActor properties (like `chromeEventHandler` or `docShells`)
* are meant to be used by the various child tab actors.
*
* ### RDP events:
*
* - `tabNavigated`:
* Sent when the tab is about to navigate or has just navigated to
* a different document.
* This event contains the following attributes:
* * url (string) The new URI being loaded.
* * nativeConsoleAPI (boolean) `false` if the console API of the page has
* been overridden (e.g. by Firebug),
* `true` if the Gecko implementation is used.
* * state (string) `start` if we just start requesting the new URL,
* `stop` if the new URL is done loading.
* * isFrameSwitching (boolean) Indicates the event is dispatched when
* switching the TabActor context to
* a different frame. When we switch to
* an iframe, there is no document load.
* The targeted document is most likely
* going to be already done loading.
* * title (string) The document title being loaded.
* (sent only on state=stop)
*
* - `frameUpdate`:
* Sent when there was a change in the child frames contained in the document
* or when the tab's context was switched to another frame.
* This event can have four different forms depending on the type of change:
* * One or many frames are updated:
* { frames: [{ id, url, title, parentID }, ...] }
* * One frame got destroyed:
* { frames: [{ id, destroy: true }]}
* * All frames got destroyed:
* { destroyAll: true }
* * We switched the context of the TabActor to a specific frame:
* { selected: #id }
*
* ### Internal, non-rdp events:
* Various events are also dispatched on the TabActor itself that are not
* related to RDP, so, not sent to the client. They all relate to the documents
* tracked by the TabActor (its main targeted document, but also any of its
* iframes).
* - will-navigate
* This event fires once navigation starts.
* All pending user prompts are dealt with,
* but it is fired before the first request starts.
* - navigate
* This event is fired once the document's readyState is "complete".
* - window-ready
* This event is fired on three distinct scenarios:
* * When a new Window object is crafted, equivalent of `DOMWindowCreated`.
* It is dispatched before any page script is executed.
* * We will have already received a window-ready event for this window
* when it was created, but we received a window-destroyed event when
* it was frozen into the bfcache, and now the user navigated back to
* this page, so it's now live again and we should resume handling it.
* * For each existing document, when an `attach` request is received.
* At this point scripts in the page will be already loaded.
* - window-destroyed
* This event is fired in two cases:
* * When the window object is destroyed, i.e. when the related document
* is garbage collected. This can happen when the tab is closed or the
* iframe is removed from the DOM.
* It is equivalent of `inner-window-destroyed` event.
* * When the page goes into the bfcache and gets frozen.
* The equivalent of `pagehide`.
* - changed-toplevel-document
* This event fires when we switch the TabActor targeted document
* to one of its iframes, or back to its original top document.
* It is dispatched between window-destroyed and window-ready.
* - stylesheet-added
* This event is fired when a StyleSheetActor is created.
* It contains the following attribute :
* * actor (StyleSheetActor) The created actor.
*
* Note that *all* these events are dispatched in the following order
* when we switch the context of the TabActor to a given iframe:
* - will-navigate
* - window-destroyed
* - changed-toplevel-document
* - window-ready
* - navigate
*
* This class is subclassed by ContentActor and others.
* Subclasses are expected to implement a getter for the docShell property.
*
* @param connection DebuggerServerConnection
* The conection to the client.
*/
function TabActor(connection) {
this.conn = connection;
this._tabActorPool = null;
// A map of actor names to actor instances provided by extensions.
this._extraActors = {};
this._exited = false;
this._sources = null;
// Map of DOM stylesheets to StyleSheetActors
this._styleSheetActors = new Map();
this._shouldAddNewGlobalAsDebuggee =
this._shouldAddNewGlobalAsDebuggee.bind(this);
this.makeDebugger = makeDebugger.bind(null, {
findDebuggees: () => {
return this.windows.concat(this.webextensionsContentScriptGlobals);
},
shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee
});
// Flag eventually overloaded by sub classes in order to watch new docshells
// Used by the ChromeActor to list all frames in the Browser Toolbox
this.listenForNewDocShells = false;
this.traits = {
reconfigure: true,
// Supports frame listing via `listFrames` request and `frameUpdate` events
// as well as frame switching via `switchToFrame` request
frames: true,
// Do not require to send reconfigure request to reset the document state
// to what it was before using the TabActor
noTabReconfigureOnClose: true
};
this._workerActorList = null;
this._workerActorPool = null;
this._onWorkerActorListChanged = this._onWorkerActorListChanged.bind(this);
}
// XXX (bug 710213): TabActor attach/detach/exit/disconnect is a
// *complete* mess, needs to be rethought asap.
TabActor.prototype = {
traits: null,
// Optional console API listener options (e.g. used by the WebExtensionActor to
// filter console messages by addonID), set to an empty (no options) object by default.
consoleAPIListenerOptions: {},
// Optional TabSources filter function (e.g. used by the WebExtensionActor to filter
// sources by addonID), allow all sources by default.
_allowSource() {
return true;
},
get exited() {
return this._exited;
},
get attached() {
return !!this._attached;
},
_tabPool: null,
get tabActorPool() {
return this._tabPool;
},
_contextPool: null,
get contextActorPool() {
return this._contextPool;
},
// A constant prefix that will be used to form the actor ID by the server.
actorPrefix: "tab",
/**
* An object on which listen for DOMWindowCreated and pageshow events.
*/
get chromeEventHandler() {
return getDocShellChromeEventHandler(this.docShell);
},
/**
* Getter for the nsIMessageManager associated to the tab.
*/
get messageManager() {
try {
return this.docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIContentFrameMessageManager);
} catch (e) {
return null;
}
},
/**
* Getter for the tab's doc shell.
*/
get docShell() {
throw new Error(
"The docShell getter should be implemented by a subclass of TabActor");
},
/**
* Getter for the list of all docshell in this tabActor
* @return {Array}
*/
get docShells() {
return getChildDocShells(this.docShell);
},
/**
* Getter for the tab content's DOM window.
*/
get window() {
// On xpcshell, there is no document
if (this.docShell) {
return this.docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
}
return null;
},
get outerWindowID() {
if (this.window) {
return this.window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
}
return null;
},
#ifdef MOZ_WEBEXTENSIONS
/**
* Getter for the WebExtensions ContentScript globals related to the
* current tab content's DOM window.
*/
get webextensionsContentScriptGlobals() {
// Ignore xpcshell runtime which spawn TabActors without a window.
if (this.window) {
return ExtensionContent.getContentScriptGlobalsForWindow(this.window);
}
return [];
},
#endif
/**
* Getter for the list of all content DOM windows in this tabActor
* @return {Array}
*/
get windows() {
return this.docShells.map(docShell => {
return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
});
},
/**
* Getter for the original docShell the tabActor got attached to in the first
* place.
* Note that your actor should normally *not* rely on this top level docShell
* if you want it to show information relative to the iframe that's currently
* being inspected in the toolbox.
*/
get originalDocShell() {
if (!this._originalWindow) {
return this.docShell;
}
return this._originalWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
},
/**
* Getter for the original window the tabActor got attached to in the first
* place.
* Note that your actor should normally *not* rely on this top level window if
* you want it to show information relative to the iframe that's currently
* being inspected in the toolbox.
*/
get originalWindow() {
return this._originalWindow || this.window;
},
/**
* Getter for the nsIWebProgress for watching this window.
*/
get webProgress() {
return this.docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
},
/**
* Getter for the nsIWebNavigation for the tab.
*/
get webNavigation() {
return this.docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation);
},
/**
* Getter for the tab's document.
*/
get contentDocument() {
return this.webNavigation.document;
},
/**
* Getter for the tab title.
* @return string
* Tab title.
*/
get title() {
return this.contentDocument.contentTitle;
},
/**
* Getter for the tab URL.
* @return string
* Tab URL.
*/
get url() {
if (this.webNavigation.currentURI) {
return this.webNavigation.currentURI.spec;
}
// Abrupt closing of the browser window may leave callbacks without a
// currentURI.
return null;
},
get sources() {
if (!this._sources) {
this._sources = new TabSources(this.threadActor, this._allowSource);
}
return this._sources;
},
/**
* This is called by BrowserTabList.getList for existing tab actors prior to
* calling |form| below. It can be used to do any async work that may be
* needed to assemble the form.
*/
update() {
return promise.resolve(this);
},
form() {
assert(!this.exited,
"form() shouldn't be called on exited browser actor.");
assert(this.actorID,
"tab should have an actorID.");
let response = {
actor: this.actorID
};
// We may try to access window while the document is closing, then
// accessing window throws. Also on xpcshell we are using tabactor even if
// there is no valid document.
if (this.docShell && !this.docShell.isBeingDestroyed()) {
response.title = this.title;
response.url = this.url;
response.outerWindowID = this.outerWindowID;
}
// Always use the same ActorPool, so existing actor instances
// (created in createExtraActors) are not lost.
if (!this._tabActorPool) {
this._tabActorPool = new ActorPool(this.conn);
this.conn.addActorPool(this._tabActorPool);
}
// Walk over tab actor factories and make sure they are all
// instantiated and added into the ActorPool. Note that some
// factories can be added dynamically by extensions.
this._createExtraActors(DebuggerServer.tabActorFactories,
this._tabActorPool);
this._appendExtraActors(response);
return response;
},
/**
* Called when the actor is removed from the connection.
*/
disconnect() {
this.exit();
},
/**
* Called by the root actor when the underlying tab is closed.
*/
exit() {
if (this.exited) {
return;
}
// Tell the thread actor that the tab is closed, so that it may terminate
// instead of resuming the debuggee script.
if (this._attached) {
this.threadActor._tabClosed = true;
}
this._detach();
Object.defineProperty(this, "docShell", {
value: null,
configurable: true
});
this._extraActors = null;
this._exited = true;
},
/**
* Return true if the given global is associated with this tab and should be
* added as a debuggee, false otherwise.
*/
_shouldAddNewGlobalAsDebuggee(wrappedGlobal) {
if (wrappedGlobal.hostAnnotations &&
wrappedGlobal.hostAnnotations.type == "document" &&
wrappedGlobal.hostAnnotations.element === this.window) {
return true;
}
let global = unwrapDebuggerObjectGlobal(wrappedGlobal);
if (!global) {
return false;
}
// Check if the global is a sdk page-mod sandbox.
let metadata = {};
let id = "";
try {
id = getInnerId(this.window);
metadata = Cu.getSandboxMetadata(global);
} catch (e) {
// ignore
}
if (metadata
&& metadata["inner-window-id"]
&& metadata["inner-window-id"] == id) {
return true;
}
return false;
},
/* Support for DebuggerServer.addTabActor. */
_createExtraActors: createExtraActors,
_appendExtraActors: appendExtraActors,
/**
* Does the actual work of attaching to a tab.
*/
_attach() {
if (this._attached) {
return;
}
// Create a pool for tab-lifetime actors.
assert(!this._tabPool, "Shouldn't have a tab pool if we weren't attached.");
this._tabPool = new ActorPool(this.conn);
this.conn.addActorPool(this._tabPool);
// ... and a pool for context-lifetime actors.
this._pushContext();
// on xpcshell, there is no document
if (this.window) {
this._progressListener = new DebuggerProgressListener(this);
// Save references to the original document we attached to
this._originalWindow = this.window;
// Ensure replying to attach() request first
// before notifying about new docshells.
DevToolsUtils.executeSoon(() => this._watchDocshells());
}
this._attached = true;
},
_watchDocshells() {
// In child processes, we watch all docshells living in the process.
if (this.listenForNewDocShells) {
Services.obs.addObserver(this, "webnavigation-create", false);
}
Services.obs.addObserver(this, "webnavigation-destroy", false);
// We watch for all child docshells under the current document,
this._progressListener.watch(this.docShell);
// And list all already existing ones.
this._updateChildDocShells();
},
onSwitchToFrame(request) {
let windowId = request.windowId;
let win;
try {
win = Services.wm.getOuterWindowWithId(windowId);
} catch (e) {
// ignore
}
if (!win) {
return { error: "noWindow",
message: "The related docshell is destroyed or not found" };
} else if (win == this.window) {
return {};
}
// Reply first before changing the document
DevToolsUtils.executeSoon(() => this._changeTopLevelDocument(win));
return {};
},
onListFrames(request) {
let windows = this._docShellsToWindows(this.docShells);
return { frames: windows };
},
onListWorkers(request) {
if (!this.attached) {
return { error: "wrongState" };
}
if (this._workerActorList === null) {
this._workerActorList = new WorkerActorList(this.conn, {
type: Ci.nsIWorkerDebugger.TYPE_DEDICATED,
window: this.window
});
}
return this._workerActorList.getList().then((actors) => {
let pool = new ActorPool(this.conn);
for (let actor of actors) {
pool.addActor(actor);
}
this.conn.removeActorPool(this._workerActorPool);
this._workerActorPool = pool;
this.conn.addActorPool(this._workerActorPool);
this._workerActorList.onListChanged = this._onWorkerActorListChanged;
return {
"from": this.actorID,
"workers": actors.map((actor) => actor.form())
};
});
},
_onWorkerActorListChanged() {
this._workerActorList.onListChanged = null;
this.conn.sendActorEvent(this.actorID, "workerListChanged");
},
observe(subject, topic, data) {
// Ignore any event that comes before/after the tab actor is attached
// That typically happens during firefox shutdown.
if (!this.attached) {
return;
}
if (topic == "webnavigation-create") {
subject.QueryInterface(Ci.nsIDocShell);
this._onDocShellCreated(subject);
} else if (topic == "webnavigation-destroy") {
this._onDocShellDestroy(subject);
}
},
_onDocShellCreated(docShell) {
// (chrome-)webnavigation-create is fired very early during docshell
// construction. In new root docshells within child processes, involving
// TabChild, this event is from within this call:
// http://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l912
// whereas the chromeEventHandler (and most likely other stuff) is set
// later:
// http://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l944
// So wait a tick before watching it:
DevToolsUtils.executeSoon(() => {
// Bug 1142752: sometimes, the docshell appears to be immediately
// destroyed, bailout early to prevent random exceptions.
if (docShell.isBeingDestroyed()) {
return;
}
// In child processes, we have new root docshells,
// let's watch them and all their child docshells.
if (this._isRootDocShell(docShell)) {
this._progressListener.watch(docShell);
}
this._notifyDocShellsUpdate([docShell]);
});
},
_onDocShellDestroy(docShell) {
let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
this._notifyDocShellDestroy(webProgress);
},
_isRootDocShell(docShell) {
// Should report as root docshell:
// - New top level window's docshells, when using ChromeActor against a
// process. It allows tracking iframes of the newly opened windows
// like Browser console or new browser windows.
// - MozActivities or window.open frames on B2G, where a new root docshell
// is spawn in the child process of the app.
return !docShell.parent;
},
// Convert docShell list to windows objects list being sent to the client
_docShellsToWindows(docshells) {
return docshells.map(docShell => {
let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
let window = webProgress.DOMWindow;
let id = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
let parentID = undefined;
// Ignore the parent of the original document on non-e10s firefox,
// as we get the xul window as parent and don't care about it.
if (window.parent && window != this._originalWindow) {
parentID = window.parent
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
}
// Collect the addonID from the document origin attributes.
let addonID = window.document.nodePrincipal.originAttributes.addonId;
return {
id,
parentID,
addonID,
url: window.location.href,
title: window.document.title,
};
});
},
_notifyDocShellsUpdate(docshells) {
let windows = this._docShellsToWindows(docshells);
// Do not send the `frameUpdate` event if the windows array is empty.
if (windows.length == 0) {
return;
}
this.conn.send({
from: this.actorID,
type: "frameUpdate",
frames: windows
});
},
_updateChildDocShells() {
this._notifyDocShellsUpdate(this.docShells);
},
_notifyDocShellDestroy(webProgress) {
webProgress = webProgress.QueryInterface(Ci.nsIWebProgress);
let id = webProgress.DOMWindow
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
this.conn.send({
from: this.actorID,
type: "frameUpdate",
frames: [{
id,
destroy: true
}]
});
// Stop watching this docshell (the unwatch() method will check if we
// started watching it before).
webProgress.QueryInterface(Ci.nsIDocShell);
this._progressListener.unwatch(webProgress);
if (webProgress.DOMWindow == this._originalWindow) {
// If the original top level document we connected to is removed,
// we try to switch to any other top level document
let rootDocShells = this.docShells
.filter(d => {
return d != this.docShell &&
this._isRootDocShell(d);
});
if (rootDocShells.length > 0) {
let newRoot = rootDocShells[0];
this._originalWindow = newRoot.DOMWindow;
this._changeTopLevelDocument(this._originalWindow);
} else {
// If for some reason (typically during Firefox shutdown), the original
// document is destroyed, and there is no other top level docshell,
// we detach the tab actor to unregister all listeners and prevent any
// exception
this.exit();
}
return;
}
// If the currently targeted context is destroyed,
// and we aren't on the top-level document,
// we have to switch to the top-level one.
if (webProgress.DOMWindow == this.window &&
this.window != this._originalWindow) {
this._changeTopLevelDocument(this._originalWindow);
}
},
_notifyDocShellDestroyAll() {
this.conn.send({
from: this.actorID,
type: "frameUpdate",
destroyAll: true
});
},
/**
* Creates a thread actor and a pool for context-lifetime actors. It then sets
* up the content window for debugging.
*/
_pushContext() {
assert(!this._contextPool, "Can't push multiple contexts");
this._contextPool = new ActorPool(this.conn);
this.conn.addActorPool(this._contextPool);
this.threadActor = new ThreadActor(this, this.window);
this._contextPool.addActor(this.threadActor);
},
/**
* Exits the current thread actor and removes the context-lifetime actor pool.
* The content window is no longer being debugged after this call.
*/
_popContext() {
assert(!!this._contextPool, "No context to pop.");
this.conn.removeActorPool(this._contextPool);
this._contextPool = null;
this.threadActor.exit();
this.threadActor = null;
this._sources = null;
},
/**
* Does the actual work of detaching from a tab.
*
* @returns false if the tab wasn't attached or true of detaching succeeds.
*/
_detach() {
if (!this.attached) {
return false;
}
// Check for docShell availability, as it can be already gone
// during Firefox shutdown.
if (this.docShell) {
this._progressListener.unwatch(this.docShell);
this._restoreDocumentSettings();
}
if (this._progressListener) {
this._progressListener.destroy();
this._progressListener = null;
this._originalWindow = null;
// Removes the observers being set in _watchDocShells
if (this.listenForNewDocShells) {
Services.obs.removeObserver(this, "webnavigation-create");
}
Services.obs.removeObserver(this, "webnavigation-destroy");
}
this._popContext();
// Shut down actors that belong to this tab's pool.
for (let sheetActor of this._styleSheetActors.values()) {
this._tabPool.removeActor(sheetActor);
}
this._styleSheetActors.clear();
this.conn.removeActorPool(this._tabPool);
this._tabPool = null;
if (this._tabActorPool) {
this.conn.removeActorPool(this._tabActorPool);
this._tabActorPool = null;
}
// Make sure that no more workerListChanged notifications are sent.
if (this._workerActorList !== null) {
this._workerActorList.onListChanged = null;
this._workerActorList = null;
}
if (this._workerActorPool !== null) {
this.conn.removeActorPool(this._workerActorPool);
this._workerActorPool = null;
}
this._attached = false;
this.conn.send({ from: this.actorID,
type: "tabDetached" });
return true;
},
// Protocol Request Handlers
onAttach(request) {
if (this.exited) {
return { type: "exited" };
}
this._attach();
return {
type: "tabAttached",
threadActor: this.threadActor.actorID,
cacheDisabled: this._getCacheDisabled(),
javascriptEnabled: this._getJavascriptEnabled(),
traits: this.traits,
};
},
onDetach(request) {
if (!this._detach()) {
return { error: "wrongState" };
}
return { type: "detached" };
},
/**
* Bring the tab's window to front.
*/
onFocus() {
if (this.window) {
this.window.focus();
}
return {};
},
/**
* Reload the page in this tab.
*/
onReload(request) {
let force = request && request.options && request.options.force;
// Wait a tick so that the response packet can be dispatched before the
// subsequent navigation event packet.
Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(() => {
// This won't work while the browser is shutting down and we don't really
// care.
if (Services.startup.shuttingDown) {
return;
}
this.webNavigation.reload(force ?
Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE :
Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
}, "TabActor.prototype.onReload's delayed body"), 0);
return {};
},
/**
* Navigate this tab to a new location
*/
onNavigateTo(request) {
// Wait a tick so that the response packet can be dispatched before the
// subsequent navigation event packet.
Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(() => {
this.window.location = request.url;
}, "TabActor.prototype.onNavigateTo's delayed body"), 0);
return {};
},
/**
* Reconfigure options.
*/
onReconfigure(request) {
let options = request.options || {};
if (!this.docShell) {
// The tab is already closed.
return {};
}
this._toggleDevToolsSettings(options);
return {};
},
/**
* Handle logic to enable/disable JS/cache/Service Worker testing.
*/
_toggleDevToolsSettings(options) {
// Wait a tick so that the response packet can be dispatched before the
// subsequent navigation event packet.
let reload = false;
if (typeof options.javascriptEnabled !== "undefined" &&
options.javascriptEnabled !== this._getJavascriptEnabled()) {
this._setJavascriptEnabled(options.javascriptEnabled);
reload = true;
}
if (typeof options.cacheDisabled !== "undefined" &&
options.cacheDisabled !== this._getCacheDisabled()) {
this._setCacheDisabled(options.cacheDisabled);
}
if ((typeof options.serviceWorkersTestingEnabled !== "undefined") &&
(options.serviceWorkersTestingEnabled !==
this._getServiceWorkersTestingEnabled())) {
this._setServiceWorkersTestingEnabled(
options.serviceWorkersTestingEnabled
);
}
// Reload if:
// - there's an explicit `performReload` flag and it's true
// - there's no `performReload` flag, but it makes sense to do so
let hasExplicitReloadFlag = "performReload" in options;
if ((hasExplicitReloadFlag && options.performReload) ||
(!hasExplicitReloadFlag && reload)) {
this.onReload();
}
},
/**
* Opposite of the _toggleDevToolsSettings method, that reset document state
* when closing the toolbox.
*/
_restoreDocumentSettings() {
this._restoreJavascript();
this._setCacheDisabled(false);
this._setServiceWorkersTestingEnabled(false);
},
/**
* Disable or enable the cache via docShell.
*/
_setCacheDisabled(disabled) {
let enable = Ci.nsIRequest.LOAD_NORMAL;
let disable = Ci.nsIRequest.LOAD_BYPASS_CACHE |
Ci.nsIRequest.INHIBIT_CACHING;
this.docShell.defaultLoadFlags = disabled ? disable : enable;
},
/**
* Disable or enable JS via docShell.
*/
_wasJavascriptEnabled: null,
_setJavascriptEnabled(allow) {
if (this._wasJavascriptEnabled === null) {
this._wasJavascriptEnabled = this.docShell.allowJavascript;
}
this.docShell.allowJavascript = allow;
},
/**
* Restore JS state, before the actor modified it.
*/
_restoreJavascript() {
if (this._wasJavascriptEnabled !== null) {
this._setJavascriptEnabled(this._wasJavascriptEnabled);
this._wasJavascriptEnabled = null;
}
},
/**
* Return JS allowed status.
*/
_getJavascriptEnabled() {
if (!this.docShell) {
// The tab is already closed.
return null;
}
return this.docShell.allowJavascript;
},
/**
* Disable or enable the service workers testing features.
*/
_setServiceWorkersTestingEnabled(enabled) {
let windowUtils = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
windowUtils.serviceWorkersTestingEnabled = enabled;
},
/**
* Return cache allowed status.
*/
_getCacheDisabled() {
if (!this.docShell) {
// The tab is already closed.
return null;
}
let disable = Ci.nsIRequest.LOAD_BYPASS_CACHE |
Ci.nsIRequest.INHIBIT_CACHING;
return this.docShell.defaultLoadFlags === disable;
},
/**
* Return service workers testing allowed status.
*/
_getServiceWorkersTestingEnabled() {
if (!this.docShell) {
// The tab is already closed.
return null;
}
let windowUtils = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
return windowUtils.serviceWorkersTestingEnabled;
},
/**
* Prepare to enter a nested event loop by disabling debuggee events.
*/
preNest() {
if (!this.window) {
// The tab is already closed.
return;
}
let windowUtils = this.window
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
windowUtils.suppressEventHandling(true);
windowUtils.suspendTimeouts();
},
/**
* Prepare to exit a nested event loop by enabling debuggee events.
*/
postNest(nestData) {
if (!this.window) {
// The tab is already closed.
return;
}
let windowUtils = this.window
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
windowUtils.resumeTimeouts();
windowUtils.suppressEventHandling(false);
},
_changeTopLevelDocument(window) {
// Fake a will-navigate on the previous document
// to let a chance to unregister it
this._willNavigate(this.window, window.location.href, null, true);
this._windowDestroyed(this.window, null, true);
// Immediately change the window as this window, if in process of unload
// may already be non working on the next cycle and start throwing
this._setWindow(window);
DevToolsUtils.executeSoon(() => {
// Then fake window-ready and navigate on the given document
this._windowReady(window, true);
DevToolsUtils.executeSoon(() => {
this._navigate(window, true);
});
});
},
_setWindow(window) {
let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
// Here is the very important call where we switch the currently
// targeted context (it will indirectly update this.window and
// many other attributes defined from docShell).
Object.defineProperty(this, "docShell", {
value: docShell,
enumerable: true,
configurable: true
});
events.emit(this, "changed-toplevel-document");
this.conn.send({
from: this.actorID,
type: "frameUpdate",
selected: this.outerWindowID
});
},
/**
* Handle location changes, by clearing the previous debuggees and enabling
* debugging, which may have been disabled temporarily by the
* DebuggerProgressListener.
*/
_windowReady(window, isFrameSwitching = false) {
let isTopLevel = window == this.window;
// We just reset iframe list on WillNavigate, so we now list all existing
// frames when we load a new document in the original window
if (window == this._originalWindow && !isFrameSwitching) {
this._updateChildDocShells();
}
events.emit(this, "window-ready", {
window: window,
isTopLevel: isTopLevel,
id: getWindowID(window)
});
// TODO bug 997119: move that code to ThreadActor by listening to
// window-ready
let threadActor = this.threadActor;
if (isTopLevel && threadActor.state != "detached") {
this.sources.reset({ sourceMaps: true });
threadActor.clearDebuggees();
threadActor.dbg.enabled = true;
threadActor.maybePauseOnExceptions();
// Update the global no matter if the debugger is on or off,
// otherwise the global will be wrong when enabled later.
threadActor.global = window;
}
// Refresh the debuggee list when a new window object appears (top window or
// iframe).
if (threadActor.attached) {
threadActor.dbg.addDebuggees();
}
},
_windowDestroyed(window, id = null, isFrozen = false) {
events.emit(this, "window-destroyed", {
window: window,
isTopLevel: window == this.window,
id: id || getWindowID(window),
isFrozen: isFrozen
});
},
/**
* Start notifying server and client about a new document
* being loaded in the currently targeted context.
*/
_willNavigate(window, newURI, request, isFrameSwitching = false) {
let isTopLevel = window == this.window;
let reset = false;
if (window == this._originalWindow && !isFrameSwitching) {
// Clear the iframe list if the original top-level document changes.
this._notifyDocShellDestroyAll();
// If the top level document changes and we are targeting
// an iframe, we need to reset to the upcoming new top level document.
// But for this will-navigate event, we will dispatch on the old window.
// (The inspector codebase expect to receive will-navigate for the
// currently displayed document in order to cleanup the markup view)
if (this.window != this._originalWindow) {
reset = true;
window = this.window;
isTopLevel = true;
}
}
// will-navigate event needs to be dispatched synchronously,
// by calling the listeners in the order or registration.
// This event fires once navigation starts,
// (all pending user prompts are dealt with),
// but before the first request starts.
events.emit(this, "will-navigate", {
window: window,
isTopLevel: isTopLevel,
newURI: newURI,
request: request
});
// We don't do anything for inner frames in TabActor.
// (we will only update thread actor on window-ready)
if (!isTopLevel) {
return;
}
// Proceed normally only if the debuggee is not paused.
// TODO bug 997119: move that code to ThreadActor by listening to
// will-navigate
let threadActor = this.threadActor;
if (threadActor.state == "paused") {
this.conn.send(
threadActor.unsafeSynchronize(Promise.resolve(threadActor.onResume())));
threadActor.dbg.enabled = false;
}
threadActor.disableAllBreakpoints();
this.conn.send({
from: this.actorID,
type: "tabNavigated",
url: newURI,
nativeConsoleAPI: true,
state: "start",
isFrameSwitching: isFrameSwitching
});
if (reset) {
this._setWindow(this._originalWindow);
}
},
/**
* Notify server and client about a new document done loading in the current
* targeted context.
*/
_navigate(window, isFrameSwitching = false) {
let isTopLevel = window == this.window;
// navigate event needs to be dispatched synchronously,
// by calling the listeners in the order or registration.
// This event is fired once the document is loaded,
// after the load event, it's document ready-state is 'complete'.
events.emit(this, "navigate", {
window: window,
isTopLevel: isTopLevel
});
// We don't do anything for inner frames in TabActor.
// (we will only update thread actor on window-ready)
if (!isTopLevel) {
return;
}
// TODO bug 997119: move that code to ThreadActor by listening to navigate
let threadActor = this.threadActor;
if (threadActor.state == "running") {
threadActor.dbg.enabled = true;
}
this.conn.send({
from: this.actorID,
type: "tabNavigated",
url: this.url,
title: this.title,
nativeConsoleAPI: this.hasNativeConsoleAPI(this.window),
state: "stop",
isFrameSwitching: isFrameSwitching
});
},
/**
* Tells if the window.console object is native or overwritten by script in
* the page.
*
* @param nsIDOMWindow window
* The window object you want to check.
* @return boolean
* True if the window.console object is native, or false otherwise.
*/
hasNativeConsoleAPI(window) {
let isNative = false;
try {
// We are very explicitly examining the "console" property of
// the non-Xrayed object here.
let console = window.wrappedJSObject.console;
isNative = new XPCNativeWrapper(console).IS_NATIVE_CONSOLE;
} catch (ex) {
// ignore
}
return isNative;
},
/**
* Create or return the StyleSheetActor for a style sheet. This method
* is here because the Style Editor and Inspector share style sheet actors.
*
* @param DOMStyleSheet styleSheet
* The style sheet to create an actor for.
* @return StyleSheetActor actor
* The actor for this style sheet.
*
*/
createStyleSheetActor(styleSheet) {
if (this._styleSheetActors.has(styleSheet)) {
return this._styleSheetActors.get(styleSheet);
}
let actor = new StyleSheetActor(styleSheet, this);
this._styleSheetActors.set(styleSheet, actor);
this._tabPool.addActor(actor);
events.emit(this, "stylesheet-added", actor);
return actor;
},
removeActorByName(name) {
if (name in this._extraActors) {
const actor = this._extraActors[name];
if (this._tabActorPool.has(actor)) {
this._tabActorPool.removeActor(actor);
}
delete this._extraActors[name];
}
},
/**
* Takes a packet containing a url, line and column and returns
* the updated url, line and column based on the current source mapping
* (source mapped files, pretty prints).
*
* @param {String} request.url
* @param {Number} request.line
* @param {Number?} request.column
* @return {Promise<Object>}
*/
onResolveLocation(request) {
let { url, line } = request;
let column = request.column || 0;
const scripts = this.threadActor.dbg.findScripts({ url });
if (!scripts[0] || !scripts[0].source) {
return promise.resolve({
from: this.actorID,
type: "resolveLocation",
error: "SOURCE_NOT_FOUND"
});
}
const source = scripts[0].source;
const generatedActor = this.sources.createNonSourceMappedActor(source);
let generatedLocation = new GeneratedLocation(
generatedActor, line, column);
return this.sources.getOriginalLocation(generatedLocation).then(loc => {
// If no map found, return this packet
if (loc.originalLine == null) {
return {
type: "resolveLocation",
error: "MAP_NOT_FOUND"
};
}
loc = loc.toJSON();
return {
from: this.actorID,
url: loc.source.url,
column: loc.column,
line: loc.line
};
});
},
};
/**
* The request types this actor can handle.
*/
TabActor.prototype.requestTypes = {
"attach": TabActor.prototype.onAttach,
"detach": TabActor.prototype.onDetach,
"focus": TabActor.prototype.onFocus,
"reload": TabActor.prototype.onReload,
"navigateTo": TabActor.prototype.onNavigateTo,
"reconfigure": TabActor.prototype.onReconfigure,
"switchToFrame": TabActor.prototype.onSwitchToFrame,
"listFrames": TabActor.prototype.onListFrames,
"listWorkers": TabActor.prototype.onListWorkers,
"resolveLocation": TabActor.prototype.onResolveLocation
};
exports.TabActor = TabActor;
/**
* Creates a tab actor for handling requests to a single browser frame.
* Both <xul:browser> and <iframe mozbrowser> are supported.
* This actor is a shim that connects to a ContentActor in a remote browser process.
* All RDP packets get forwarded using the message manager.
*
* @param connection The main RDP connection.
* @param browser <xul:browser> or <iframe mozbrowser> element to connect to.
*/
function BrowserTabActor(connection, browser) {
this._conn = connection;
this._browser = browser;
this._form = null;
}
BrowserTabActor.prototype = {
connect() {
let onDestroy = () => {
this._form = null;
};
let connect = DebuggerServer.connectToChild(this._conn, this._browser, onDestroy);
return connect.then(form => {
this._form = form;
return this;
});
},
get _tabbrowser() {
if (typeof this._browser.getTabBrowser == "function") {
return this._browser.getTabBrowser();
}
return null;
},
get _mm() {
// Get messageManager from XUL browser (which might be a specialized tunnel for RDM)
// or else fallback to asking the frameLoader itself.
return this._browser.messageManager ||
this._browser.frameLoader.messageManager;
},
update() {
// If the child happens to be crashed/close/detach, it won't have _form set,
// so only request form update if some code is still listening on the other
// side.
if (this._form) {
let deferred = promise.defer();
let onFormUpdate = msg => {
// There may be more than just one childtab.js up and running
if (this._form.actor != msg.json.actor) {
return;
}
this._mm.removeMessageListener("debug:form", onFormUpdate);
this._form = msg.json;
deferred.resolve(this);
};
this._mm.addMessageListener("debug:form", onFormUpdate);
this._mm.sendAsyncMessage("debug:form");
return deferred.promise;
}
return this.connect();
},
/**
* If we don't have a title from the content side because it's a zombie tab, try to find
* it on the chrome side.
*/
get title() {
// On Fennec, we can check the session store data for zombie tabs
if (this._browser.__SS_restore) {
let sessionStore = this._browser.__SS_data;
// Get the last selected entry
let entry = sessionStore.entries[sessionStore.index - 1];
return entry.title;
}
// If contentTitle is empty (e.g. on a not-yet-restored tab), but there is a
// tabbrowser (i.e. desktop Firefox, but not Fennec), we can use the label
// as the title.
if (this._tabbrowser) {
let tab = this._tabbrowser.getTabForBrowser(this._browser);
if (tab) {
return tab.label;
}
}
return "";
},
/**
* If we don't have a url from the content side because it's a zombie tab, try to find
* it on the chrome side.
*/
get url() {
// On Fennec, we can check the session store data for zombie tabs
if (this._browser.__SS_restore) {
let sessionStore = this._browser.__SS_data;
// Get the last selected entry
let entry = sessionStore.entries[sessionStore.index - 1];
return entry.url;
}
return null;
},
form() {
let form = Object.assign({}, this._form);
// In some cases, the title and url fields might be empty. Zombie tabs (not yet
// restored) are a good example. In such cases, try to look up values for these
// fields using other data in the parent process.
if (!form.title) {
form.title = this.title;
}
if (!form.url) {
form.url = this.url;
}
return form;
},
exit() {
this._browser = null;
},
};
exports.BrowserTabActor = BrowserTabActor;
function BrowserAddonList(connection) {
this._connection = connection;
this._actorByAddonId = new Map();
this._onListChanged = null;
}
BrowserAddonList.prototype.getList = function () {
let deferred = promise.defer();
AddonManager.getAllAddons((addons) => {
for (let addon of addons) {
let actor = this._actorByAddonId.get(addon.id);
if (!actor) {
if (addon.isWebExtension) {
actor = new WebExtensionActor(this._connection, addon);
} else {
actor = new BrowserAddonActor(this._connection, addon);
}
this._actorByAddonId.set(addon.id, actor);
}
}
deferred.resolve([...this._actorByAddonId].map(([_, actor]) => actor));
});
return deferred.promise;
};
Object.defineProperty(BrowserAddonList.prototype, "onListChanged", {
enumerable: true,
configurable: true,
get() {
return this._onListChanged;
},
set(v) {
if (v !== null && typeof v != "function") {
throw new Error(
"onListChanged property may only be set to 'null' or a function");
}
this._onListChanged = v;
this._adjustListener();
}
});
BrowserAddonList.prototype.onInstalled = function (addon) {
this._notifyListChanged();
this._adjustListener();
};
BrowserAddonList.prototype.onUninstalled = function (addon) {
this._actorByAddonId.delete(addon.id);
this._notifyListChanged();
this._adjustListener();
};
BrowserAddonList.prototype._notifyListChanged = function () {
if (this._onListChanged) {
this._onListChanged();
}
};
BrowserAddonList.prototype._adjustListener = function () {
if (this._onListChanged) {
// As long as the callback exists, we need to listen for changes
// so we can notify about add-on changes.
AddonManager.addAddonListener(this);
} else if (this._actorByAddonId.size === 0) {
// When the callback does not exist, we only need to keep listening
// if the actor cache will need adjusting when add-ons change.
AddonManager.removeAddonListener(this);
}
};
exports.BrowserAddonList = BrowserAddonList;
/**
* The DebuggerProgressListener object is an nsIWebProgressListener which
* handles onStateChange events for the inspected browser. If the user tries to
* navigate away from a paused page, the listener makes sure that the debuggee
* is resumed before the navigation begins.
*
* @param TabActor aTabActor
* The tab actor associated with this listener.
*/
function DebuggerProgressListener(tabActor) {
this._tabActor = tabActor;
this._onWindowCreated = this.onWindowCreated.bind(this);
this._onWindowHidden = this.onWindowHidden.bind(this);
// Watch for windows destroyed (global observer that will need filtering)
Services.obs.addObserver(this, "inner-window-destroyed", false);
// XXX: for now we maintain the list of windows we know about in this instance
// so that we can discriminate windows we care about when observing
// inner-window-destroyed events. Bug 1016952 would remove the need for this.
this._knownWindowIDs = new Map();
this._watchedDocShells = new WeakSet();
}
DebuggerProgressListener.prototype = {
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference,
Ci.nsISupports,
]),
destroy() {
Services.obs.removeObserver(this, "inner-window-destroyed", false);
this._knownWindowIDs.clear();
this._knownWindowIDs = null;
},
watch(docShell) {
// Add the docshell to the watched set. We're actually adding the window,
// because docShell objects are not wrappercached and would be rejected
// by the WeakSet.
let docShellWindow = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
this._watchedDocShells.add(docShellWindow);
let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this,
Ci.nsIWebProgress.NOTIFY_STATUS |
Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
let handler = getDocShellChromeEventHandler(docShell);
handler.addEventListener("DOMWindowCreated", this._onWindowCreated, true);
handler.addEventListener("pageshow", this._onWindowCreated, true);
handler.addEventListener("pagehide", this._onWindowHidden, true);
// Dispatch the _windowReady event on the tabActor for pre-existing windows
for (let win of this._getWindowsInDocShell(docShell)) {
this._tabActor._windowReady(win);
this._knownWindowIDs.set(getWindowID(win), win);
}
},
unwatch(docShell) {
let docShellWindow = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
if (!this._watchedDocShells.has(docShellWindow)) {
return;
}
let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
// During process shutdown, the docshell may already be cleaned up and throw
try {
webProgress.removeProgressListener(this);
} catch (e) {
// ignore
}
let handler = getDocShellChromeEventHandler(docShell);
handler.removeEventListener("DOMWindowCreated",
this._onWindowCreated, true);
handler.removeEventListener("pageshow", this._onWindowCreated, true);
handler.removeEventListener("pagehide", this._onWindowHidden, true);
for (let win of this._getWindowsInDocShell(docShell)) {
this._knownWindowIDs.delete(getWindowID(win));
}
},
_getWindowsInDocShell(docShell) {
return getChildDocShells(docShell).map(d => {
return d.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
});
},
onWindowCreated: DevToolsUtils.makeInfallible(function (evt) {
if (!this._tabActor.attached) {
return;
}
// pageshow events for non-persisted pages have already been handled by a
// prior DOMWindowCreated event. For persisted pages, act as if the window
// had just been created since it's been unfrozen from bfcache.
if (evt.type == "pageshow" && !evt.persisted) {
return;
}
let window = evt.target.defaultView;
this._tabActor._windowReady(window);
if (evt.type !== "pageshow") {
this._knownWindowIDs.set(getWindowID(window), window);
}
}, "DebuggerProgressListener.prototype.onWindowCreated"),
onWindowHidden: DevToolsUtils.makeInfallible(function (evt) {
if (!this._tabActor.attached) {
return;
}
// Only act as if the window has been destroyed if the 'pagehide' event
// was sent for a persisted window (persisted is set when the page is put
// and frozen in the bfcache). If the page isn't persisted, the observer's
// inner-window-destroyed event will handle it.
if (!evt.persisted) {
return;
}
let window = evt.target.defaultView;
this._tabActor._windowDestroyed(window, null, true);
}, "DebuggerProgressListener.prototype.onWindowHidden"),
observe: DevToolsUtils.makeInfallible(function (subject, topic) {
if (!this._tabActor.attached) {
return;
}
// Because this observer will be called for all inner-window-destroyed in
// the application, we need to filter out events for windows we are not
// watching
let innerID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
let window = this._knownWindowIDs.get(innerID);
if (window) {
this._knownWindowIDs.delete(innerID);
this._tabActor._windowDestroyed(window, innerID);
}
}, "DebuggerProgressListener.prototype.observe"),
onStateChange:
DevToolsUtils.makeInfallible(function (progress, request, flag, status) {
if (!this._tabActor.attached) {
return;
}
let isStart = flag & Ci.nsIWebProgressListener.STATE_START;
let isStop = flag & Ci.nsIWebProgressListener.STATE_STOP;
let isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
let isWindow = flag & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
// Catch any iframe location change
if (isDocument && isStop) {
// Watch document stop to ensure having the new iframe url.
progress.QueryInterface(Ci.nsIDocShell);
this._tabActor._notifyDocShellsUpdate([progress]);
}
let window = progress.DOMWindow;
if (isDocument && isStart) {
// One of the earliest events that tells us a new URI
// is being loaded in this window.
let newURI = request instanceof Ci.nsIChannel ? request.URI.spec : null;
this._tabActor._willNavigate(window, newURI, request);
}
if (isWindow && isStop) {
// Don't dispatch "navigate" event just yet when there is a redirect to
// about:neterror page.
// Navigating to about:neterror will make `status` be something else than NS_OK.
// But for some error like NS_BINDING_ABORTED we don't want to emit any `navigate`
// event as the page load has been cancelled and the related page document is going
// to be a dead wrapper.
if (request.status != Cr.NS_OK && request.status != Cr.NS_BINDING_ABORTED) {
// Instead, listen for DOMContentLoaded as about:neterror is loaded
// with LOAD_BACKGROUND flags and never dispatches load event.
// That may be the same reason why there is no onStateChange event
// for about:neterror loads.
let handler = getDocShellChromeEventHandler(progress);
let onLoad = evt => {
// Ignore events from iframes
if (evt.target == window.document) {
handler.removeEventListener("DOMContentLoaded", onLoad, true);
this._tabActor._navigate(window);
}
};
handler.addEventListener("DOMContentLoaded", onLoad, true);
} else {
// Somewhat equivalent of load event.
// (window.document.readyState == complete)
this._tabActor._navigate(window);
}
}
}, "DebuggerProgressListener.prototype.onStateChange")
};
exports.register = function (handle) {
handle.setRootActor(createRootActor);
};
exports.unregister = function (handle) {
handle.setRootActor(null);
};