diff options
Diffstat (limited to 'devtools/server/actors/targets/browsing-context.js')
-rw-r--r-- | devtools/server/actors/targets/browsing-context.js | 1899 |
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"), +}; |