summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/targets/browsing-context.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/targets/browsing-context.js')
-rw-r--r--devtools/server/actors/targets/browsing-context.js1899
1 files changed, 1899 insertions, 0 deletions
diff --git a/devtools/server/actors/targets/browsing-context.js b/devtools/server/actors/targets/browsing-context.js
new file mode 100644
index 0000000000..d7debfcb2f
--- /dev/null
+++ b/devtools/server/actors/targets/browsing-context.js
@@ -0,0 +1,1899 @@
+/* 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 */
+
+// protocol.js uses objects as exceptions in order to define
+// error packets.
+/* eslint-disable no-throw-literal */
+
+/*
+ * BrowsingContextTargetActor is an abstract class used by target actors that hold
+ * documents, such as frames, chrome windows, etc.
+ *
+ * This class is extended by FrameTargetActor, ParentProcessTargetActor, and
+ * ChromeWindowTargetActor.
+ *
+ * See devtools/docs/backend/actor-hierarchy.md for more details.
+ *
+ * For performance matters, this file should only be loaded in the targeted context's
+ * process. For example, it shouldn't be evaluated in the parent process until we try to
+ * debug a document living in the parent process.
+ */
+
+var { Ci, Cu, Cr, Cc } = require("chrome");
+var Services = require("Services");
+const ChromeUtils = require("ChromeUtils");
+var { ActorRegistry } = require("devtools/server/actors/utils/actor-registry");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var { assert } = DevToolsUtils;
+var {
+ SourcesManager,
+} = require("devtools/server/actors/utils/sources-manager");
+var makeDebugger = require("devtools/server/actors/utils/make-debugger");
+const InspectorUtils = require("InspectorUtils");
+const Targets = require("devtools/server/actors/targets/index");
+const { TargetActorRegistry } = ChromeUtils.import(
+ "resource://devtools/server/actors/targets/target-actor-registry.jsm"
+);
+
+const EXTENSION_CONTENT_JSM = "resource://gre/modules/ExtensionContent.jsm";
+
+const { Actor, Pool } = require("devtools/shared/protocol");
+const {
+ LazyPool,
+ createExtraActors,
+} = require("devtools/shared/protocol/lazy-pool");
+const {
+ browsingContextTargetSpec,
+} = require("devtools/shared/specs/targets/browsing-context");
+const Resources = require("devtools/server/actors/resources/index");
+const TargetActorMixin = require("devtools/server/actors/targets/target-actor-mixin");
+
+loader.lazyRequireGetter(
+ this,
+ ["ThreadActor", "unwrapDebuggerObjectGlobal"],
+ "devtools/server/actors/thread",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "WorkerDescriptorActorList",
+ "devtools/server/actors/worker/worker-descriptor-actor-list",
+ true
+);
+loader.lazyImporter(this, "ExtensionContent", EXTENSION_CONTENT_JSM);
+
+loader.lazyRequireGetter(
+ this,
+ ["StyleSheetActor", "getSheetText"],
+ "devtools/server/actors/style-sheet",
+ true
+);
+
+function getWindowID(window) {
+ return window.windowGlobalChild.innerWindowId;
+}
+
+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.domWindow;
+ } catch (e) {
+ // ignore
+ }
+ }
+ return handler;
+}
+
+function getChildDocShells(parentDocShell) {
+ const allDocShells = parentDocShell.getAllDocShellsInSubtree(
+ Ci.nsIDocShellTreeItem.typeAll,
+ Ci.nsIDocShell.ENUMERATE_FORWARDS
+ );
+
+ const docShells = [];
+ for (const docShell of allDocShells) {
+ docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ docShells.push(docShell);
+ }
+ return docShells;
+}
+
+exports.getChildDocShells = getChildDocShells;
+
+/**
+ * Browser-specific actors.
+ */
+
+function getInnerId(window) {
+ return window.windowGlobalChild.innerWindowId;
+}
+
+const browsingContextTargetPrototype = {
+ /**
+ * BrowsingContextTargetActor is an abstract class used by target actors that
+ * hold documents, such as frames, chrome windows, etc. The term "browsing
+ * context" is defined in the HTML spec as "an environment in which `Document`
+ * objects are presented to the user". In Gecko, this means a browsing context
+ * is a `docShell`.
+ *
+ * The main goal of this class is to expose the target-scoped actors being registered
+ * via `ActorRegistry.registerModule` and manage their lifetimes. In addition, this
+ * class also tracks the lifetime of the targeted browsing context.
+ *
+ * ### 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 actor, and its child target-scoped actors
+ * to an iframe or back to its original document.
+ *
+ * Most properties (like `chromeEventHandler` or `docShells`) are meant to be
+ * used by the various child target actors.
+ *
+ * ### RDP events:
+ *
+ * - `tabNavigated`:
+ * Sent when the browsing context 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 actor 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 actor'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 actor to a specific frame:
+ * { selected: #id }
+ *
+ * ### Internal, non-rdp events:
+ *
+ * Various events are also dispatched on the actor itself without being sent to
+ * the client. They all relate to the documents tracked by this target actor
+ * (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 in various 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.
+ * * When `swapFrameLoaders` is used, such as with moving browsing contexts
+ * between windows or toggling Responsive Design Mode.
+ * - 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 browsing context 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 actor's 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 actor to a given iframe:
+ * - will-navigate
+ * - window-destroyed
+ * - changed-toplevel-document
+ * - window-ready
+ * - navigate
+ *
+ * This class is subclassed by FrameTargetActor and others.
+ * Subclasses are expected to implement a getter for the docShell property.
+ *
+ * @param connection DevToolsServerConnection
+ * The conection to the client.
+ * @param docShell nsIDocShell
+ * The |docShell| for the debugged frame.
+ * @param options Object
+ * Object with following attributes:
+ * - followWindowGlobalLifeCycle Boolean
+ * If true, the target actor will only inspect the current WindowGlobal (and its children windows).
+ * But won't inspect next document loaded in the same BrowsingContext.
+ * The actor will behave more like a WindowGlobalTarget rather than a BrowsingContextTarget.
+ * We may eventually switch everything to this, i.e. uses only WindowGlobalTarget.
+ * But for now, we restrict this behavior to remoted iframes.
+ * - doNotFireFrameUpdates Boolean
+ * If true, omit emitting `frameUpdate` events. This is only useful
+ * for the top level target, in order to populate the toolbox iframe selector dropdown.
+ * But we can avoid sending these RDP messages for any additional remote target.
+ */
+ initialize: function(connection, docShell, options = {}) {
+ Actor.prototype.initialize.call(this, connection);
+
+ if (!docShell) {
+ throw new Error(
+ "A docShell should be provided as constructor argument of BrowsingContextTargetActor"
+ );
+ }
+ this.docShell = docShell;
+
+ this.followWindowGlobalLifeCycle = options.followWindowGlobalLifeCycle;
+ this.doNotFireFrameUpdates = options.doNotFireFrameUpdates;
+
+ // A map of actor names to actor instances provided by extensions.
+ this._extraActors = {};
+ this._exited = false;
+ this._sourcesManager = 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 ParentProcessTargetActor to list all frames in the Browser Toolbox
+ this.watchNewDocShells = false;
+
+ this.traits = {
+ reconfigure: true,
+ // Supports frame listing via `listFrames` request and `frameUpdate` events
+ // as well as frame switching via `switchToFrame` request
+ frames: true,
+ // Supports the logInPage request.
+ logInPage: true,
+ // Supports watchpoints in the server. We need to keep this trait because target
+ // actors that don't extend BrowsingContextTargetActor (Worker, ContentProcess, …)
+ // might not support watchpoints.
+ watchpoints: true,
+ // Supports back and forward navigation
+ navigation: true,
+ };
+
+ this._workerDescriptorActorList = null;
+ this._workerDescriptorActorPool = null;
+ this._onWorkerDescriptorActorListChanged = this._onWorkerDescriptorActorListChanged.bind(
+ this
+ );
+
+ TargetActorRegistry.registerTargetActor(this);
+ },
+
+ 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 SourcesManager 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;
+ },
+
+ /*
+ * Return a Debugger instance or create one if there is none yet
+ */
+ get dbg() {
+ if (!this._dbg) {
+ this._dbg = this.makeDebugger();
+ }
+ return this._dbg;
+ },
+
+ /**
+ * Try to locate the console actor if it exists.
+ */
+ get _consoleActor() {
+ if (this.exited || this.isDestroyed()) {
+ return null;
+ }
+ const form = this.form();
+ return this.conn._getOrCreateActor(form.consoleActor);
+ },
+
+ _targetScopedActorPool: null,
+
+ /**
+ * An object on which listen for DOMWindowCreated and pageshow events.
+ */
+ get chromeEventHandler() {
+ return getDocShellChromeEventHandler(this.docShell);
+ },
+
+ /**
+ * Getter for the nsIMessageManager associated to the browsing context.
+ */
+ get messageManager() {
+ try {
+ return this.docShell.messageManager;
+ } catch (e) {
+ // In some cases we can't get a docshell. We just have no message manager
+ // then,
+ return null;
+ }
+ },
+
+ /**
+ * Getter for the list of all `docShell`s in the browsing context.
+ * @return {Array}
+ */
+ get docShells() {
+ return getChildDocShells(this.docShell);
+ },
+
+ /**
+ * Getter for the browsing context's current DOM window.
+ */
+ get window() {
+ return this.docShell && this.docShell.domWindow;
+ },
+
+ get outerWindowID() {
+ if (this.docShell) {
+ return this.docShell.outerWindowID;
+ }
+ return null;
+ },
+
+ get browsingContext() {
+ return this.docShell?.browsingContext;
+ },
+
+ get browsingContextID() {
+ return this.browsingContext?.id;
+ },
+
+ get browserId() {
+ return this.browsingContext?.browserId;
+ },
+
+ /**
+ * Getter for the WebExtensions ContentScript globals related to the
+ * browsing context's current DOM window.
+ */
+ get webextensionsContentScriptGlobals() {
+ // Only retrieve the content scripts globals if the ExtensionContent JSM module
+ // has been already loaded (which is true if the WebExtensions internals have already
+ // been loaded in the same content process).
+ if (Cu.isModuleLoaded(EXTENSION_CONTENT_JSM)) {
+ return ExtensionContent.getContentScriptGlobals(this.window);
+ }
+
+ return [];
+ },
+
+ /**
+ * Getter for the list of all content DOM windows in the browsing context.
+ * @return {Array}
+ */
+ get windows() {
+ return this.docShells.map(docShell => {
+ return docShell.domWindow;
+ });
+ },
+
+ /**
+ * Getter for the original docShell this actor 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.docShell;
+ },
+
+ /**
+ * Getter for the original window this actor 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 target.
+ */
+ get webNavigation() {
+ return this.docShell.QueryInterface(Ci.nsIWebNavigation);
+ },
+
+ /**
+ * Getter for the browsing context's document.
+ */
+ get contentDocument() {
+ return this.webNavigation.document;
+ },
+
+ /**
+ * Getter for the browsing context's title.
+ */
+ get title() {
+ return this.contentDocument.contentTitle;
+ },
+
+ /**
+ * Getter for the browsing context's 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 sourcesManager() {
+ if (!this._sourcesManager) {
+ this._sourcesManager = new SourcesManager(
+ this.threadActor,
+ this._allowSource
+ );
+ }
+ return this._sourcesManager;
+ },
+
+ _createExtraActors() {
+ // Always use the same Pool, so existing actor instances
+ // (created in createExtraActors) are not lost.
+ if (!this._targetScopedActorPool) {
+ this._targetScopedActorPool = new LazyPool(this.conn);
+ }
+
+ // Walk over target-scoped actor factories and make sure they are all
+ // instantiated and added into the Pool.
+ return createExtraActors(
+ ActorRegistry.targetScopedActorFactories,
+ this._targetScopedActorPool,
+ this
+ );
+ },
+
+ form() {
+ assert(!this.exited, "form() shouldn't be called on exited browser actor.");
+ assert(this.actorID, "Actor should have an actorID.");
+
+ const response = {
+ actor: this.actorID,
+ browsingContextID: this.browsingContextID,
+ traits: {
+ // @backward-compat { version 64 } Exposes a new trait to help identify
+ // BrowsingContextActor's inherited actors from the client side.
+ isBrowsingContext: true,
+ },
+ };
+
+ // We may try to access window while the document is closing, then accessing window
+ // throws.
+ if (!this.docShell.isBeingDestroyed()) {
+ response.title = this.title;
+ response.url = this.url;
+ response.outerWindowID = this.outerWindowID;
+ }
+
+ const actors = this._createExtraActors();
+ Object.assign(response, actors);
+
+ // The thread actor is the only actor manually created by the target actor.
+ // It is not registered in targetScopedActorFactoriesand therefore needs
+ // to be added here manually.
+ if (this.threadActor) {
+ Object.assign(response, {
+ threadActor: this.threadActor.actorID,
+ });
+ }
+
+ return response;
+ },
+
+ /**
+ * Called when the actor is removed from the connection.
+ */
+ destroy() {
+ this.exit();
+ Actor.prototype.destroy.call(this);
+ TargetActorRegistry.unregisterTargetActor(this);
+ Resources.unwatchAllTargetResources(this);
+ },
+
+ /**
+ * Called by the root actor when the underlying browsing context is closed.
+ */
+ exit() {
+ if (this.exited) {
+ return;
+ }
+
+ // Tell the thread actor that the browsing context is closed, so that it may terminate
+ // instead of resuming the debuggee script.
+ if (this._attached) {
+ // TODO: Bug 997119: Remove this coupling with thread actor
+ this.threadActor._parentClosed = true;
+ }
+
+ this._detach();
+
+ this.docShell = null;
+
+ this._extraActors = null;
+
+ this._exited = true;
+ },
+
+ /**
+ * Return true if the given global is associated with this browsing context and should
+ * be added as a debuggee, false otherwise.
+ */
+ _shouldAddNewGlobalAsDebuggee(wrappedGlobal) {
+ // Otherwise, check if it is a WebExtension content script sandbox
+ const 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?.["inner-window-id"] && metadata["inner-window-id"] == id) {
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Does the actual work of attaching to a browsing context.
+ */
+ _attach() {
+ if (this._attached) {
+ return;
+ }
+
+ // Create a pool for context-lifetime actors.
+ this._createThreadActor();
+
+ this._progressListener = new DebuggerProgressListener(this);
+
+ // Save references to the original document we attached to
+ this._originalWindow = this.window;
+
+ this._docShellsObserved = false;
+
+ // Ensure replying to attach() request first
+ // before notifying about new docshells.
+ DevToolsUtils.executeSoon(() => this._watchDocshells());
+
+ this._attached = true;
+ },
+
+ _watchDocshells() {
+ // If for some unexpected reason, the actor is immediately destroyed,
+ // avoid registering leaking observer listener.
+ if (this.exited) {
+ return;
+ }
+
+ // In child processes, we watch all docshells living in the process.
+ Services.obs.addObserver(this, "webnavigation-create");
+ Services.obs.addObserver(this, "webnavigation-destroy");
+ this._docShellsObserved = true;
+
+ // We watch for all child docshells under the current document,
+ this._progressListener.watch(this.docShell);
+
+ // And list all already existing ones.
+ this._updateChildDocShells();
+ },
+
+ _unwatchDocshells() {
+ if (this._progressListener) {
+ this._progressListener.destroy();
+ this._progressListener = null;
+ this._originalWindow = null;
+ }
+
+ // Removes the observers being set in _watchDocshells, but only
+ // if _watchDocshells has been called. The target actor may be immediately destroyed
+ // and doesn't have time to register them.
+ // (Calling removeObserver without having called addObserver throws)
+ if (this._docShellsObserved) {
+ Services.obs.removeObserver(this, "webnavigation-create");
+ Services.obs.removeObserver(this, "webnavigation-destroy");
+ this._docShellsObserved = true;
+ }
+ },
+
+ _unwatchDocShell(docShell) {
+ if (this._progressListener) {
+ this._progressListener.unwatch(docShell);
+ }
+ },
+
+ switchToFrame(request) {
+ const windowId = request.windowId;
+ let win;
+
+ try {
+ win = Services.wm.getOuterWindowWithId(windowId);
+ } catch (e) {
+ // ignore
+ }
+ if (!win) {
+ throw {
+ 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 {};
+ },
+
+ listFrames(request) {
+ const windows = this._docShellsToWindows(this.docShells);
+ return { frames: windows };
+ },
+
+ ensureWorkerDescriptorActorList() {
+ if (this._workerDescriptorActorList === null) {
+ this._workerDescriptorActorList = new WorkerDescriptorActorList(
+ this.conn,
+ {
+ type: Ci.nsIWorkerDebugger.TYPE_DEDICATED,
+ window: this.window,
+ }
+ );
+ }
+ return this._workerDescriptorActorList;
+ },
+
+ pauseWorkersUntilAttach(shouldPause) {
+ this.ensureWorkerDescriptorActorList().workerPauser.setPauseMatching(
+ shouldPause
+ );
+ },
+
+ listWorkers(request) {
+ if (!this.attached) {
+ throw {
+ error: "wrongState",
+ };
+ }
+
+ return this.ensureWorkerDescriptorActorList()
+ .getList()
+ .then(actors => {
+ const pool = new Pool(this.conn, "worker-targets");
+ for (const actor of actors) {
+ pool.manage(actor);
+ }
+
+ // Do not destroy the pool before transfering ownership to the newly created
+ // pool, so that we do not accidently destroy actors that are still in use.
+ if (this._workerDescriptorActorPool) {
+ this._workerDescriptorActorPool.destroy();
+ }
+
+ this._workerDescriptorActorPool = pool;
+ this._workerDescriptorActorList.onListChanged = this._onWorkerDescriptorActorListChanged;
+
+ return {
+ workers: actors,
+ };
+ });
+ },
+
+ logInPage(request) {
+ const { text, category, flags } = request;
+ const scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
+ const scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);
+ scriptError.initWithWindowID(
+ text,
+ null,
+ null,
+ 0,
+ 0,
+ flags,
+ category,
+ getInnerId(this.window)
+ );
+ Services.console.logMessage(scriptError);
+ return {};
+ },
+
+ _onWorkerDescriptorActorListChanged() {
+ this._workerDescriptorActorList.onListChanged = null;
+ this.emit("workerListChanged");
+ },
+
+ observe(subject, topic, data) {
+ // Ignore any event that comes before/after the actor is attached.
+ // That typically happens during Firefox shutdown.
+ if (!this.attached) {
+ return;
+ }
+
+ subject.QueryInterface(Ci.nsIDocShell);
+
+ if (topic == "webnavigation-create") {
+ 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
+ // BrowserChild, this event is from within this call:
+ // https://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l912
+ // whereas the chromeEventHandler (and most likely other stuff) is set
+ // later:
+ // https://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.watchNewDocShells) {
+ this._progressListener.watch(docShell);
+ }
+ this._notifyDocShellsUpdate([docShell]);
+ });
+ },
+
+ _onDocShellDestroy(docShell) {
+ // Stop watching this docshell (the unwatch() method will check if we
+ // started watching it before).
+ this._unwatchDocShell(docShell);
+
+ const webProgress = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ this._notifyDocShellDestroy(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
+ const rootDocShells = this.docShells.filter(d => {
+ return d != this.docShell && this._isRootDocShell(d);
+ });
+ if (rootDocShells.length > 0) {
+ const 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 actor to unregister all listeners and prevent any
+ // exception.
+ this.exit();
+ }
+ return;
+ }
+
+ // If the currently targeted browsing 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);
+ }
+ },
+
+ _isRootDocShell(docShell) {
+ // Should report as root docshell:
+ // - New top level window's docshells, when using ParentProcessTargetActor 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;
+ },
+
+ _docShellToWindow(docShell) {
+ const webProgress = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ const window = webProgress.DOMWindow;
+ const id = docShell.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.
+ // Furthermore, ignore setting parentID when parent window is same as
+ // current window in order to deal with front end. e.g. toolbox will be fall
+ // into infinite loop due to recursive search with by using parent id.
+ if (
+ window.parent &&
+ window.parent != window &&
+ window != this._originalWindow
+ ) {
+ parentID = window.parent.docShell.outerWindowID;
+ }
+
+ return {
+ id,
+ parentID,
+ url: window.location.href,
+ title: window.document.title,
+ };
+ },
+
+ // Convert docShell list to windows objects list being sent to the client
+ _docShellsToWindows(docshells) {
+ return docshells.map(docShell => this._docShellToWindow(docShell));
+ },
+
+ _notifyDocShellsUpdate(docshells) {
+ // Only top level target uses frameUpdate in order to update the iframe dropdown.
+ // This may eventually be replaced by Target listening and target switching.
+ if (this.doNotFireFrameUpdates) {
+ return;
+ }
+
+ const windows = this._docShellsToWindows(docshells);
+
+ // Do not send the `frameUpdate` event if the windows array is empty.
+ if (windows.length == 0) {
+ return;
+ }
+
+ this.emit("frameUpdate", {
+ frames: windows,
+ });
+ },
+
+ _updateChildDocShells() {
+ this._notifyDocShellsUpdate(this.docShells);
+ },
+
+ _notifyDocShellDestroy(webProgress) {
+ // Only top level target uses frameUpdate in order to update the iframe dropdown.
+ // This may eventually be replaced by Target listening and target switching.
+ if (this.doNotFireFrameUpdates) {
+ return;
+ }
+
+ webProgress = webProgress.QueryInterface(Ci.nsIWebProgress);
+ const id = webProgress.DOMWindow.docShell.outerWindowID;
+ this.emit("frameUpdate", {
+ frames: [
+ {
+ id,
+ destroy: true,
+ },
+ ],
+ });
+ },
+
+ _notifyDocShellDestroyAll() {
+ // Only top level target uses frameUpdate in order to update the iframe dropdown.
+ // This may eventually be replaced by Target listening and target switching.
+ if (this.doNotFireFrameUpdates) {
+ return;
+ }
+
+ this.emit("frameUpdate", {
+ destroyAll: true,
+ });
+ },
+
+ /**
+ * Creates and manages the thread actor as part of the Browsing Context Target pool.
+ * This sets up the content window for being debugged
+ */
+ _createThreadActor() {
+ this.threadActor = new ThreadActor(this, this.window);
+ this.manage(this.threadActor);
+ },
+
+ /**
+ * Exits the current thread actor and removes it from the Browsing Context Target pool.
+ * The content window is no longer being debugged after this call.
+ */
+ _destroyThreadActor() {
+ this.threadActor.destroy();
+ this.threadActor = null;
+
+ if (this._sourcesManager) {
+ this._sourcesManager.destroy();
+ this._sourcesManager = null;
+ }
+ },
+
+ /**
+ * Does the actual work of detaching from a browsing context.
+ *
+ * @returns false if the actor 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._unwatchDocShell(this.docShell);
+ this._restoreDocumentSettings();
+ }
+ this._unwatchDocshells();
+
+ this._destroyThreadActor();
+
+ // Shut down actors that belong to this target's pool.
+ this._styleSheetActors.clear();
+ if (this._targetScopedActorPool) {
+ this._targetScopedActorPool.destroy();
+ this._targetScopedActorPool = null;
+ }
+
+ // Make sure that no more workerListChanged notifications are sent.
+ if (this._workerDescriptorActorList !== null) {
+ this._workerDescriptorActorList.destroy();
+ this._workerDescriptorActorList = null;
+ }
+
+ if (this._workerDescriptorActorPool !== null) {
+ this._workerDescriptorActorPool.destroy();
+ this._workerDescriptorActorPool = null;
+ }
+
+ if (this._dbg) {
+ this._dbg.disable();
+ this._dbg = null;
+ }
+
+ this._attached = false;
+
+ // When the target actor acts as a WindowGlobalTarget, the actor will be destroyed
+ // without having to send an RDP event. The parent process will receive a window-global-destroyed
+ // and report the target actor as destroyed via the Watcher actor.
+ if (this.followWindowGlobalLifeCycle) {
+ return true;
+ }
+
+ this.emit("tabDetached");
+
+ return true;
+ },
+
+ // Protocol Request Handlers
+
+ attach(request) {
+ if (this.exited) {
+ throw {
+ error: "exited",
+ };
+ }
+
+ this._attach();
+
+ return {
+ threadActor: this.threadActor.actorID,
+ cacheDisabled: this._getCacheDisabled(),
+ javascriptEnabled: this._getJavascriptEnabled(),
+ traits: this.traits,
+ };
+ },
+
+ detach(request) {
+ if (!this._detach()) {
+ throw {
+ error: "wrongState",
+ };
+ }
+
+ return {};
+ },
+
+ /**
+ * Bring the browsing context's window to front.
+ */
+ focus() {
+ if (this.window) {
+ this.window.focus();
+ }
+ return {};
+ },
+
+ goForward() {
+ // Wait a tick so that the response packet can be dispatched before the
+ // subsequent navigation event packet.
+ Services.tm.dispatchToMainThread(
+ 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.goForward();
+ }, "BrowsingContextTargetActor.prototype.goForward's delayed body")
+ );
+
+ return {};
+ },
+
+ goBack() {
+ // Wait a tick so that the response packet can be dispatched before the
+ // subsequent navigation event packet.
+ Services.tm.dispatchToMainThread(
+ 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.goBack();
+ }, "BrowsingContextTargetActor.prototype.goBack's delayed body")
+ );
+
+ return {};
+ },
+
+ /**
+ * Reload the page in this browsing context.
+ */
+ reload(request) {
+ const force = request?.options?.force;
+ // Wait a tick so that the response packet can be dispatched before the
+ // subsequent navigation event packet.
+ Services.tm.dispatchToMainThread(
+ 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
+ );
+ }, "BrowsingContextTargetActor.prototype.reload's delayed body")
+ );
+ return {};
+ },
+
+ /**
+ * Navigate this browsing context to a new location
+ */
+ navigateTo(request) {
+ // Wait a tick so that the response packet can be dispatched before the
+ // subsequent navigation event packet.
+ Services.tm.dispatchToMainThread(
+ DevToolsUtils.makeInfallible(() => {
+ this.window.location = request.url;
+ }, "BrowsingContextTargetActor.prototype.navigateTo's delayed body:" + request.url)
+ );
+ return {};
+ },
+
+ /**
+ * Reconfigure options.
+ */
+ reconfigure(request) {
+ const options = request.options || {};
+
+ if (!this.docShell) {
+ // The browsing context is already closed.
+ return {};
+ }
+ this._toggleDevToolsSettings(options);
+
+ return {};
+ },
+
+ /**
+ * Ensure that CSS error reporting is enabled.
+ */
+ async ensureCSSErrorReportingEnabled(request) {
+ const promises = [];
+ for (const docShell of this.docShells) {
+ if (docShell.cssErrorReportingEnabled) {
+ continue;
+ }
+ try {
+ docShell.cssErrorReportingEnabled = true;
+ } catch (e) {
+ continue;
+ }
+ // Ensure docShell.document is available.
+ docShell.QueryInterface(Ci.nsIWebNavigation);
+ // We don't really want to reparse UA sheets and such, but want to do
+ // Shadow DOM / XBL.
+ const sheets = InspectorUtils.getAllStyleSheets(
+ docShell.document,
+ /* documentOnly = */ true
+ );
+ for (const sheet of sheets) {
+ if (InspectorUtils.hasRulesModifiedByCSSOM(sheet)) {
+ continue;
+ }
+ // Reparse the sheet so that we see the existing errors.
+ const onStyleSheetParsed = getSheetText(sheet)
+ .then(text => {
+ InspectorUtils.parseStyleSheet(sheet, text, /* aUpdate = */ false);
+ })
+ .catch(e => console.error("Error while parsing stylesheet"));
+ promises.push(onStyleSheetParsed);
+ }
+ }
+ await Promise.all(promises);
+ 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.paintFlashing !== "undefined" &&
+ options.PaintFlashing !== this._getPaintFlashing()
+ ) {
+ this._setPaintFlashingEnabled(options.paintFlashing);
+ }
+ if (
+ typeof options.serviceWorkersTestingEnabled !== "undefined" &&
+ options.serviceWorkersTestingEnabled !==
+ this._getServiceWorkersTestingEnabled()
+ ) {
+ this._setServiceWorkersTestingEnabled(
+ options.serviceWorkersTestingEnabled
+ );
+ }
+ if (typeof options.restoreFocus == "boolean") {
+ this._restoreFocus = options.restoreFocus;
+ }
+
+ // Reload if:
+ // - there's an explicit `performReload` flag and it's true
+ // - there's no `performReload` flag, but it makes sense to do so
+ const hasExplicitReloadFlag = "performReload" in options;
+ if (
+ (hasExplicitReloadFlag && options.performReload) ||
+ (!hasExplicitReloadFlag && reload)
+ ) {
+ this.reload();
+ }
+ },
+
+ /**
+ * Opposite of the _toggleDevToolsSettings method, that reset document state
+ * when closing the toolbox.
+ */
+ _restoreDocumentSettings() {
+ this._restoreJavascript();
+ this._setCacheDisabled(false);
+ this._setServiceWorkersTestingEnabled(false);
+ this._setPaintFlashingEnabled(false);
+
+ if (this._restoreFocus && this.browsingContext?.isActive) {
+ this.window.focus();
+ }
+ },
+
+ /**
+ * Disable or enable the cache via docShell.
+ */
+ _setCacheDisabled(disabled) {
+ const enable = Ci.nsIRequest.LOAD_NORMAL;
+ const disable = Ci.nsIRequest.LOAD_BYPASS_CACHE;
+
+ 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 browsing context is already closed.
+ return null;
+ }
+
+ return this.docShell.allowJavascript;
+ },
+
+ /**
+ * Disable or enable the service workers testing features.
+ */
+ _setServiceWorkersTestingEnabled(enabled) {
+ const windowUtils = this.window.windowUtils;
+ windowUtils.serviceWorkersTestingEnabled = enabled;
+ },
+
+ /**
+ * Disable or enable the paint flashing on the target.
+ */
+ _setPaintFlashingEnabled(enabled) {
+ const windowUtils = this.window.windowUtils;
+ windowUtils.paintFlashing = enabled;
+ },
+
+ /**
+ * Return cache allowed status.
+ */
+ _getCacheDisabled() {
+ if (!this.docShell) {
+ // The browsing context is already closed.
+ return null;
+ }
+
+ const disable = Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ return this.docShell.defaultLoadFlags === disable;
+ },
+
+ /**
+ * Return paint flashing status.
+ */
+ _getPaintFlashing() {
+ if (!this.docShell) {
+ // The browsing context is already closed.
+ return null;
+ }
+
+ return this.window.windowUtils.paintFlashing;
+ },
+
+ /**
+ * Return service workers testing allowed status.
+ */
+ _getServiceWorkersTestingEnabled() {
+ if (!this.docShell) {
+ // The browsing context is already closed.
+ return null;
+ }
+
+ const windowUtils = this.window.windowUtils;
+ return windowUtils.serviceWorkersTestingEnabled;
+ },
+
+ _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(() => {
+ // No need to do anything more if the actor is not attached anymore
+ // e.g. the client has been closed and the actors destroyed in the meantime.
+ if (!this.attached) {
+ return;
+ }
+
+ // Then fake window-ready and navigate on the given document
+ this._windowReady(window, { isFrameSwitching: true });
+ DevToolsUtils.executeSoon(() => {
+ this._navigate(window, true);
+ });
+ });
+ },
+
+ _setWindow(window) {
+ // Here is the very important call where we switch the currently targeted
+ // browsing context (it will indirectly update this.window and many other
+ // attributes defined from docShell).
+ this.docShell = window.docShell;
+ this.emit("changed-toplevel-document");
+ this.emit("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, isBFCache } = {}) {
+ const 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();
+ }
+
+ this.emit("window-ready", {
+ window: window,
+ isTopLevel: isTopLevel,
+ isBFCache,
+ id: getWindowID(window),
+ });
+ },
+
+ _windowDestroyed(window, id = null, isFrozen = false) {
+ this.emit("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 browsing 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.
+ this.emit("will-navigate", {
+ window: window,
+ isTopLevel: isTopLevel,
+ newURI: newURI,
+ request: request,
+ });
+
+ // We don't do anything for inner frames here.
+ // (we will only update thread actor on window-ready)
+ if (!isTopLevel) {
+ return;
+ }
+
+ // When the actor acts as a WindowGlobalTarget, will-navigate won't fired.
+ // Instead we will receive a new top level target with isTargetSwitching=true.
+ if (!this.followWindowGlobalLifeCycle) {
+ this.emit("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 browsing context.
+ */
+ _navigate(window, isFrameSwitching = false) {
+ const 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'.
+ this.emit("navigate", {
+ window: window,
+ isTopLevel: isTopLevel,
+ });
+
+ // We don't do anything for inner frames here.
+ // (we will only update thread actor on window-ready)
+ if (!isTopLevel) {
+ return;
+ }
+
+ // We may still significate when the document is done loading, via navigate.
+ // But as we no longer fire the "will-navigate", may be it is better to find
+ // other ways to get to our means.
+ // Listening to "navigate" is misleading as the document may already be loaded
+ // if we just opened the DevTools. So it is better to use "watch" pattern
+ // and instead have the actor either emit immediately resources as they are
+ // already available, or later on as the load progresses.
+ if (this.followWindowGlobalLifeCycle) {
+ return;
+ }
+
+ this.emit("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.
+ const 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) {
+ assert(!this.exited, "Target must not be exited to create a sheet actor.");
+ if (this._styleSheetActors.has(styleSheet)) {
+ return this._styleSheetActors.get(styleSheet);
+ }
+ const actor = new StyleSheetActor(styleSheet, this);
+ this._styleSheetActors.set(styleSheet, actor);
+
+ this._targetScopedActorPool.manage(actor);
+ this.emit("stylesheet-added", actor);
+
+ return actor;
+ },
+
+ removeActorByName(name) {
+ if (name in this._extraActors) {
+ const actor = this._extraActors[name];
+ if (this._targetScopedActorPool.has(actor)) {
+ this._targetScopedActorPool.removeActor(actor);
+ }
+ delete this._extraActors[name];
+ }
+ },
+};
+
+exports.browsingContextTargetPrototype = browsingContextTargetPrototype;
+exports.BrowsingContextTargetActor = TargetActorMixin(
+ Targets.TYPES.FRAME,
+ browsingContextTargetSpec,
+ browsingContextTargetPrototype
+);
+
+/**
+ * The DebuggerProgressListener object is an nsIWebProgressListener which
+ * handles onStateChange events for the targeted browsing context. 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 BrowsingContextTargetActor targetActor
+ * The browsing context target actor associated with this listener.
+ */
+function DebuggerProgressListener(targetActor) {
+ this._targetActor = targetActor;
+ 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");
+
+ // 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: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+
+ destroy() {
+ Services.obs.removeObserver(this, "inner-window-destroyed");
+ 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.
+ const docShellWindow = docShell.domWindow;
+ this._watchedDocShells.add(docShellWindow);
+
+ const webProgress = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ webProgress.addProgressListener(
+ this,
+ Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
+ Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
+ );
+
+ const 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 targetActor for pre-existing windows
+ for (const win of this._getWindowsInDocShell(docShell)) {
+ this._targetActor._windowReady(win);
+ this._knownWindowIDs.set(getWindowID(win), win);
+ }
+
+ // The `watchedByDevTools` enables gecko behavior tied to this flag, such as:
+ // - reporting the contents of HTML loaded in the docshells,
+ // - or capturing stacks for the network monitor.
+ //
+ // This attribute may already have been toggled by a parent BrowsingContext.
+ // Typically the parent process or tab target. Both are top level BrowsingContext.
+ if (docShell.browsingContext.top == docShell.browsingContext) {
+ docShell.browsingContext.watchedByDevTools = true;
+ }
+ },
+
+ unwatch(docShell) {
+ const docShellWindow = docShell.domWindow;
+ if (!this._watchedDocShells.has(docShellWindow)) {
+ return;
+ }
+ this._watchedDocShells.delete(docShellWindow);
+
+ const 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
+ }
+
+ const handler = getDocShellChromeEventHandler(docShell);
+ handler.removeEventListener(
+ "DOMWindowCreated",
+ this._onWindowCreated,
+ true
+ );
+ handler.removeEventListener("pageshow", this._onWindowCreated, true);
+ handler.removeEventListener("pagehide", this._onWindowHidden, true);
+
+ for (const win of this._getWindowsInDocShell(docShell)) {
+ this._knownWindowIDs.delete(getWindowID(win));
+ }
+
+ // We can only toggle this attribute on top level BrowsingContext,
+ // this will be propagated over the whole tree of BC.
+ // So we only need to set it from Parent Process Target
+ // and Tab Target. Tab's BrowsingContext are actually considered as top level BC.
+ if (docShell.browsingContext.top == docShell.browsingContext) {
+ docShell.browsingContext.watchedByDevTools = false;
+ }
+ },
+
+ _getWindowsInDocShell(docShell) {
+ return getChildDocShells(docShell).map(d => {
+ return d.domWindow;
+ });
+ },
+
+ onWindowCreated: DevToolsUtils.makeInfallible(function(evt) {
+ if (!this._targetActor.attached) {
+ return;
+ }
+
+ // If we're in a frame swap (which occurs when toggling RDM, for example), then we can
+ // ignore this event, as the window never really went anywhere for our purposes.
+ if (evt.inFrameSwap) {
+ return;
+ }
+
+ const window = evt.target.defaultView;
+ if (!window) {
+ // Some old UIs might emit unrelated events called pageshow/pagehide on
+ // elements which are not documents. Bail in this case. See Bug 1669666.
+ return;
+ }
+
+ const innerID = getWindowID(window);
+
+ // This handler is called for two events: "DOMWindowCreated" and "pageshow".
+ // Bail out if we already processed this window.
+ if (this._knownWindowIDs.has(innerID)) {
+ return;
+ }
+ this._knownWindowIDs.set(innerID, window);
+
+ // For a regular page navigation, "DOMWindowCreated" is fired before
+ // "pageshow". If the current event is "pageshow" but we have not processed
+ // the window yet, it means this is a BF cache navigation. In theory,
+ // `event.persisted` should be set for BF cache navigation events, but it is
+ // not always available, so we fallback on checking if "pageshow" is the
+ // first event received for a given window (see Bug 1378133).
+ const isBFCache = evt.type == "pageshow";
+
+ this._targetActor._windowReady(window, { isBFCache });
+ }, "DebuggerProgressListener.prototype.onWindowCreated"),
+
+ onWindowHidden: DevToolsUtils.makeInfallible(function(evt) {
+ if (!this._targetActor.attached) {
+ return;
+ }
+
+ // If we're in a frame swap (which occurs when toggling RDM, for example), then we can
+ // ignore this event, as the window isn't really going anywhere for our purposes.
+ if (evt.inFrameSwap) {
+ 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;
+ }
+
+ const window = evt.target.defaultView;
+ if (!window) {
+ // Some old UIs might emit unrelated events called pageshow/pagehide on
+ // elements which are not documents. Bail in this case. See Bug 1669666.
+ return;
+ }
+
+ this._targetActor._windowDestroyed(window, null, true);
+ this._knownWindowIDs.delete(getWindowID(window));
+ }, "DebuggerProgressListener.prototype.onWindowHidden"),
+
+ observe: DevToolsUtils.makeInfallible(function(subject, topic) {
+ if (!this._targetActor.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
+ const innerID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ const window = this._knownWindowIDs.get(innerID);
+ if (window) {
+ this._knownWindowIDs.delete(innerID);
+ this._targetActor._windowDestroyed(window, innerID);
+ }
+
+ // Bug 1598364: when debugging browser.xhtml from the Browser Toolbox
+ // the DOMWindowCreated/pageshow/pagehide event listeners have to be
+ // re-registered against the next document when we reload browser.html
+ // (or navigate to another doc).
+ // That's because we registered the listener on docShell.domWindow as
+ // top level windows don't have a chromeEventHandler.
+ if (
+ this._watchedDocShells.has(window) &&
+ !window.docShell.chromeEventHandler
+ ) {
+ // First cleanup all the existing listeners
+ this.unwatch(window.docShell);
+ // Re-register new ones. The docShell is already referencing the new document.
+ this.watch(window.docShell);
+ }
+ }, "DebuggerProgressListener.prototype.observe"),
+
+ onStateChange: DevToolsUtils.makeInfallible(function(
+ progress,
+ request,
+ flag,
+ status
+ ) {
+ if (!this._targetActor.attached) {
+ return;
+ }
+ progress.QueryInterface(Ci.nsIDocShell);
+ if (progress.isBeingDestroyed()) {
+ return;
+ }
+
+ const isStart = flag & Ci.nsIWebProgressListener.STATE_START;
+ const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP;
+ const isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
+ const 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.
+ this._targetActor._notifyDocShellsUpdate([progress]);
+ }
+
+ const window = progress.DOMWindow;
+ if (isDocument && isStart) {
+ // One of the earliest events that tells us a new URI
+ // is being loaded in this window.
+ const newURI = request instanceof Ci.nsIChannel ? request.URI.spec : null;
+ this._targetActor._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.
+ const handler = getDocShellChromeEventHandler(progress);
+ const onLoad = evt => {
+ // Ignore events from iframes
+ if (evt.target === window.document) {
+ handler.removeEventListener("DOMContentLoaded", onLoad, true);
+ this._targetActor._navigate(window);
+ }
+ };
+ handler.addEventListener("DOMContentLoaded", onLoad, true);
+ } else {
+ // Somewhat equivalent of load event.
+ // (window.document.readyState == complete)
+ this._targetActor._navigate(window);
+ }
+ }
+ },
+ "DebuggerProgressListener.prototype.onStateChange"),
+};