summaryrefslogtreecommitdiffstats
path: root/remote/domains/content
diff options
context:
space:
mode:
Diffstat (limited to 'remote/domains/content')
-rw-r--r--remote/domains/content/DOM.jsm250
-rw-r--r--remote/domains/content/Emulation.jsm44
-rw-r--r--remote/domains/content/Input.jsm71
-rw-r--r--remote/domains/content/Log.jsm93
-rw-r--r--remote/domains/content/Network.jsm24
-rw-r--r--remote/domains/content/Page.jsm457
-rw-r--r--remote/domains/content/Performance.jsm38
-rw-r--r--remote/domains/content/Runtime.jsm621
-rw-r--r--remote/domains/content/Security.jsm38
-rw-r--r--remote/domains/content/runtime/ExecutionContext.jsm538
10 files changed, 2174 insertions, 0 deletions
diff --git a/remote/domains/content/DOM.jsm b/remote/domains/content/DOM.jsm
new file mode 100644
index 0000000000..a771dfbffb
--- /dev/null
+++ b/remote/domains/content/DOM.jsm
@@ -0,0 +1,250 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["DOM"];
+
+const { ContentProcessDomain } = ChromeUtils.import(
+ "chrome://remote/content/domains/ContentProcessDomain.jsm"
+);
+
+class DOM extends ContentProcessDomain {
+ constructor(session) {
+ super(session);
+ this.enabled = false;
+ }
+
+ destructor() {
+ this.disable();
+ }
+
+ // commands
+
+ async enable() {
+ if (!this.enabled) {
+ this.enabled = true;
+ }
+ }
+
+ /**
+ * Describes node given its id.
+ *
+ * Does not require domain to be enabled. Does not start tracking any objects.
+ *
+ * @param {Object} options
+ * @param {number=} options.backendNodeId [not supported]
+ * Identifier of the backend node.
+ * @param {number=} options.depth [not supported]
+ * The maximum depth at which children should be retrieved, defaults to 1.
+ * Use -1 for the entire subtree or provide an integer larger than 0.
+ * @param {number=} options.nodeId [not supported]
+ * Identifier of the node.
+ * @param {string} options.objectId
+ * JavaScript object id of the node wrapper.
+ * @param {boolean=} options.pierce [not supported]
+ * Whether or not iframes and shadow roots should be traversed
+ * when returning the subtree, defaults to false.
+ *
+ * @return {DOM.Node}
+ * Node description.
+ */
+ describeNode(options = {}) {
+ const { objectId } = options;
+
+ // Until nodeId/backendNodeId is supported force usage of the objectId
+ if (!["string"].includes(typeof objectId)) {
+ throw new TypeError("objectId: string value expected");
+ }
+
+ const Runtime = this.session.domains.get("Runtime");
+ const debuggerObj = Runtime._getRemoteObject(objectId);
+ if (!debuggerObj) {
+ throw new Error("Could not find object with given id");
+ }
+
+ if (typeof debuggerObj.nodeId == "undefined") {
+ throw new Error("Object id doesn't reference a Node");
+ }
+
+ const unsafeObj = debuggerObj.unsafeDereference();
+
+ const attributes = [];
+ if (unsafeObj.attributes) {
+ // Flatten the list of attributes for name and value
+ for (const attribute of unsafeObj.attributes) {
+ attributes.push(attribute.name, attribute.value);
+ }
+ }
+
+ let context = this.docShell.browsingContext;
+ if (unsafeObj instanceof HTMLIFrameElement) {
+ context = unsafeObj.contentWindow.docShell.browsingContext;
+ }
+
+ const node = {
+ nodeId: debuggerObj.nodeId,
+ backendNodeId: debuggerObj.nodeId,
+ nodeType: unsafeObj.nodeType,
+ nodeName: unsafeObj.nodeName,
+ localName: unsafeObj.localName,
+ nodeValue: unsafeObj.nodeValue ? unsafeObj.nodeValue.toString() : "",
+ childNodeCount: unsafeObj.childElementCount,
+ attributes: attributes.length > 0 ? attributes : undefined,
+ frameId: context.id.toString(),
+ };
+
+ return { node };
+ }
+
+ disable() {
+ if (this.enabled) {
+ this.enabled = false;
+ }
+ }
+
+ getContentQuads({ objectId }) {
+ const Runtime = this.session.domains.get("Runtime");
+ const debuggerObj = Runtime._getRemoteObject(objectId);
+ if (!debuggerObj) {
+ throw new Error(`Cannot find object with id: ${objectId}`);
+ }
+ const unsafeObject = debuggerObj.unsafeDereference();
+ if (!unsafeObject.getBoxQuads) {
+ throw new Error("RemoteObject is not a node");
+ }
+ let quads = unsafeObject.getBoxQuads({ relativeTo: this.content.document });
+ quads = quads.map(quad => {
+ return [
+ quad.p1.x,
+ quad.p1.y,
+ quad.p2.x,
+ quad.p2.y,
+ quad.p3.x,
+ quad.p3.y,
+ quad.p4.x,
+ quad.p4.y,
+ ].map(Math.round);
+ });
+ return { quads };
+ }
+
+ getBoxModel({ objectId }) {
+ const Runtime = this.session.domains.get("Runtime");
+ const debuggerObj = Runtime._getRemoteObject(objectId);
+ if (!debuggerObj) {
+ throw new Error(`Cannot find object with id: ${objectId}`);
+ }
+ const unsafeObject = debuggerObj.unsafeDereference();
+ const bounding = unsafeObject.getBoundingClientRect();
+ const model = {
+ width: Math.round(bounding.width),
+ height: Math.round(bounding.height),
+ };
+ for (const box of ["content", "padding", "border", "margin"]) {
+ const quads = unsafeObject.getBoxQuads({
+ box,
+ relativeTo: this.content.document,
+ });
+
+ // getBoxQuads may return more than one element. In this case we have to compute the bounding box
+ // of all these boxes.
+ let bounding = {
+ p1: { x: Infinity, y: Infinity },
+ p2: { x: -Infinity, y: Infinity },
+ p3: { x: -Infinity, y: -Infinity },
+ p4: { x: Infinity, y: -Infinity },
+ };
+ quads.forEach(quad => {
+ bounding = {
+ p1: {
+ x: Math.min(bounding.p1.x, quad.p1.x),
+ y: Math.min(bounding.p1.y, quad.p1.y),
+ },
+ p2: {
+ x: Math.max(bounding.p2.x, quad.p2.x),
+ y: Math.min(bounding.p2.y, quad.p2.y),
+ },
+ p3: {
+ x: Math.max(bounding.p3.x, quad.p3.x),
+ y: Math.max(bounding.p3.y, quad.p3.y),
+ },
+ p4: {
+ x: Math.min(bounding.p4.x, quad.p4.x),
+ y: Math.max(bounding.p4.y, quad.p4.y),
+ },
+ };
+ });
+
+ model[box] = [
+ bounding.p1.x,
+ bounding.p1.y,
+ bounding.p2.x,
+ bounding.p2.y,
+ bounding.p3.x,
+ bounding.p3.y,
+ bounding.p4.x,
+ bounding.p4.y,
+ ].map(Math.round);
+ }
+ return {
+ model,
+ };
+ }
+
+ /**
+ * Resolves the JavaScript node object for a given NodeId or BackendNodeId.
+ *
+ * @param {Object} options
+ * @param {number} options.backendNodeId [required for now]
+ * Backend identifier of the node to resolve.
+ * @param {number=} options.executionContextId
+ * Execution context in which to resolve the node.
+ * @param {number=} options.nodeId [not supported]
+ * Id of the node to resolve.
+ * @param {string=} options.objectGroup [not supported]
+ * Symbolic group name that can be used to release multiple objects.
+ *
+ * @return {Runtime.RemoteObject}
+ * JavaScript object wrapper for given node.
+ */
+ resolveNode(options = {}) {
+ const { backendNodeId, executionContextId } = options;
+
+ // Until nodeId is supported force usage of the backendNodeId
+ // Bug 1625417 - CDP expects the id as number
+ if (!["string"].includes(typeof backendNodeId)) {
+ throw new TypeError("backendNodeId: string value expected");
+ }
+ if (!["undefined", "number"].includes(typeof executionContextId)) {
+ throw new TypeError("executionContextId: integer value expected");
+ }
+
+ const Runtime = this.session.domains.get("Runtime");
+
+ // Retrieve the node to resolve, and its context
+ const debuggerObj = Runtime._getRemoteObjectByNodeId(backendNodeId);
+
+ if (!debuggerObj) {
+ throw new Error(`No node with given id found`);
+ }
+
+ // If execution context isn't specified use the default one for the node
+ let context;
+ if (typeof executionContextId != "undefined") {
+ context = Runtime.contexts.get(executionContextId);
+ if (!context) {
+ throw new Error(`Node with given id does not belong to the document`);
+ }
+ } else {
+ context = Runtime._getDefaultContextForWindow();
+ }
+
+ Runtime._setRemoteObject(debuggerObj, context);
+
+ return {
+ object: Runtime._serializeRemoteObject(debuggerObj, context.id),
+ };
+ }
+}
diff --git a/remote/domains/content/Emulation.jsm b/remote/domains/content/Emulation.jsm
new file mode 100644
index 0000000000..323204202b
--- /dev/null
+++ b/remote/domains/content/Emulation.jsm
@@ -0,0 +1,44 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["Emulation"];
+
+const { ContentProcessDomain } = ChromeUtils.import(
+ "chrome://remote/content/domains/ContentProcessDomain.jsm"
+);
+
+class Emulation extends ContentProcessDomain {
+ // commands
+
+ /**
+ * Internal methods: the following methods are not part of CDP;
+ * note the _ prefix.
+ */
+
+ /**
+ * Waits until the viewport has reached the new dimensions.
+ */
+ async _awaitViewportDimensions({ width, height }) {
+ const win = this.content;
+
+ if (win.innerWidth === width && win.innerHeight === height) {
+ return;
+ }
+
+ await new Promise(resolve => {
+ win.addEventListener("resize", function resized() {
+ if (win.innerWidth === width && win.innerHeight === height) {
+ win.removeEventListener("resize", resized);
+ resolve();
+ }
+ });
+ });
+ }
+
+ _setDPPXOverride(dppx) {
+ this.docShell.contentViewer.overrideDPPX = dppx;
+ }
+}
diff --git a/remote/domains/content/Input.jsm b/remote/domains/content/Input.jsm
new file mode 100644
index 0000000000..17d88550e5
--- /dev/null
+++ b/remote/domains/content/Input.jsm
@@ -0,0 +1,71 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["Input"];
+
+const { ContentProcessDomain } = ChromeUtils.import(
+ "chrome://remote/content/domains/ContentProcessDomain.jsm"
+);
+
+class Input extends ContentProcessDomain {
+ constructor(session) {
+ super(session);
+
+ // Internal id used to track existing event handlers.
+ this._eventId = 0;
+
+ // Map of event id -> event handler promise.
+ this._eventPromises = new Map();
+ }
+
+ /**
+ * Internal methods: the following methods are not part of CDP;
+ * note the _ prefix.
+ */
+
+ /**
+ * Add an event listener in the content page for the provided eventName.
+ * This method will return a unique handler id that can be used to wait
+ * for the event.
+ *
+ * Example usage from a parent process domain:
+ *
+ * const id = await this.executeInChild("addContentEventListener", "click");
+ * // do something that triggers a click in content
+ * await this.executeInChild("waitForContentEvent", id);
+ */
+ _addContentEventListener(eventName) {
+ const eventPromise = new Promise(r => {
+ this.chromeEventHandler.addEventListener(eventName, r, {
+ mozSystemGroup: true,
+ once: true,
+ });
+ });
+ this._eventId++;
+ this._eventPromises.set(this._eventId, eventPromise);
+ return this._eventId;
+ }
+
+ /**
+ * Wait for an event listener added via `addContentEventListener` to be fired.
+ */
+ async _waitForContentEvent(eventId) {
+ const eventPromise = this._eventPromises.get(eventId);
+ if (!eventPromise) {
+ throw new Error("No event promise found for id " + eventId);
+ }
+ await eventPromise;
+ this._eventPromises.delete(eventId);
+ }
+
+ /**
+ * Expose docShell.doCommand to parent domain.
+ * Used in temporary workaround for emulating certain native key bindings
+ */
+ _doDocShellCommand(command) {
+ this.docShell.doCommand(command);
+ }
+}
diff --git a/remote/domains/content/Log.jsm b/remote/domains/content/Log.jsm
new file mode 100644
index 0000000000..316f1adfeb
--- /dev/null
+++ b/remote/domains/content/Log.jsm
@@ -0,0 +1,93 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["Log"];
+
+const { ContentProcessDomain } = ChromeUtils.import(
+ "chrome://remote/content/domains/ContentProcessDomain.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const CONSOLE_MESSAGE_LEVEL_MAP = {
+ [Ci.nsIConsoleMessage.debug]: "verbose",
+ [Ci.nsIConsoleMessage.info]: "info",
+ [Ci.nsIConsoleMessage.warn]: "warning",
+ [Ci.nsIConsoleMessage.error]: "error",
+};
+
+class Log extends ContentProcessDomain {
+ constructor(session) {
+ super(session);
+ this.enabled = false;
+ }
+
+ destructor() {
+ this.disable();
+
+ super.destructor();
+ }
+
+ enable() {
+ if (!this.enabled) {
+ this.enabled = true;
+
+ Services.console.registerListener(this);
+ }
+ }
+
+ disable() {
+ if (this.enabled) {
+ this.enabled = false;
+
+ Services.console.unregisterListener(this);
+ }
+ }
+
+ _getLogCategory(category) {
+ if (category.startsWith("CORS")) {
+ return "network";
+ } else if (category.includes("javascript")) {
+ return "javascript";
+ }
+
+ return "other";
+ }
+
+ // nsIObserver
+
+ /**
+ * Takes all script error messages that do not have an exception attached,
+ * and emits a "Log.entryAdded" event.
+ *
+ * @param {nsIConsoleMessage} message
+ * Message originating from the nsIConsoleService.
+ */
+ observe(message) {
+ if (message instanceof Ci.nsIScriptError && !message.hasException) {
+ let url;
+ if (message.sourceName !== "debugger eval code") {
+ url = message.sourceName;
+ }
+
+ const entry = {
+ source: this._getLogCategory(message.category),
+ level: CONSOLE_MESSAGE_LEVEL_MAP[message.logLevel],
+ text: message.errorMessage,
+ timestamp: message.timeStamp,
+ url,
+ lineNumber: message.lineNumber,
+ };
+
+ this.emit("Log.entryAdded", { entry });
+ }
+ }
+
+ // XPCOM
+
+ get QueryInterface() {
+ return ChromeUtils.generateQI(["nsIConsoleListener"]);
+ }
+}
diff --git a/remote/domains/content/Network.jsm b/remote/domains/content/Network.jsm
new file mode 100644
index 0000000000..3f83e2c967
--- /dev/null
+++ b/remote/domains/content/Network.jsm
@@ -0,0 +1,24 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["Network"];
+
+const { ContentProcessDomain } = ChromeUtils.import(
+ "chrome://remote/content/domains/ContentProcessDomain.jsm"
+);
+
+class Network extends ContentProcessDomain {
+ // commands
+
+ /**
+ * Internal methods: the following methods are not part of CDP;
+ * note the _ prefix.
+ */
+
+ _updateLoadFlags(flags) {
+ this.docShell.defaultLoadFlags = flags;
+ }
+}
diff --git a/remote/domains/content/Page.jsm b/remote/domains/content/Page.jsm
new file mode 100644
index 0000000000..8a3c85ce26
--- /dev/null
+++ b/remote/domains/content/Page.jsm
@@ -0,0 +1,457 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["Page"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+const { ContentProcessDomain } = ChromeUtils.import(
+ "chrome://remote/content/domains/ContentProcessDomain.jsm"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "uuidGen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator"
+);
+
+const {
+ LOAD_FLAGS_BYPASS_CACHE,
+ LOAD_FLAGS_BYPASS_PROXY,
+ LOAD_FLAGS_NONE,
+} = Ci.nsIWebNavigation;
+
+class Page extends ContentProcessDomain {
+ constructor(session) {
+ super(session);
+
+ this.enabled = false;
+ this.lifecycleEnabled = false;
+ // script id => { source, worldName }
+ this.scriptsToEvaluateOnLoad = new Map();
+ this.worldsToEvaluateOnLoad = new Set();
+
+ // This map is used to keep a reference to the loader id for
+ // those Page events, which do not directly rely on
+ // Network events. This might be a temporary solution until
+ // the Network observer could be queried for that. But right
+ // now this lives in the parent process.
+ this.frameIdToLoaderId = new Map();
+
+ this._onFrameAttached = this._onFrameAttached.bind(this);
+ this._onFrameDetached = this._onFrameDetached.bind(this);
+ this._onFrameNavigated = this._onFrameNavigated.bind(this);
+ this._onScriptLoaded = this._onScriptLoaded.bind(this);
+
+ this.session.contextObserver.on("script-loaded", this._onScriptLoaded);
+ }
+
+ destructor() {
+ this.setLifecycleEventsEnabled({ enabled: false });
+ this.session.contextObserver.off("script-loaded", this._onScriptLoaded);
+ this.disable();
+
+ super.destructor();
+ }
+
+ // commands
+
+ async enable() {
+ if (!this.enabled) {
+ this.session.contextObserver.on(
+ "docshell-created",
+ this._onFrameAttached
+ );
+ this.session.contextObserver.on(
+ "docshell-destroyed",
+ this._onFrameDetached
+ );
+ this.session.contextObserver.on(
+ "frame-navigated",
+ this._onFrameNavigated
+ );
+
+ this.chromeEventHandler.addEventListener("readystatechange", this, {
+ mozSystemGroup: true,
+ capture: true,
+ });
+ this.chromeEventHandler.addEventListener("pagehide", this, {
+ mozSystemGroup: true,
+ });
+ this.chromeEventHandler.addEventListener("unload", this, {
+ mozSystemGroup: true,
+ capture: true,
+ });
+ this.chromeEventHandler.addEventListener("DOMContentLoaded", this, {
+ mozSystemGroup: true,
+ });
+ this.chromeEventHandler.addEventListener("load", this, {
+ mozSystemGroup: true,
+ capture: true,
+ });
+ this.chromeEventHandler.addEventListener("pageshow", this, {
+ mozSystemGroup: true,
+ });
+
+ this.enabled = true;
+ }
+ }
+
+ disable() {
+ if (this.enabled) {
+ this.session.contextObserver.off(
+ "docshell-created",
+ this._onFrameAttached
+ );
+ this.session.contextObserver.off(
+ "docshell-destroyed",
+ this._onFrameDetached
+ );
+ this.session.contextObserver.off(
+ "frame-navigated",
+ this._onFrameNavigated
+ );
+
+ this.chromeEventHandler.removeEventListener("readystatechange", this, {
+ mozSystemGroup: true,
+ capture: true,
+ });
+ this.chromeEventHandler.removeEventListener("pagehide", this, {
+ mozSystemGroup: true,
+ });
+ this.chromeEventHandler.removeEventListener("unload", this, {
+ mozSystemGroup: true,
+ capture: true,
+ });
+ this.chromeEventHandler.removeEventListener("DOMContentLoaded", this, {
+ mozSystemGroup: true,
+ });
+ this.chromeEventHandler.removeEventListener("load", this, {
+ mozSystemGroup: true,
+ capture: true,
+ });
+ this.chromeEventHandler.removeEventListener("pageshow", this, {
+ mozSystemGroup: true,
+ });
+ this.enabled = false;
+ }
+ }
+
+ async reload({ ignoreCache }) {
+ let flags = LOAD_FLAGS_NONE;
+ if (ignoreCache) {
+ flags |= LOAD_FLAGS_BYPASS_CACHE;
+ flags |= LOAD_FLAGS_BYPASS_PROXY;
+ }
+ this.docShell.reload(flags);
+ }
+
+ getFrameTree() {
+ const getFrames = context => {
+ const frameTree = {
+ frame: this._getFrameDetails({ context }),
+ };
+
+ if (context.children.length > 0) {
+ const frames = [];
+ for (const childContext of context.children) {
+ frames.push(getFrames(childContext));
+ }
+ frameTree.childFrames = frames;
+ }
+
+ return frameTree;
+ };
+
+ return {
+ frameTree: getFrames(this.docShell.browsingContext),
+ };
+ }
+
+ /**
+ * Enqueues given script to be evaluated in every frame upon creation
+ *
+ * If `worldName` is specified, creates an execution context with the given name
+ * and evaluates given script in it.
+ *
+ * At this time, queued scripts do not get evaluated, hence `source` is marked as
+ * "unsupported".
+ *
+ * @param {Object} options
+ * @param {string} options.source (not supported)
+ * @param {string=} options.worldName
+ * @return {string} Page.ScriptIdentifier
+ */
+ addScriptToEvaluateOnNewDocument(options = {}) {
+ const { source, worldName } = options;
+ if (worldName) {
+ this.worldsToEvaluateOnLoad.add(worldName);
+ }
+ const identifier = uuidGen
+ .generateUUID()
+ .toString()
+ .slice(1, -1);
+ this.scriptsToEvaluateOnLoad.set(identifier, { worldName, source });
+
+ return { identifier };
+ }
+
+ /**
+ * Creates an isolated world for the given frame.
+ *
+ * Really it just creates an execution context with label "isolated".
+ *
+ * @param {Object} options
+ * @param {string} options.frameId
+ * Id of the frame in which the isolated world should be created.
+ * @param {string=} options.worldName
+ * An optional name which is reported in the Execution Context.
+ * @param {boolean=} options.grantUniversalAccess (not supported)
+ * This is a powerful option, use with caution.
+ *
+ * @return {number} Runtime.ExecutionContextId
+ * Execution context of the isolated world.
+ */
+ createIsolatedWorld(options = {}) {
+ const { frameId, worldName } = options;
+
+ if (typeof frameId != "string") {
+ throw new TypeError("frameId: string value expected");
+ }
+
+ if (!["undefined", "string"].includes(typeof worldName)) {
+ throw new TypeError("worldName: string value expected");
+ }
+
+ const Runtime = this.session.domains.get("Runtime");
+ const contexts = Runtime._getContextsForFrame(frameId);
+ if (contexts.length == 0) {
+ throw new Error("No frame for given id found");
+ }
+
+ const defaultContext = Runtime._getDefaultContextForWindow(
+ contexts[0].windowId
+ );
+ const window = defaultContext.window;
+
+ const executionContextId = Runtime._onContextCreated("context-created", {
+ windowId: window.windowGlobalChild.innerWindowId,
+ window,
+ isDefault: false,
+ contextName: worldName,
+ contextType: "isolated",
+ });
+
+ return { executionContextId };
+ }
+
+ /**
+ * Controls whether page will emit lifecycle events.
+ *
+ * @param {Object} options
+ * @param {boolean} options.enabled
+ * If true, starts emitting lifecycle events.
+ */
+ setLifecycleEventsEnabled(options = {}) {
+ const { enabled } = options;
+
+ this.lifecycleEnabled = enabled;
+ }
+
+ url() {
+ return this.content.location.href;
+ }
+
+ _onFrameAttached(name, { id }) {
+ const bc = BrowsingContext.get(id);
+
+ // Don't emit for top-level browsing contexts
+ if (!bc.parent) {
+ return;
+ }
+
+ // TODO: Use a unique identifier for frames (bug 1605359)
+ this.emit("Page.frameAttached", {
+ frameId: bc.id.toString(),
+ parentFrameId: bc.parent.id.toString(),
+ stack: null,
+ });
+
+ const loaderId = this.frameIdToLoaderId.get(bc.id);
+ const timestamp = Date.now() / 1000;
+ this.emit("Page.frameStartedLoading", { frameId: bc.id.toString() });
+ this.emitLifecycleEvent(bc.id, loaderId, "init", timestamp);
+ }
+
+ _onFrameDetached(name, { id }) {
+ const bc = BrowsingContext.get(id);
+
+ // Don't emit for top-level browsing contexts
+ if (!bc.parent) {
+ return;
+ }
+
+ // TODO: Use a unique identifier for frames (bug 1605359)
+ this.emit("Page.frameDetached", { frameId: bc.id.toString() });
+ }
+
+ _onFrameNavigated(name, { frameId }) {
+ const bc = BrowsingContext.get(frameId);
+
+ this.emit("Page.frameNavigated", {
+ frame: this._getFrameDetails({ context: bc }),
+ });
+ }
+
+ /**
+ * @param {Object=} options
+ * @param {number} options.windowId
+ * The inner window id of the window the script has been loaded for.
+ * @param {Window} options.window
+ * The window object of the document.
+ */
+ _onScriptLoaded(name, options = {}) {
+ const { windowId, window } = options;
+
+ const Runtime = this.session.domains.get("Runtime");
+ for (const world of this.worldsToEvaluateOnLoad) {
+ Runtime._onContextCreated("context-created", {
+ windowId,
+ window,
+ isDefault: false,
+ contextName: world,
+ contextType: "isolated",
+ });
+ }
+ // TODO evaluate each onNewDoc script in the appropriate world
+ }
+
+ emitLifecycleEvent(frameId, loaderId, name, timestamp) {
+ if (this.lifecycleEnabled) {
+ this.emit("Page.lifecycleEvent", {
+ frameId: frameId.toString(),
+ loaderId,
+ name,
+ timestamp,
+ });
+ }
+ }
+
+ handleEvent({ type, target }) {
+ const timestamp = Date.now() / 1000;
+ const frameId = target.defaultView.docShell.browsingContext.id;
+ const isFrame = !!target.defaultView.docShell.browsingContext.parent;
+ const loaderId = this.frameIdToLoaderId.get(frameId);
+ const url = target.location.href;
+
+ switch (type) {
+ case "DOMContentLoaded":
+ if (!isFrame) {
+ this.emit("Page.domContentEventFired", { timestamp });
+ }
+ this.emitLifecycleEvent(
+ frameId,
+ loaderId,
+ "DOMContentLoaded",
+ timestamp
+ );
+ break;
+
+ case "pagehide":
+ // Maybe better to bound to "unload" once we can register for this event
+ this.emit("Page.frameStartedLoading", { frameId: frameId.toString() });
+ this.emitLifecycleEvent(frameId, loaderId, "init", timestamp);
+ break;
+
+ case "load":
+ if (!isFrame) {
+ this.emit("Page.loadEventFired", { timestamp });
+ }
+ this.emitLifecycleEvent(frameId, loaderId, "load", timestamp);
+
+ // Todo: Only to be emitted for hashchange events (bug 1636453)
+ this.emit("Page.navigatedWithinDocument", {
+ frameId: frameId.toString(),
+ url,
+ });
+
+ // XXX this should most likely be sent differently
+ this.emit("Page.frameStoppedLoading", { frameId: frameId.toString() });
+ break;
+
+ case "readystatechange":
+ if (this.content.document.readyState === "loading") {
+ this.emitLifecycleEvent(frameId, loaderId, "init", timestamp);
+ }
+ }
+ }
+
+ _updateLoaderId(data) {
+ const { frameId, loaderId } = data;
+
+ this.frameIdToLoaderId.set(frameId, loaderId);
+ }
+
+ _contentRect() {
+ const docEl = this.content.document.documentElement;
+
+ return {
+ x: 0,
+ y: 0,
+ width: docEl.scrollWidth,
+ height: docEl.scrollHeight,
+ };
+ }
+
+ _devicePixelRatio() {
+ return this.content.devicePixelRatio;
+ }
+
+ _getFrameDetails({ context, id }) {
+ const bc = context || BrowsingContext.get(id);
+ const frame = bc.embedderElement;
+
+ return {
+ id: bc.id.toString(),
+ parentId: bc.parent?.id.toString(),
+ loaderId: this.frameIdToLoaderId.get(bc.id),
+ url: bc.docShell.domWindow.location.href,
+ name: frame?.id || frame?.name,
+ securityOrigin: null,
+ mimeType: null,
+ };
+ }
+
+ _getScrollbarSize() {
+ const scrollbarHeight = {};
+ const scrollbarWidth = {};
+
+ this.content.windowUtils.getScrollbarSize(
+ false,
+ scrollbarWidth,
+ scrollbarHeight
+ );
+
+ return {
+ width: scrollbarWidth.value,
+ height: scrollbarHeight.value,
+ };
+ }
+
+ _layoutViewport() {
+ const scrollbarSize = this._getScrollbarSize();
+
+ return {
+ pageX: this.content.pageXOffset,
+ pageY: this.content.pageYOffset,
+ clientWidth: this.content.innerWidth - scrollbarSize.width,
+ clientHeight: this.content.innerHeight - scrollbarSize.height,
+ };
+ }
+}
diff --git a/remote/domains/content/Performance.jsm b/remote/domains/content/Performance.jsm
new file mode 100644
index 0000000000..2252ba3759
--- /dev/null
+++ b/remote/domains/content/Performance.jsm
@@ -0,0 +1,38 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["Performance"];
+
+const { ContentProcessDomain } = ChromeUtils.import(
+ "chrome://remote/content/domains/ContentProcessDomain.jsm"
+);
+
+class Performance extends ContentProcessDomain {
+ constructor(session) {
+ super(session);
+ this.enabled = false;
+ }
+
+ destructor() {
+ this.disable();
+
+ super.destructor();
+ }
+
+ // commands
+
+ async enable() {
+ if (!this.enabled) {
+ this.enabled = true;
+ }
+ }
+
+ disable() {
+ if (this.enabled) {
+ this.enabled = false;
+ }
+ }
+}
diff --git a/remote/domains/content/Runtime.jsm b/remote/domains/content/Runtime.jsm
new file mode 100644
index 0000000000..2613de742a
--- /dev/null
+++ b/remote/domains/content/Runtime.jsm
@@ -0,0 +1,621 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["Runtime"];
+
+const { addDebuggerToGlobal } = ChromeUtils.import(
+ "resource://gre/modules/jsdebugger.jsm",
+ {}
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const { ContentProcessDomain } = ChromeUtils.import(
+ "chrome://remote/content/domains/ContentProcessDomain.jsm"
+);
+const { ExecutionContext } = ChromeUtils.import(
+ "chrome://remote/content/domains/content/runtime/ExecutionContext.jsm"
+);
+const { executeSoon } = ChromeUtils.import("chrome://remote/content/Sync.jsm");
+
+// Import the `Debugger` constructor in the current scope
+addDebuggerToGlobal(Cu.getGlobalForObject(this));
+
+const OBSERVER_CONSOLE_API = "console-api-log-event";
+
+const CONSOLE_API_LEVEL_MAP = {
+ warn: "warning",
+};
+
+class SetMap extends Map {
+ constructor() {
+ super();
+ this._count = 1;
+ }
+ // Every key in the map is associated with a Set.
+ // The first time `key` is used `obj.set(key, value)` maps `key` to
+ // to `Set(value)`. Subsequent calls add more values to the Set for `key`.
+ // Note that `obj.get(key)` will return undefined if there's no such key,
+ // as in a regular Map.
+ set(key, value) {
+ const innerSet = this.get(key);
+ if (innerSet) {
+ innerSet.add(value);
+ } else {
+ super.set(key, new Set([value]));
+ }
+ this._count++;
+ return this;
+ }
+ // used as ExecutionContext id
+ get count() {
+ return this._count;
+ }
+}
+
+class Runtime extends ContentProcessDomain {
+ constructor(session) {
+ super(session);
+ this.enabled = false;
+
+ // Map of all the ExecutionContext instances:
+ // [id (Number) => ExecutionContext instance]
+ this.contexts = new Map();
+ // [innerWindowId (Number) => Set of ExecutionContext instances]
+ this.innerWindowIdToContexts = new SetMap();
+
+ this._onContextCreated = this._onContextCreated.bind(this);
+ this._onContextDestroyed = this._onContextDestroyed.bind(this);
+
+ // TODO Bug 1602083
+ this.session.contextObserver.on("context-created", this._onContextCreated);
+ this.session.contextObserver.on(
+ "context-destroyed",
+ this._onContextDestroyed
+ );
+ }
+
+ destructor() {
+ this.disable();
+
+ this.session.contextObserver.off("context-created", this._onContextCreated);
+ this.session.contextObserver.off(
+ "context-destroyed",
+ this._onContextDestroyed
+ );
+
+ super.destructor();
+ }
+
+ // commands
+
+ async enable() {
+ if (!this.enabled) {
+ this.enabled = true;
+
+ Services.console.registerListener(this);
+ Services.obs.addObserver(this, OBSERVER_CONSOLE_API);
+
+ // Spin the event loop in order to send the `executionContextCreated` event right
+ // after we replied to `enable` request.
+ executeSoon(() => {
+ this._onContextCreated("context-created", {
+ windowId: this.content.windowGlobalChild.innerWindowId,
+ window: this.content,
+ isDefault: true,
+ });
+ });
+ }
+ }
+
+ disable() {
+ if (this.enabled) {
+ this.enabled = false;
+
+ Services.console.unregisterListener(this);
+ Services.obs.removeObserver(this, OBSERVER_CONSOLE_API);
+ }
+ }
+
+ releaseObject({ objectId }) {
+ let context = null;
+ for (const ctx of this.contexts.values()) {
+ if (ctx.hasRemoteObject(objectId)) {
+ context = ctx;
+ break;
+ }
+ }
+ if (!context) {
+ throw new Error(
+ `Unable to get execution context by object ID: ${objectId}`
+ );
+ }
+ context.releaseObject(objectId);
+ }
+
+ /**
+ * Calls function with given declaration on the given object.
+ *
+ * Object group of the result is inherited from the target object.
+ *
+ * @param {Object} options
+ * @param {string} options.functionDeclaration
+ * Declaration of the function to call.
+ * @param {Array.<Object>=} options.arguments
+ * Call arguments. All call arguments must belong to the same
+ * JavaScript world as the target object.
+ * @param {boolean=} options.awaitPromise
+ * Whether execution should `await` for resulting value
+ * and return once awaited promise is resolved.
+ * @param {number=} options.executionContextId
+ * Specifies execution context which global object will be used
+ * to call function on. Either executionContextId or objectId
+ * should be specified.
+ * @param {string=} options.objectId
+ * Identifier of the object to call function on.
+ * Either objectId or executionContextId should be specified.
+ * @param {boolean=} options.returnByValue
+ * Whether the result is expected to be a JSON object
+ * which should be sent by value.
+ *
+ * @return {Object.<RemoteObject, ExceptionDetails>}
+ */
+ callFunctionOn(options = {}) {
+ if (typeof options.functionDeclaration != "string") {
+ throw new TypeError("functionDeclaration: string value expected");
+ }
+ if (
+ typeof options.arguments != "undefined" &&
+ !Array.isArray(options.arguments)
+ ) {
+ throw new TypeError("arguments: array value expected");
+ }
+ if (!["undefined", "boolean"].includes(typeof options.awaitPromise)) {
+ throw new TypeError("awaitPromise: boolean value expected");
+ }
+ if (!["undefined", "number"].includes(typeof options.executionContextId)) {
+ throw new TypeError("executionContextId: number value expected");
+ }
+ if (!["undefined", "string"].includes(typeof options.objectId)) {
+ throw new TypeError("objectId: string value expected");
+ }
+ if (!["undefined", "boolean"].includes(typeof options.returnByValue)) {
+ throw new TypeError("returnByValue: boolean value expected");
+ }
+
+ if (
+ typeof options.executionContextId == "undefined" &&
+ typeof options.objectId == "undefined"
+ ) {
+ throw new Error(
+ "Either objectId or executionContextId must be specified"
+ );
+ }
+
+ let context = null;
+ // When an `objectId` is passed, we want to execute the function of a given object
+ // So we first have to find its ExecutionContext
+ if (options.objectId) {
+ for (const ctx of this.contexts.values()) {
+ if (ctx.hasRemoteObject(options.objectId)) {
+ context = ctx;
+ break;
+ }
+ }
+ if (!context) {
+ throw new Error(
+ `Unable to get the context for object with id: ${options.objectId}`
+ );
+ }
+ } else {
+ context = this.contexts.get(options.executionContextId);
+ if (!context) {
+ throw new Error("Cannot find context with specified id");
+ }
+ }
+
+ return context.callFunctionOn(
+ options.functionDeclaration,
+ options.arguments,
+ options.returnByValue,
+ options.awaitPromise,
+ options.objectId
+ );
+ }
+
+ /**
+ * Evaluate expression on global object.
+ *
+ * @param {Object} options
+ * @param {string} options.expression
+ * Expression to evaluate.
+ * @param {boolean=} options.awaitPromise
+ * Whether execution should `await` for resulting value
+ * and return once awaited promise is resolved.
+ * @param {number=} options.contextId
+ * Specifies in which execution context to perform evaluation.
+ * If the parameter is omitted the evaluation will be performed
+ * in the context of the inspected page.
+ * @param {boolean=} options.returnByValue
+ * Whether the result is expected to be a JSON object
+ * that should be sent by value. Defaults to false.
+ * @param {boolean=} options.userGesture [unsupported]
+ * Whether execution should be treated as initiated by user in the UI.
+ *
+ * @return {Object<RemoteObject, exceptionDetails>}
+ * The evaluation result, and optionally exception details.
+ */
+ evaluate(options = {}) {
+ const {
+ expression,
+ awaitPromise = false,
+ contextId,
+ returnByValue = false,
+ } = options;
+
+ if (typeof expression != "string") {
+ throw new Error("expression: string value expected");
+ }
+ if (!["undefined", "boolean"].includes(typeof options.awaitPromise)) {
+ throw new TypeError("awaitPromise: boolean value expected");
+ }
+ if (typeof returnByValue != "boolean") {
+ throw new Error("returnByValue: boolean value expected");
+ }
+
+ let context;
+ if (typeof contextId != "undefined") {
+ context = this.contexts.get(contextId);
+ if (!context) {
+ throw new Error("Cannot find context with specified id");
+ }
+ } else {
+ context = this._getDefaultContextForWindow();
+ }
+
+ return context.evaluate(expression, awaitPromise, returnByValue);
+ }
+
+ getProperties({ objectId, ownProperties }) {
+ for (const ctx of this.contexts.values()) {
+ const debuggerObj = ctx.getRemoteObject(objectId);
+ if (debuggerObj) {
+ return ctx.getProperties({ objectId, ownProperties });
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Internal methods: the following methods are not part of CDP;
+ * note the _ prefix.
+ */
+
+ get _debugger() {
+ if (this.__debugger) {
+ return this.__debugger;
+ }
+ this.__debugger = new Debugger();
+ return this.__debugger;
+ }
+
+ _buildStackTrace(stack) {
+ const callFrames = [];
+
+ while (
+ stack &&
+ stack.source !== "debugger eval code" &&
+ !stack.source.startsWith("chrome://")
+ ) {
+ callFrames.push({
+ functionName: stack.functionDisplayName,
+ scriptId: stack.sourceId,
+ url: stack.source,
+ lineNumber: stack.line,
+ columnNumber: stack.column,
+ });
+ stack = stack.parent || stack.asyncParent;
+ }
+
+ return {
+ callFrames,
+ };
+ }
+
+ _getRemoteObject(objectId) {
+ for (const ctx of this.contexts.values()) {
+ const debuggerObj = ctx.getRemoteObject(objectId);
+ if (debuggerObj) {
+ return debuggerObj;
+ }
+ }
+ return null;
+ }
+
+ _serializeRemoteObject(debuggerObj, executionContextId) {
+ const ctx = this.contexts.get(executionContextId);
+ return ctx._toRemoteObject(debuggerObj);
+ }
+
+ _getRemoteObjectByNodeId(nodeId, executionContextId) {
+ let debuggerObj = null;
+
+ if (typeof executionContextId != "undefined") {
+ const ctx = this.contexts.get(executionContextId);
+ debuggerObj = ctx.getRemoteObjectByNodeId(nodeId);
+ } else {
+ for (const ctx of this.contexts.values()) {
+ const obj = ctx.getRemoteObjectByNodeId(nodeId);
+ if (obj) {
+ debuggerObj = obj;
+ break;
+ }
+ }
+ }
+
+ return debuggerObj;
+ }
+
+ _setRemoteObject(debuggerObj, context) {
+ return context.setRemoteObject(debuggerObj);
+ }
+
+ _getDefaultContextForWindow(innerWindowId) {
+ if (!innerWindowId) {
+ innerWindowId = this.content.windowGlobalChild.innerWindowId;
+ }
+ const curContexts = this.innerWindowIdToContexts.get(innerWindowId);
+ if (curContexts) {
+ for (const ctx of curContexts) {
+ if (ctx.isDefault) {
+ return ctx;
+ }
+ }
+ }
+ return null;
+ }
+
+ _getContextsForFrame(frameId) {
+ const frameContexts = [];
+ for (const ctx of this.contexts.values()) {
+ if (ctx.frameId == frameId) {
+ frameContexts.push(ctx);
+ }
+ }
+ return frameContexts;
+ }
+
+ _emitConsoleAPICalled(payload) {
+ // Filter out messages that aren't coming from a valid inner window, or from
+ // a different browser tab. Also messages of type "time", which are not
+ // getting reported by Chrome.
+ const curBrowserId = this.session.browsingContext.browserId;
+ const win = Services.wm.getCurrentInnerWindowWithId(payload.innerWindowId);
+ if (
+ !win ||
+ BrowsingContext.getFromWindow(win).browserId != curBrowserId ||
+ payload.type === "time"
+ ) {
+ return;
+ }
+
+ const context = this._getDefaultContextForWindow();
+ this.emit("Runtime.consoleAPICalled", {
+ args: payload.arguments.map(arg => context._toRemoteObject(arg)),
+ executionContextId: context?.id || 0,
+ timestamp: payload.timestamp,
+ type: payload.type,
+ stackTrace: this._buildStackTrace(payload.stack),
+ });
+ }
+
+ _emitExceptionThrown(payload) {
+ // Filter out messages that aren't coming from a valid inner window, or from
+ // a different browser tab. Also messages of type "time", which are not
+ // getting reported by Chrome.
+ const curBrowserId = this.session.browsingContext.browserId;
+ const win = Services.wm.getCurrentInnerWindowWithId(payload.innerWindowId);
+ if (!win || BrowsingContext.getFromWindow(win).browserId != curBrowserId) {
+ return;
+ }
+
+ const context = this._getDefaultContextForWindow();
+ this.emit("Runtime.exceptionThrown", {
+ timestamp: payload.timestamp,
+ exceptionDetails: {
+ // Temporary placeholder to return a number.
+ exceptionId: 0,
+ text: payload.text,
+ lineNumber: payload.lineNumber,
+ columnNumber: payload.columnNumber,
+ url: payload.url,
+ stackTrace: this._buildStackTrace(payload.stack),
+ executionContextId: context?.id || undefined,
+ },
+ });
+ }
+
+ /**
+ * Helper method in order to instantiate the ExecutionContext for a given
+ * DOM Window as well as emitting the related
+ * `Runtime.executionContextCreated` event
+ *
+ * @param {string} name
+ * Event name
+ * @param {Object=} options
+ * @param {number} options.windowId
+ * The inner window id of the newly instantiated document.
+ * @param {Window} options.window
+ * The window object of the newly instantiated document.
+ * @param {string=} options.contextName
+ * Human-readable name to describe the execution context.
+ * @param {boolean=} options.isDefault
+ * Whether the execution context is the default one.
+ * @param {string=} options.contextType
+ * "default" or "isolated"
+ *
+ * @return {number} ID of created context
+ *
+ */
+ _onContextCreated(name, options = {}) {
+ const {
+ windowId,
+ window,
+ contextName = "",
+ isDefault = true,
+ contextType = "default",
+ } = options;
+
+ if (windowId === undefined) {
+ throw new Error("windowId is required");
+ }
+
+ // allow only one default context per inner window
+ if (isDefault && this.innerWindowIdToContexts.has(windowId)) {
+ for (const ctx of this.innerWindowIdToContexts.get(windowId)) {
+ if (ctx.isDefault) {
+ return null;
+ }
+ }
+ }
+
+ const context = new ExecutionContext(
+ this._debugger,
+ window,
+ this.innerWindowIdToContexts.count,
+ isDefault
+ );
+ this.contexts.set(context.id, context);
+ this.innerWindowIdToContexts.set(windowId, context);
+
+ if (this.enabled) {
+ this.emit("Runtime.executionContextCreated", {
+ context: {
+ id: context.id,
+ origin: window.location.href,
+ name: contextName,
+ auxData: {
+ isDefault,
+ frameId: context.frameId,
+ type: contextType,
+ },
+ },
+ });
+ }
+
+ return context.id;
+ }
+
+ /**
+ * Helper method to destroy the ExecutionContext of the given id. Also emit
+ * the related `Runtime.executionContextDestroyed` and
+ * `Runtime.executionContextsCleared` events.
+ * ContextObserver will call this method with either `id` or `frameId` argument
+ * being set.
+ *
+ * @param {string} name
+ * Event name
+ * @param {Object=} options
+ * @param {number} id
+ * The execution context id to destroy.
+ * @param {number} windowId
+ * The inner-window id of the execution context to destroy.
+ * @param {number} frameId
+ * The frame id of execution context to destroy.
+ * Either `id` or `frameId` or `windowId` is passed.
+ */
+ _onContextDestroyed(name, { id, frameId, windowId }) {
+ let contexts;
+ if ([id, frameId, windowId].filter(id => !!id).length > 1) {
+ throw new Error("Expects only *one* of id, frameId, windowId");
+ }
+
+ if (id) {
+ contexts = [this.contexts.get(id)];
+ } else if (frameId) {
+ contexts = this._getContextsForFrame(frameId);
+ } else {
+ contexts = this.innerWindowIdToContexts.get(windowId) || [];
+ }
+
+ for (const ctx of contexts) {
+ const isFrame = !!BrowsingContext.get(ctx.frameId).parent;
+
+ ctx.destructor();
+ this.contexts.delete(ctx.id);
+ this.innerWindowIdToContexts.get(ctx.windowId).delete(ctx);
+
+ if (this.enabled) {
+ this.emit("Runtime.executionContextDestroyed", {
+ executionContextId: ctx.id,
+ });
+ }
+
+ if (this.innerWindowIdToContexts.get(ctx.windowId).size == 0) {
+ this.innerWindowIdToContexts.delete(ctx.windowId);
+ // Only emit when all the exeuction contexts were cleared for the
+ // current browser / target, which means it should only be emitted
+ // for a top-level browsing context reference.
+ if (this.enabled && !isFrame) {
+ this.emit("Runtime.executionContextsCleared");
+ }
+ }
+ }
+ }
+
+ // nsIObserver
+
+ /**
+ * Takes a console message belonging to the current window and emits a
+ * "exceptionThrown" event if it's a Javascript error, otherwise a
+ * "consoleAPICalled" event.
+ *
+ * @param {nsIConsoleMessage} message
+ * Console message.
+ */
+ observe(subject, topic, data) {
+ let entry;
+
+ if (topic == OBSERVER_CONSOLE_API) {
+ const message = subject.wrappedJSObject;
+ entry = fromConsoleAPI(message);
+ this._emitConsoleAPICalled(entry);
+ } else if (subject instanceof Ci.nsIScriptError && subject.hasException) {
+ entry = fromScriptError(subject);
+ this._emitExceptionThrown(entry);
+ }
+ }
+
+ // XPCOM
+
+ get QueryInterface() {
+ return ChromeUtils.generateQI(["nsIConsoleListener"]);
+ }
+}
+
+function fromConsoleAPI(message) {
+ // From sendConsoleAPIMessage (toolkit/modules/Console.jsm)
+ return {
+ arguments: message.arguments,
+ innerWindowId: message.innerID,
+ // TODO: Fetch the stack (Bug 1679981)
+ stack: undefined,
+ timestamp: message.timeStamp,
+ type: CONSOLE_API_LEVEL_MAP[message.level] || message.level,
+ };
+}
+
+function fromScriptError(error) {
+ // From dom/bindings/nsIScriptError.idl
+ return {
+ innerWindowId: error.innerWindowID,
+ columnNumber: error.columnNumber,
+ lineNumber: error.lineNumber,
+ stack: error.stack,
+ text: error.errorMessage,
+ timestamp: error.timeStamp,
+ url: error.sourceName,
+ };
+}
diff --git a/remote/domains/content/Security.jsm b/remote/domains/content/Security.jsm
new file mode 100644
index 0000000000..7131df4fa9
--- /dev/null
+++ b/remote/domains/content/Security.jsm
@@ -0,0 +1,38 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["Security"];
+
+const { ContentProcessDomain } = ChromeUtils.import(
+ "chrome://remote/content/domains/ContentProcessDomain.jsm"
+);
+
+class Security extends ContentProcessDomain {
+ constructor(session) {
+ super(session);
+ this.enabled = false;
+ }
+
+ destructor() {
+ this.disable();
+
+ super.destructor();
+ }
+
+ // commands
+
+ async enable() {
+ if (!this.enabled) {
+ this.enabled = true;
+ }
+ }
+
+ disable() {
+ if (this.enabled) {
+ this.enabled = false;
+ }
+ }
+}
diff --git a/remote/domains/content/runtime/ExecutionContext.jsm b/remote/domains/content/runtime/ExecutionContext.jsm
new file mode 100644
index 0000000000..7a04f3ae8f
--- /dev/null
+++ b/remote/domains/content/runtime/ExecutionContext.jsm
@@ -0,0 +1,538 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["ExecutionContext"];
+
+const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(
+ Ci.nsIUUIDGenerator
+);
+
+const TYPED_ARRAY_CLASSES = [
+ "Uint8Array",
+ "Uint8ClampedArray",
+ "Uint16Array",
+ "Uint32Array",
+ "Int8Array",
+ "Int16Array",
+ "Int32Array",
+ "Float32Array",
+ "Float64Array",
+];
+
+function uuid() {
+ return uuidGen
+ .generateUUID()
+ .toString()
+ .slice(1, -1);
+}
+
+/**
+ * This class represent a debuggable context onto which we can evaluate Javascript.
+ * This is typically a document, but it could also be a worker, an add-on, ... or
+ * any kind of context involving JS scripts.
+ *
+ * @param {Debugger} dbg
+ * A Debugger instance that we can use to inspect the given global.
+ * @param {GlobalObject} debuggee
+ * The debuggable context's global object. This is typically the document window
+ * object. But it can also be any global object, like a worker global scope object.
+ */
+class ExecutionContext {
+ constructor(dbg, debuggee, id, isDefault) {
+ this._debugger = dbg;
+ this._debuggee = this._debugger.addDebuggee(debuggee);
+
+ // Here, we assume that debuggee is a window object and we will propably have
+ // to adapt that once we cover workers or contexts that aren't a document.
+ this.window = debuggee;
+ this.windowId = this.window.windowGlobalChild.innerWindowId;
+ this.id = id;
+ this.frameId = this.window.browsingContext.id.toString();
+ this.isDefault = isDefault;
+
+ // objectId => Debugger.Object
+ this._remoteObjects = new Map();
+ }
+
+ destructor() {
+ this._debugger.removeDebuggee(this._debuggee);
+ }
+
+ get browsingContext() {
+ return this.window.browsingContext;
+ }
+
+ hasRemoteObject(objectId) {
+ return this._remoteObjects.has(objectId);
+ }
+
+ getRemoteObject(objectId) {
+ return this._remoteObjects.get(objectId);
+ }
+
+ getRemoteObjectByNodeId(nodeId) {
+ for (const value of this._remoteObjects.values()) {
+ if (value.nodeId == nodeId) {
+ return value;
+ }
+ }
+
+ return null;
+ }
+
+ releaseObject(objectId) {
+ return this._remoteObjects.delete(objectId);
+ }
+
+ /**
+ * Add a new debuggerObj to the object cache.
+ *
+ * Whenever an object is returned as reference, a new entry is added
+ * to the internal object cache. It means the same underlying object or node
+ * can be represented via multiple references.
+ */
+ setRemoteObject(debuggerObj) {
+ const objectId = uuid();
+
+ // TODO: Wrap Symbol into an object,
+ // which would allow us to set the objectId.
+ if (typeof debuggerObj == "object") {
+ debuggerObj.objectId = objectId;
+ }
+
+ // For node objects add an unique identifier.
+ if (
+ debuggerObj instanceof Debugger.Object &&
+ Node.isInstance(debuggerObj.unsafeDereference())
+ ) {
+ debuggerObj.nodeId = uuid();
+ // We do not differentiate between backendNodeId and nodeId (yet)
+ debuggerObj.backendNodeId = debuggerObj.nodeId;
+ }
+
+ this._remoteObjects.set(objectId, debuggerObj);
+
+ return objectId;
+ }
+
+ /**
+ * Evaluate a Javascript expression.
+ *
+ * @param {String} expression
+ * The JS expression to evaluate against the JS context.
+ * @param {boolean} options.awaitPromise
+ * Whether execution should `await` for resulting value
+ * and return once awaited promise is resolved.
+ * @param {boolean} returnByValue
+ * Whether the result is expected to be a JSON object
+ * that should be sent by value.
+ *
+ * @return {Object} A multi-form object depending if the execution
+ * succeed or failed. If the expression failed to evaluate,
+ * it will return an object with an `exceptionDetails` attribute
+ * matching the `ExceptionDetails` CDP type. Otherwise it will
+ * return an object with `result` attribute whose type is
+ * `RemoteObject` CDP type.
+ */
+ async evaluate(expression, awaitPromise, returnByValue) {
+ let rv = this._debuggee.executeInGlobal(expression);
+ if (!rv) {
+ return {
+ exceptionDetails: {
+ text: "Evaluation terminated!",
+ },
+ };
+ }
+
+ if (rv.throw) {
+ return this._returnError(rv.throw);
+ }
+
+ let result = rv.return;
+
+ if (result && result.isPromise && awaitPromise) {
+ if (result.promiseState === "fulfilled") {
+ result = result.promiseValue;
+ } else if (result.promiseState === "rejected") {
+ return this._returnError(result.promiseReason);
+ } else {
+ try {
+ const promiseResult = await result.unsafeDereference();
+ result = this._debuggee.makeDebuggeeValue(promiseResult);
+ } catch (e) {
+ // The promise has been rejected
+ return this._returnError(e);
+ }
+ }
+ }
+
+ if (returnByValue) {
+ result = this._toRemoteObjectByValue(result);
+ } else {
+ result = this._toRemoteObject(result);
+ }
+
+ return { result };
+ }
+
+ /**
+ * Given a Debugger.Object reference for an Exception, return a JSON object
+ * describing the exception by following CDP ExceptionDetails specification.
+ */
+ _returnError(exception) {
+ if (
+ this._debuggee.executeInGlobalWithBindings("exception instanceof Error", {
+ exception,
+ }).return
+ ) {
+ const text = this._debuggee.executeInGlobalWithBindings(
+ "exception.message",
+ { exception }
+ ).return;
+ return {
+ exceptionDetails: {
+ text,
+ },
+ };
+ }
+
+ // If that isn't an Error, consider the exception as a JS value
+ return {
+ exceptionDetails: {
+ exception: this._toRemoteObject(exception),
+ },
+ };
+ }
+
+ async callFunctionOn(
+ functionDeclaration,
+ callArguments = [],
+ returnByValue = false,
+ awaitPromise = false,
+ objectId = null
+ ) {
+ // Map the given objectId to a JS reference.
+ let thisArg = null;
+ if (objectId) {
+ thisArg = this.getRemoteObject(objectId);
+ if (!thisArg) {
+ throw new Error(`Unable to get target object with id: ${objectId}`);
+ }
+ }
+
+ // First evaluate the function
+ const fun = this._debuggee.executeInGlobal("(" + functionDeclaration + ")");
+ if (!fun) {
+ return {
+ exceptionDetails: {
+ text: "Evaluation terminated!",
+ },
+ };
+ }
+ if (fun.throw) {
+ return this._returnError(fun.throw);
+ }
+
+ // Then map all input arguments, which are matching CDP's CallArguments type,
+ // into JS values
+ const args = callArguments.map(arg => this._fromCallArgument(arg));
+
+ // Finally, call the function with these arguments
+ const rv = fun.return.apply(thisArg, args);
+ if (rv.throw) {
+ return this._returnError(rv.throw);
+ }
+
+ let result = rv.return;
+
+ if (result && result.isPromise && awaitPromise) {
+ if (result.promiseState === "fulfilled") {
+ result = result.promiseValue;
+ } else if (result.promiseState === "rejected") {
+ return this._returnError(result.promiseReason);
+ } else {
+ try {
+ const promiseResult = await result.unsafeDereference();
+ result = this._debuggee.makeDebuggeeValue(promiseResult);
+ } catch (e) {
+ // The promise has been rejected
+ return this._returnError(e);
+ }
+ }
+ }
+
+ if (returnByValue) {
+ result = this._toRemoteObjectByValue(result);
+ } else {
+ result = this._toRemoteObject(result);
+ }
+
+ return { result };
+ }
+
+ getProperties({ objectId, ownProperties }) {
+ let debuggerObj = this.getRemoteObject(objectId);
+ if (!debuggerObj) {
+ throw new Error("Could not find object with given id");
+ }
+
+ const result = [];
+ const serializeObject = (debuggerObj, isOwn) => {
+ for (const propertyName of debuggerObj.getOwnPropertyNames()) {
+ const descriptor = debuggerObj.getOwnPropertyDescriptor(propertyName);
+ result.push({
+ name: propertyName,
+
+ configurable: descriptor.configurable,
+ enumerable: descriptor.enumerable,
+ writable: descriptor.writable,
+ value: this._toRemoteObject(descriptor.value),
+ get: descriptor.get
+ ? this._toRemoteObject(descriptor.get)
+ : undefined,
+ set: descriptor.set
+ ? this._toRemoteObject(descriptor.set)
+ : undefined,
+
+ isOwn,
+ });
+ }
+ };
+
+ // When `ownProperties` is set to true, we only iterate over own properties.
+ // Otherwise, we also iterate over propreties inherited from the prototype chain.
+ serializeObject(debuggerObj, true);
+
+ if (!ownProperties) {
+ while (true) {
+ debuggerObj = debuggerObj.proto;
+ if (!debuggerObj) {
+ break;
+ }
+ serializeObject(debuggerObj, false);
+ }
+ }
+
+ return {
+ result,
+ };
+ }
+
+ /**
+ * Given a CDP `CallArgument`, return a JS value that represent this argument.
+ * Note that `CallArgument` is actually very similar to `RemoteObject`
+ */
+ _fromCallArgument(arg) {
+ if (arg.objectId) {
+ if (!this.hasRemoteObject(arg.objectId)) {
+ throw new Error("Could not find object with given id");
+ }
+ return this.getRemoteObject(arg.objectId);
+ }
+
+ if (arg.unserializableValue) {
+ switch (arg.unserializableValue) {
+ case "-0":
+ return -0;
+ case "Infinity":
+ return Infinity;
+ case "-Infinity":
+ return -Infinity;
+ case "NaN":
+ return NaN;
+ default:
+ if (/^\d+n$/.test(arg.unserializableValue)) {
+ // eslint-disable-next-line no-undef
+ return BigInt(arg.unserializableValue.slice(0, -1));
+ }
+ throw new Error("Couldn't parse value object in call argument");
+ }
+ }
+
+ return this._deserialize(arg.value);
+ }
+
+ /**
+ * Given a JS value, create a copy of it within the debugee compartment.
+ */
+ _deserialize(obj) {
+ if (typeof obj !== "object") {
+ return obj;
+ }
+ const result = this._debuggee.executeInGlobalWithBindings(
+ "JSON.parse(obj)",
+ { obj: JSON.stringify(obj) }
+ );
+ if (result.throw) {
+ throw new Error("Unable to deserialize object");
+ }
+ return result.return;
+ }
+
+ /**
+ * Given a `Debugger.Object` object, return a JSON-serializable description of it
+ * matching `RemoteObject` CDP type.
+ *
+ * @param {Debugger.Object} debuggerObj
+ * The object to serialize
+ * @return {RemoteObject}
+ * The serialized description of the given object
+ */
+ _toRemoteObject(debuggerObj) {
+ const result = {};
+
+ // First handle all non-primitive values which are going to be wrapped by the
+ // Debugger API into Debugger.Object instances
+ if (debuggerObj instanceof Debugger.Object) {
+ const rawObj = debuggerObj.unsafeDereference();
+
+ result.objectId = this.setRemoteObject(debuggerObj);
+ result.type = typeof rawObj;
+
+ // Map the Debugger API `class` attribute to CDP `subtype`
+ const cls = debuggerObj.class;
+ if (debuggerObj.isProxy) {
+ result.subtype = "proxy";
+ } else if (cls == "Array") {
+ result.subtype = "array";
+ } else if (cls == "RegExp") {
+ result.subtype = "regexp";
+ } else if (cls == "Date") {
+ result.subtype = "date";
+ } else if (cls == "Map") {
+ result.subtype = "map";
+ } else if (cls == "Set") {
+ result.subtype = "set";
+ } else if (cls == "WeakMap") {
+ result.subtype = "weakmap";
+ } else if (cls == "WeakSet") {
+ result.subtype = "weakset";
+ } else if (cls == "Error") {
+ result.subtype = "error";
+ } else if (cls == "Promise") {
+ result.subtype = "promise";
+ } else if (TYPED_ARRAY_CLASSES.includes(cls)) {
+ result.subtype = "typedarray";
+ } else if (Node.isInstance(rawObj)) {
+ result.subtype = "node";
+ result.className = ChromeUtils.getClassName(rawObj);
+ result.description = rawObj.localName || rawObj.nodeName;
+ if (rawObj.id) {
+ result.description += `#${rawObj.id}`;
+ }
+ }
+ return result;
+ }
+
+ // Now, handle all values that Debugger API isn't wrapping into Debugger.API.
+ // This is all the primitive JS types.
+ result.type = typeof debuggerObj;
+
+ // Symbol and BigInt are primitive values but aren't serializable.
+ // CDP expects them to be considered as objects, with an objectId to later inspect
+ // them.
+ if (result.type == "symbol") {
+ result.description = debuggerObj.toString();
+ result.objectId = this.setRemoteObject(debuggerObj);
+
+ return result;
+ }
+
+ // A few primitive type can't be serialized and CDP has special case for them
+ if (Object.is(debuggerObj, NaN)) {
+ result.unserializableValue = "NaN";
+ } else if (Object.is(debuggerObj, -0)) {
+ result.unserializableValue = "-0";
+ } else if (Object.is(debuggerObj, Infinity)) {
+ result.unserializableValue = "Infinity";
+ } else if (Object.is(debuggerObj, -Infinity)) {
+ result.unserializableValue = "-Infinity";
+ } else if (result.type == "bigint") {
+ result.unserializableValue = `${debuggerObj}n`;
+ }
+
+ if (result.unserializableValue) {
+ result.description = result.unserializableValue;
+ return result;
+ }
+
+ // Otherwise, we serialize the primitive values as-is via `value` attribute
+ result.value = debuggerObj;
+
+ // null is special as it has a dedicated subtype
+ if (debuggerObj === null) {
+ result.subtype = "null";
+ }
+
+ return result;
+ }
+
+ /**
+ * Given a `Debugger.Object` object, return a JSON-serializable description of it
+ * matching `RemoteObject` CDP type.
+ *
+ * @param {Debugger.Object} debuggerObj
+ * The object to serialize
+ * @return {RemoteObject}
+ * The serialized description of the given object
+ */
+ _toRemoteObjectByValue(debuggerObj) {
+ const type = typeof debuggerObj;
+
+ if (type == "undefined") {
+ return { type };
+ }
+
+ let unserializableValue = undefined;
+ if (Object.is(debuggerObj, -0)) {
+ unserializableValue = "-0";
+ } else if (Object.is(debuggerObj, NaN)) {
+ unserializableValue = "NaN";
+ } else if (Object.is(debuggerObj, Infinity)) {
+ unserializableValue = "Infinity";
+ } else if (Object.is(debuggerObj, -Infinity)) {
+ unserializableValue = "-Infinity";
+ } else if (typeof debuggerObj == "bigint") {
+ unserializableValue = `${debuggerObj}n`;
+ }
+
+ if (unserializableValue) {
+ return {
+ type,
+ unserializableValue,
+ description: unserializableValue,
+ };
+ }
+
+ const value = this._serialize(debuggerObj);
+ return {
+ type: typeof value,
+ value,
+ description: value != null ? value.toString() : value,
+ };
+ }
+
+ /**
+ * Convert a given `Debugger.Object` to an object.
+ *
+ * @param {Debugger.Object} obj
+ * The object to convert
+ *
+ * @return {Object}
+ * The converted object
+ */
+ _serialize(debuggerObj) {
+ const result = this._debuggee.executeInGlobalWithBindings(
+ "JSON.stringify(e)",
+ { e: debuggerObj }
+ );
+ if (result.throw) {
+ throw new Error("Object is not serializable");
+ }
+
+ return JSON.parse(result.return);
+ }
+}