summaryrefslogtreecommitdiffstats
path: root/remote/cdp/domains
diff options
context:
space:
mode:
Diffstat (limited to 'remote/cdp/domains')
-rw-r--r--remote/cdp/domains/ContentProcessDomain.sys.mjs25
-rw-r--r--remote/cdp/domains/ContentProcessDomains.sys.mjs19
-rw-r--r--remote/cdp/domains/Domain.sys.mjs71
-rw-r--r--remote/cdp/domains/DomainCache.sys.mjs113
-rw-r--r--remote/cdp/domains/ParentProcessDomains.sys.mjs18
-rw-r--r--remote/cdp/domains/content/DOM.sys.mjs246
-rw-r--r--remote/cdp/domains/content/Emulation.sys.mjs50
-rw-r--r--remote/cdp/domains/content/Input.sys.mjs57
-rw-r--r--remote/cdp/domains/content/Log.sys.mjs86
-rw-r--r--remote/cdp/domains/content/Network.sys.mjs18
-rw-r--r--remote/cdp/domains/content/Page.sys.mjs451
-rw-r--r--remote/cdp/domains/content/Performance.sys.mjs32
-rw-r--r--remote/cdp/domains/content/Runtime.sys.mjs629
-rw-r--r--remote/cdp/domains/content/Security.sys.mjs32
-rw-r--r--remote/cdp/domains/content/runtime/ExecutionContext.sys.mjs530
-rw-r--r--remote/cdp/domains/parent/Browser.sys.mjs40
-rw-r--r--remote/cdp/domains/parent/Emulation.sys.mjs177
-rw-r--r--remote/cdp/domains/parent/Fetch.sys.mjs30
-rw-r--r--remote/cdp/domains/parent/IO.sys.mjs114
-rw-r--r--remote/cdp/domains/parent/Input.sys.mjs168
-rw-r--r--remote/cdp/domains/parent/Network.sys.mjs538
-rw-r--r--remote/cdp/domains/parent/Page.sys.mjs775
-rw-r--r--remote/cdp/domains/parent/Security.sys.mjs58
-rw-r--r--remote/cdp/domains/parent/Target.sys.mjs193
-rw-r--r--remote/cdp/domains/parent/page/DialogHandler.sys.mjs140
25 files changed, 4610 insertions, 0 deletions
diff --git a/remote/cdp/domains/ContentProcessDomain.sys.mjs b/remote/cdp/domains/ContentProcessDomain.sys.mjs
new file mode 100644
index 0000000000..fefe6aece5
--- /dev/null
+++ b/remote/cdp/domains/ContentProcessDomain.sys.mjs
@@ -0,0 +1,25 @@
+/* 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/. */
+
+import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs";
+
+export class ContentProcessDomain extends Domain {
+ destructor() {
+ super.destructor();
+ }
+
+ // helpers
+
+ get content() {
+ return this.session.content;
+ }
+
+ get docShell() {
+ return this.session.docShell;
+ }
+
+ get chromeEventHandler() {
+ return this.docShell.chromeEventHandler;
+ }
+}
diff --git a/remote/cdp/domains/ContentProcessDomains.sys.mjs b/remote/cdp/domains/ContentProcessDomains.sys.mjs
new file mode 100644
index 0000000000..a434dc0067
--- /dev/null
+++ b/remote/cdp/domains/ContentProcessDomains.sys.mjs
@@ -0,0 +1,19 @@
+/* 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/. */
+
+export const ContentProcessDomains = {};
+
+// eslint-disable-next-line mozilla/lazy-getter-object-name
+ChromeUtils.defineESModuleGetters(ContentProcessDomains, {
+ DOM: "chrome://remote/content/cdp/domains/content/DOM.sys.mjs",
+ Emulation: "chrome://remote/content/cdp/domains/content/Emulation.sys.mjs",
+ Input: "chrome://remote/content/cdp/domains/content/Input.sys.mjs",
+ Log: "chrome://remote/content/cdp/domains/content/Log.sys.mjs",
+ Network: "chrome://remote/content/cdp/domains/content/Network.sys.mjs",
+ Page: "chrome://remote/content/cdp/domains/content/Page.sys.mjs",
+ Performance:
+ "chrome://remote/content/cdp/domains/content/Performance.sys.mjs",
+ Runtime: "chrome://remote/content/cdp/domains/content/Runtime.sys.mjs",
+ Security: "chrome://remote/content/cdp/domains/content/Security.sys.mjs",
+});
diff --git a/remote/cdp/domains/Domain.sys.mjs b/remote/cdp/domains/Domain.sys.mjs
new file mode 100644
index 0000000000..e20f9aadbd
--- /dev/null
+++ b/remote/cdp/domains/Domain.sys.mjs
@@ -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/. */
+
+export class Domain {
+ constructor(session) {
+ this.session = session;
+ this.name = this.constructor.name;
+
+ this.eventListeners_ = new Set();
+ this._requestCounter = 0;
+ }
+
+ destructor() {}
+
+ emit(eventName, params = {}) {
+ for (const listener of this.eventListeners_) {
+ try {
+ if (isEventHandler(listener)) {
+ listener.onEvent(eventName, params);
+ } else {
+ listener.call(this, eventName, params);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+
+ /**
+ * Execute the provided method in the child domain that has the same domain
+ * name. eg. calling this.executeInChild from domains/parent/Input.jsm will
+ * attempt to execute the method in domains/content/Input.jsm.
+ *
+ * This can only be called from parent domains managed by a TabSession.
+ *
+ * @param {String} method
+ * Name of the method to call on the child domain.
+ * @param {Object} params
+ * Optional parameters. Must be serializable.
+ */
+ executeInChild(method, params) {
+ if (!this.session.executeInChild) {
+ throw new Error(
+ "executeInChild can only be used in Domains managed by a TabSession"
+ );
+ }
+ this._requestCounter++;
+ const id = this.name + "-" + this._requestCounter;
+ return this.session.executeInChild(id, this.name, method, params);
+ }
+
+ addEventListener(listener) {
+ if (typeof listener != "function" && !isEventHandler(listener)) {
+ throw new TypeError();
+ }
+ this.eventListeners_.add(listener);
+ }
+
+ // static
+
+ static implements(command) {
+ return command && typeof this.prototype[command] == "function";
+ }
+}
+
+function isEventHandler(listener) {
+ return (
+ listener && "onEvent" in listener && typeof listener.onEvent == "function"
+ );
+}
diff --git a/remote/cdp/domains/DomainCache.sys.mjs b/remote/cdp/domains/DomainCache.sys.mjs
new file mode 100644
index 0000000000..b4651cbe68
--- /dev/null
+++ b/remote/cdp/domains/DomainCache.sys.mjs
@@ -0,0 +1,113 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Domain: "chrome://remote/content/cdp/domains/Domain.sys.mjs",
+ UnknownMethodError: "chrome://remote/content/cdp/Error.sys.mjs",
+});
+
+/**
+ * Lazy domain instance cache.
+ *
+ * Domains are loaded into each target's realm, and consequently
+ * there exists one domain cache per realm. Domains are preregistered
+ * with this cache and then constructed lazily upon request.
+ *
+ * @param {Session} session
+ * Session that domains should be associated with as they
+ * are constructed.
+ * @param {Map.<string, string>} modules
+ * Table defining JS modules available to this domain cache.
+ * This should be a mapping between domain name
+ * and JS module path passed to ChromeUtils.import.
+ */
+export class DomainCache {
+ constructor(session, modules) {
+ this.session = session;
+ this.modules = modules;
+ this.instances = new Map();
+ }
+
+ /** Test if domain supports method. */
+ domainSupportsMethod(name, method) {
+ const domain = this.modules[name];
+ if (domain) {
+ return domain.implements(method);
+ }
+ return false;
+ }
+
+ /**
+ * Gets the current instance of the domain, or creates a new one,
+ * and associates it with the predefined session.
+ *
+ * @throws {UnknownMethodError}
+ * If domain is not preregistered with this domain cache.
+ */
+ get(name) {
+ let inst = this.instances.get(name);
+ if (!inst) {
+ const Cls = this.modules[name];
+ if (!Cls) {
+ throw new lazy.UnknownMethodError(name);
+ }
+ if (!isConstructor(Cls)) {
+ throw new TypeError("Domain cannot be constructed");
+ }
+
+ inst = new Cls(this.session);
+ if (!(inst instanceof lazy.Domain)) {
+ throw new TypeError("Instance not a domain");
+ }
+
+ inst.addEventListener(this.session);
+
+ this.instances.set(name, inst);
+ }
+
+ return inst;
+ }
+
+ /**
+ * Tells if a Domain of the given name is available
+ */
+ has(name) {
+ return name in this.modules;
+ }
+
+ get size() {
+ return this.instances.size;
+ }
+
+ /**
+ * Execute the given command (function) of a given domain with the given parameters.
+ * If the command doesn't exists, it will throw.
+ * It returns the returned value of the command, which is most likely a promise.
+ */
+ execute(domain, command, params) {
+ if (!this.domainSupportsMethod(domain, command)) {
+ throw new lazy.UnknownMethodError(domain, command);
+ }
+ const inst = this.get(domain);
+ return inst[command](params);
+ }
+
+ /** Calls destructor on each domain and clears the cache. */
+ clear() {
+ for (const inst of this.instances.values()) {
+ inst.destructor();
+ }
+ this.instances.clear();
+ }
+
+ toString() {
+ return `[object DomainCache ${this.size}]`;
+ }
+}
+
+function isConstructor(obj) {
+ return !!obj.prototype && !!obj.prototype.constructor.name;
+}
diff --git a/remote/cdp/domains/ParentProcessDomains.sys.mjs b/remote/cdp/domains/ParentProcessDomains.sys.mjs
new file mode 100644
index 0000000000..48121980c1
--- /dev/null
+++ b/remote/cdp/domains/ParentProcessDomains.sys.mjs
@@ -0,0 +1,18 @@
+/* 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/. */
+
+export const ParentProcessDomains = {};
+
+// eslint-disable-next-line mozilla/lazy-getter-object-name
+ChromeUtils.defineESModuleGetters(ParentProcessDomains, {
+ Browser: "chrome://remote/content/cdp/domains/parent/Browser.sys.mjs",
+ Emulation: "chrome://remote/content/cdp/domains/parent/Emulation.sys.mjs",
+ Fetch: "chrome://remote/content/cdp/domains/parent/Fetch.sys.mjs",
+ Input: "chrome://remote/content/cdp/domains/parent/Input.sys.mjs",
+ IO: "chrome://remote/content/cdp/domains/parent/IO.sys.mjs",
+ Network: "chrome://remote/content/cdp/domains/parent/Network.sys.mjs",
+ Page: "chrome://remote/content/cdp/domains/parent/Page.sys.mjs",
+ Security: "chrome://remote/content/cdp/domains/parent/Security.sys.mjs",
+ Target: "chrome://remote/content/cdp/domains/parent/Target.sys.mjs",
+});
diff --git a/remote/cdp/domains/content/DOM.sys.mjs b/remote/cdp/domains/content/DOM.sys.mjs
new file mode 100644
index 0000000000..8f920756e6
--- /dev/null
+++ b/remote/cdp/domains/content/DOM.sys.mjs
@@ -0,0 +1,246 @@
+/* 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/. */
+
+import { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs";
+
+export 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 (HTMLIFrameElement.isInstance(unsafeObj)) {
+ 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 ? attributes : undefined,
+ frameId: context.id.toString(),
+ };
+
+ return { node };
+ }
+
+ disable() {
+ if (this.enabled) {
+ this.enabled = false;
+ }
+ }
+
+ getContentQuads(options = {}) {
+ const { objectId } = options;
+ 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(options = {}) {
+ const { objectId } = options;
+ 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/cdp/domains/content/Emulation.sys.mjs b/remote/cdp/domains/content/Emulation.sys.mjs
new file mode 100644
index 0000000000..41bb0c76ea
--- /dev/null
+++ b/remote/cdp/domains/content/Emulation.sys.mjs
@@ -0,0 +1,50 @@
+/* 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/. */
+
+import { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AnimationFramePromise: "chrome://remote/content/shared/Sync.sys.mjs",
+});
+
+export 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;
+ let resized;
+
+ // Updates for background tabs are throttled, and we also we have to make
+ // sure that the new browser dimensions have been received by the content
+ // process. As such wait for the next animation frame.
+ await lazy.AnimationFramePromise(win);
+
+ const checkBrowserSize = () => {
+ if (win.innerWidth === width && win.innerHeight === height) {
+ resized();
+ }
+ };
+
+ return new Promise(resolve => {
+ resized = resolve;
+
+ win.addEventListener("resize", checkBrowserSize);
+
+ // Trigger a layout flush in case none happened yet.
+ checkBrowserSize();
+ }).finally(() => {
+ win.removeEventListener("resize", checkBrowserSize);
+ });
+ }
+}
diff --git a/remote/cdp/domains/content/Input.sys.mjs b/remote/cdp/domains/content/Input.sys.mjs
new file mode 100644
index 0000000000..e7a6ffc709
--- /dev/null
+++ b/remote/cdp/domains/content/Input.sys.mjs
@@ -0,0 +1,57 @@
+/* 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/. */
+
+import { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs";
+
+export 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(resolve => {
+ this.chromeEventHandler.addEventListener(eventName, resolve, {
+ 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);
+ }
+}
diff --git a/remote/cdp/domains/content/Log.sys.mjs b/remote/cdp/domains/content/Log.sys.mjs
new file mode 100644
index 0000000000..f8c561d429
--- /dev/null
+++ b/remote/cdp/domains/content/Log.sys.mjs
@@ -0,0 +1,86 @@
+/* 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/. */
+
+import { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs";
+
+const CONSOLE_MESSAGE_LEVEL_MAP = {
+ [Ci.nsIConsoleMessage.debug]: "verbose",
+ [Ci.nsIConsoleMessage.info]: "info",
+ [Ci.nsIConsoleMessage.warn]: "warning",
+ [Ci.nsIConsoleMessage.error]: "error",
+};
+
+export 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/cdp/domains/content/Network.sys.mjs b/remote/cdp/domains/content/Network.sys.mjs
new file mode 100644
index 0000000000..91dc44cf46
--- /dev/null
+++ b/remote/cdp/domains/content/Network.sys.mjs
@@ -0,0 +1,18 @@
+/* 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/. */
+
+import { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs";
+
+export 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/cdp/domains/content/Page.sys.mjs b/remote/cdp/domains/content/Page.sys.mjs
new file mode 100644
index 0000000000..5f833a9102
--- /dev/null
+++ b/remote/cdp/domains/content/Page.sys.mjs
@@ -0,0 +1,451 @@
+/* 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/. */
+
+import { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs";
+
+const {
+ LOAD_FLAGS_BYPASS_CACHE,
+ LOAD_FLAGS_BYPASS_PROXY,
+ LOAD_FLAGS_NONE,
+} = Ci.nsIWebNavigation;
+
+export 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("frame-attached", this._onFrameAttached);
+ this.session.contextObserver.on("frame-detached", 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("hashchange", this, {
+ mozSystemGroup: true,
+ capture: 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("frame-attached", this._onFrameAttached);
+ this.session.contextObserver.off("frame-detached", 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("hashchange", this, {
+ mozSystemGroup: true,
+ capture: true,
+ });
+ this.chromeEventHandler.removeEventListener("load", this, {
+ mozSystemGroup: true,
+ capture: true,
+ });
+ this.chromeEventHandler.removeEventListener("pageshow", this, {
+ mozSystemGroup: true,
+ });
+ this.enabled = false;
+ }
+ }
+
+ async reload(options = {}) {
+ const { ignoreCache } = options;
+ 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) {
+ 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 = Services.uuid
+ .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) {
+ 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, { frameId, window }) {
+ const bc = BrowsingContext.get(frameId);
+
+ // 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: frameId.toString(),
+ parentFrameId: bc.parent.id.toString(),
+ stack: null,
+ });
+
+ // Usually both events are emitted when the "pagehide" event is received.
+ // But this wont happen for a new window or frame when the initial
+ // about:blank page has already loaded, and is being replaced with the
+ // final document.
+ if (!window.document.isInitialDocument) {
+ this.emit("Page.frameStartedLoading", { frameId: frameId.toString() });
+
+ const loaderId = this.frameIdToLoaderId.get(frameId);
+ const timestamp = Date.now() / 1000;
+ this.emitLifecycleEvent(frameId, loaderId, "init", timestamp);
+ }
+ }
+
+ _onFrameDetached(name, { frameId }) {
+ const bc = BrowsingContext.get(frameId);
+
+ // 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: frameId.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;
+
+ // Some events such as "hashchange" use the window as the target, while
+ // others have a document.
+ const win = Window.isInstance(target) ? target : target.defaultView;
+ const frameId = win.docShell.browsingContext.id;
+ const isFrame = !!win.docShell.browsingContext.parent;
+ const loaderId = this.frameIdToLoaderId.get(frameId);
+ const url = win.location.href;
+
+ switch (type) {
+ case "DOMContentLoaded":
+ if (!isFrame) {
+ this.emit("Page.domContentEventFired", { timestamp });
+ }
+ this.emitLifecycleEvent(
+ frameId,
+ loaderId,
+ "DOMContentLoaded",
+ timestamp
+ );
+ break;
+
+ case "hashchange":
+ this.emit("Page.navigatedWithinDocument", {
+ frameId: frameId.toString(),
+ url,
+ });
+ 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);
+
+ // 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.browsingContext.overrideDPPX || 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/cdp/domains/content/Performance.sys.mjs b/remote/cdp/domains/content/Performance.sys.mjs
new file mode 100644
index 0000000000..e5726725b5
--- /dev/null
+++ b/remote/cdp/domains/content/Performance.sys.mjs
@@ -0,0 +1,32 @@
+/* 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/. */
+
+import { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs";
+
+export 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/cdp/domains/content/Runtime.sys.mjs b/remote/cdp/domains/content/Runtime.sys.mjs
new file mode 100644
index 0000000000..0f3ab16d2a
--- /dev/null
+++ b/remote/cdp/domains/content/Runtime.sys.mjs
@@ -0,0 +1,629 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+import { addDebuggerToGlobal } from "resource://gre/modules/jsdebugger.sys.mjs";
+
+import { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ executeSoon: "chrome://remote/content/shared/Sync.sys.mjs",
+ ExecutionContext:
+ "chrome://remote/content/cdp/domains/content/runtime/ExecutionContext.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "ConsoleAPIStorage", () => {
+ return Cc["@mozilla.org/consoleAPI-storage;1"].getService(
+ Ci.nsIConsoleAPIStorage
+ );
+});
+
+// Import the `Debugger` constructor in the current scope
+// eslint-disable-next-line mozilla/reject-globalThis-modification
+addDebuggerToGlobal(globalThis);
+
+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;
+ }
+}
+
+export 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);
+ this.onConsoleLogEvent = this.onConsoleLogEvent.bind(this);
+ lazy.ConsoleAPIStorage.addLogEventListener(
+ this.onConsoleLogEvent,
+ Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal)
+ );
+
+ // Spin the event loop in order to send the `executionContextCreated` event right
+ // after we replied to `enable` request.
+ lazy.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);
+ lazy.ConsoleAPIStorage.removeLogEventListener(this.onConsoleLogEvent);
+ }
+ }
+
+ releaseObject(options = {}) {
+ const { objectId } = options;
+
+ 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(options = {}) {
+ const { objectId, ownProperties } = options;
+
+ 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.toString(),
+ url: stack.source,
+ lineNumber: stack.line - 1,
+ columnNumber: stack.column - 1,
+ });
+ 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 lazy.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.origin,
+ 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");
+ }
+ }
+ }
+ }
+
+ onConsoleLogEvent(message) {
+ let entry = fromConsoleAPI(message);
+ this._emitConsoleAPICalled(entry);
+ }
+
+ // 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) {
+ if (subject instanceof Ci.nsIScriptError && subject.hasException) {
+ let entry = fromScriptError(subject);
+ this._emitExceptionThrown(entry);
+ }
+ }
+
+ // XPCOM
+
+ get QueryInterface() {
+ return ChromeUtils.generateQI(["nsIConsoleListener"]);
+ }
+}
+
+function fromConsoleAPI(message) {
+ // From sendConsoleAPIMessage (toolkit/modules/Console.sys.mjs)
+ 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 - 1,
+ lineNumber: error.lineNumber - 1,
+ stack: error.stack,
+ text: error.errorMessage,
+ timestamp: error.timeStamp,
+ url: error.sourceName,
+ };
+}
diff --git a/remote/cdp/domains/content/Security.sys.mjs b/remote/cdp/domains/content/Security.sys.mjs
new file mode 100644
index 0000000000..7d21d386b4
--- /dev/null
+++ b/remote/cdp/domains/content/Security.sys.mjs
@@ -0,0 +1,32 @@
+/* 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/. */
+
+import { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs";
+
+export 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/cdp/domains/content/runtime/ExecutionContext.sys.mjs b/remote/cdp/domains/content/runtime/ExecutionContext.sys.mjs
new file mode 100644
index 0000000000..8e5a496770
--- /dev/null
+++ b/remote/cdp/domains/content/runtime/ExecutionContext.sys.mjs
@@ -0,0 +1,530 @@
+/* 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/. */
+
+const TYPED_ARRAY_CLASSES = [
+ "Uint8Array",
+ "Uint8ClampedArray",
+ "Uint16Array",
+ "Uint32Array",
+ "Int8Array",
+ "Int16Array",
+ "Int32Array",
+ "Float32Array",
+ "Float64Array",
+];
+
+function uuid() {
+ return Services.uuid
+ .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.
+ */
+export 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;
+ 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);
+ }
+}
diff --git a/remote/cdp/domains/parent/Browser.sys.mjs b/remote/cdp/domains/parent/Browser.sys.mjs
new file mode 100644
index 0000000000..ecf93d4d8d
--- /dev/null
+++ b/remote/cdp/domains/parent/Browser.sys.mjs
@@ -0,0 +1,40 @@
+/* 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/. */
+
+import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs";
+
+export class Browser extends Domain {
+ getVersion() {
+ const { isHeadless } = Cc["@mozilla.org/gfx/info;1"].getService(
+ Ci.nsIGfxInfo
+ );
+ const { userAgent } = Cc[
+ "@mozilla.org/network/protocol;1?name=http"
+ ].getService(Ci.nsIHttpProtocolHandler);
+ return {
+ jsVersion: Services.appinfo.version,
+ protocolVersion: "1.3",
+ product:
+ (isHeadless ? "Headless" : "") +
+ `${Services.appinfo.name}/${Services.appinfo.version}`,
+ revision: Services.appinfo.sourceURL.split("/").pop(),
+ userAgent,
+ };
+ }
+
+ close() {
+ // Notify all windows that an application quit has been requested.
+ const cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested");
+
+ // If the shutdown of the application is prevented force quit it instead.
+ const mode = cancelQuit.data
+ ? Ci.nsIAppStartup.eForceQuit
+ : Ci.nsIAppStartup.eAttemptQuit;
+
+ Services.startup.quit(mode);
+ }
+}
diff --git a/remote/cdp/domains/parent/Emulation.sys.mjs b/remote/cdp/domains/parent/Emulation.sys.mjs
new file mode 100644
index 0000000000..f5ce827f0e
--- /dev/null
+++ b/remote/cdp/domains/parent/Emulation.sys.mjs
@@ -0,0 +1,177 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+});
+
+const MAX_WINDOW_SIZE = 10000000;
+
+export class Emulation extends Domain {
+ destructor() {
+ this.setUserAgentOverride({ userAgent: "", platform: "" });
+
+ super.destructor();
+ }
+
+ /**
+ * Overrides the values of device screen dimensions.
+ *
+ * Values as modified are:
+ * - window.screen.width
+ * - window.screen.height
+ * - window.innerWidth
+ * - window.innerHeight
+ * - "device-width"/"device-height"-related CSS media query results
+ *
+ * @param {Object} options
+ * @param {number} options.width
+ * Overriding width value in pixels. 0 disables the override.
+ * @param {number} options.height
+ * Overriding height value in pixels. 0 disables the override.
+ * @param {number} options.deviceScaleFactor
+ * Overriding device scale factor value. 0 disables the override.
+ * @param {number} options.mobile [not supported]
+ * Whether to emulate a mobile device. This includes viewport meta tag,
+ * overlay scrollbars, text autosizing and more.
+ * @param {number} options.screenOrientation
+ * Screen orientation override [not supported]
+ */
+ async setDeviceMetricsOverride(options = {}) {
+ const { width, height, deviceScaleFactor } = options;
+
+ if (
+ width < 0 ||
+ width > MAX_WINDOW_SIZE ||
+ height < 0 ||
+ height > MAX_WINDOW_SIZE
+ ) {
+ throw new TypeError(
+ `Width and height values must be positive, not greater than ${MAX_WINDOW_SIZE}`
+ );
+ }
+
+ if (typeof deviceScaleFactor != "number") {
+ throw new TypeError("deviceScaleFactor: number expected");
+ }
+
+ if (deviceScaleFactor < 0) {
+ throw new TypeError("deviceScaleFactor: must be positive");
+ }
+
+ const { tab } = this.session.target;
+ const { linkedBrowser: browser } = tab;
+
+ const { browsingContext } = this.session.target;
+ browsingContext.overrideDPPX = deviceScaleFactor;
+
+ // With a value of 0 the current size is used
+ const { layoutViewport } = await this.session.execute(
+ this.session.id,
+ "Page",
+ "getLayoutMetrics"
+ );
+
+ const targetWidth = width > 0 ? width : layoutViewport.clientWidth;
+ const targetHeight = height > 0 ? height : layoutViewport.clientHeight;
+
+ browser.style.setProperty("min-width", targetWidth + "px");
+ browser.style.setProperty("max-width", targetWidth + "px");
+ browser.style.setProperty("min-height", targetHeight + "px");
+ browser.style.setProperty("max-height", targetHeight + "px");
+
+ // Wait until the viewport has been resized
+ await this.executeInChild("_awaitViewportDimensions", {
+ width: targetWidth,
+ height: targetHeight,
+ });
+ }
+
+ /**
+ * Enables touch on platforms which do not support them.
+ *
+ * @param {Object} options
+ * @param {boolean} options.enabled
+ * Whether the touch event emulation should be enabled.
+ * @param {number=} options.maxTouchPoints [not yet supported]
+ * Maximum touch points supported. Defaults to one.
+ */
+ async setTouchEmulationEnabled(options = {}) {
+ const { enabled } = options;
+
+ if (typeof enabled != "boolean") {
+ throw new TypeError(
+ "Invalid parameters (enabled: boolean value expected)"
+ );
+ }
+
+ const { browsingContext } = this.session.target;
+ if (enabled) {
+ browsingContext.touchEventsOverride = "enabled";
+ } else {
+ browsingContext.touchEventsOverride = "none";
+ }
+ }
+
+ /**
+ * Allows overriding user agent with the given string.
+ *
+ * @param {Object} options
+ * @param {string} options.userAgent
+ * User agent to use.
+ * @param {string=} options.acceptLanguage [not yet supported]
+ * Browser langugage to emulate.
+ * @param {string=} options.platform
+ * The platform navigator.platform should return.
+ */
+ async setUserAgentOverride(options = {}) {
+ const { userAgent, platform } = options;
+
+ if (typeof userAgent != "string") {
+ throw new TypeError(
+ "Invalid parameters (userAgent: string value expected)"
+ );
+ }
+
+ if (!["undefined", "string"].includes(typeof platform)) {
+ throw new TypeError("platform: string value expected");
+ }
+
+ const { browsingContext } = this.session.target;
+
+ if (!userAgent.length) {
+ browsingContext.customUserAgent = null;
+ } else if (this._isValidHTTPRequestHeaderValue(userAgent)) {
+ browsingContext.customUserAgent = userAgent;
+ } else {
+ throw new TypeError("Invalid characters found in userAgent");
+ }
+
+ if (platform?.length > 0) {
+ browsingContext.customPlatform = platform;
+ } else {
+ browsingContext.customPlatform = null;
+ }
+ }
+
+ _isValidHTTPRequestHeaderValue(value) {
+ try {
+ const channel = lazy.NetUtil.newChannel({
+ uri: "http://localhost",
+ loadUsingSystemPrincipal: true,
+ });
+ channel.QueryInterface(Ci.nsIHttpChannel);
+ channel.setRequestHeader("X-check", value, false);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+}
diff --git a/remote/cdp/domains/parent/Fetch.sys.mjs b/remote/cdp/domains/parent/Fetch.sys.mjs
new file mode 100644
index 0000000000..39e6965ccd
--- /dev/null
+++ b/remote/cdp/domains/parent/Fetch.sys.mjs
@@ -0,0 +1,30 @@
+/* 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/. */
+
+import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs";
+
+// Note: For now this domain has only been added so that clients using CDP
+// (like Selenium) don't break when trying to disable Fetch events.
+
+export class Fetch extends Domain {
+ constructor(session) {
+ super(session);
+
+ this.enabled = false;
+ }
+
+ destructor() {
+ this.disable();
+
+ super.destructor();
+ }
+
+ disable() {
+ if (!this.enabled) {
+ return;
+ }
+
+ this.enabled = false;
+ }
+}
diff --git a/remote/cdp/domains/parent/IO.sys.mjs b/remote/cdp/domains/parent/IO.sys.mjs
new file mode 100644
index 0000000000..f0877d017f
--- /dev/null
+++ b/remote/cdp/domains/parent/IO.sys.mjs
@@ -0,0 +1,114 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs";
+import { StreamRegistry } from "chrome://remote/content/cdp/StreamRegistry.sys.mjs";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ OS: "resource://gre/modules/osfile.jsm",
+});
+
+const DEFAULT_CHUNK_SIZE = 10 * 1024 * 1024;
+
+// Global singleton for managing open streams
+export const streamRegistry = new StreamRegistry();
+
+export class IO extends Domain {
+ // commands
+
+ /**
+ * Close the stream, discard any temporary backing storage.
+ *
+ * @param {Object} options
+ * @param {string} options.handle
+ * Handle of the stream to close.
+ */
+ async close(options = {}) {
+ const { handle } = options;
+
+ if (typeof handle != "string") {
+ throw new TypeError(`handle: string value expected`);
+ }
+
+ await streamRegistry.remove(handle);
+ }
+
+ /**
+ * Read a chunk of the stream.
+ *
+ * @param {Object} options
+ * @param {string} options.handle
+ * Handle of the stream to read.
+ * @param {number=} options.offset
+ * Seek to the specified offset before reading. If not specificed,
+ * proceed with offset following the last read.
+ * Some types of streams may only support sequential reads.
+ * @param {number=} options.size
+ * Maximum number of bytes to read (left upon the agent
+ * discretion if not specified).
+ *
+ * @return {string, boolean, boolean}
+ * Data that were read, including flags for base64-encoded, and end-of-file reached.
+ */
+ async read(options = {}) {
+ const { handle, offset, size } = options;
+
+ if (typeof handle != "string") {
+ throw new TypeError(`handle: string value expected`);
+ }
+
+ const stream = streamRegistry.get(handle);
+ const fileInfo = await stream.stat();
+
+ if (typeof offset != "undefined") {
+ if (typeof offset != "number") {
+ throw new TypeError(`offset: integer value expected`);
+ }
+
+ // To keep compatibility with Chrome clip invalid offsets
+ const seekTo = Math.max(0, Math.min(offset, fileInfo.size));
+ await stream.setPosition(seekTo, lazy.OS.File.POS_START);
+ }
+
+ const curPos = await stream.getPosition();
+ const remainingBytes = fileInfo.size - curPos;
+
+ let chunkSize;
+ if (typeof size != "undefined") {
+ if (typeof size != "number") {
+ throw new TypeError(`size: integer value expected`);
+ }
+
+ // Chromium currently crashes for negative sizes (https://bit.ly/2P6h0Fv),
+ // but might behave similar to the offset and clip invalid values
+ chunkSize = Math.max(0, Math.min(size, remainingBytes));
+ } else {
+ chunkSize = Math.min(DEFAULT_CHUNK_SIZE, remainingBytes);
+ }
+
+ const bytes = await stream.read(chunkSize);
+ // Each UCS2 character has an upper byte of 0 and a lower byte matching
+ // the binary data. Using a loop here prevents us from hitting the browser's
+ // internal `arguments.length` limit.
+ const ARGS_MAX = 262144;
+ const stringData = [];
+ for (let i = 0; i < bytes.length; i += ARGS_MAX) {
+ let argsChunk = Math.min(bytes.length, i + ARGS_MAX);
+ stringData.push(
+ String.fromCharCode.apply(null, bytes.slice(i, argsChunk))
+ );
+ }
+ const data = btoa(stringData.join(""));
+
+ return {
+ data,
+ base64Encoded: true,
+ eof: remainingBytes - bytes.length == 0,
+ };
+ }
+}
diff --git a/remote/cdp/domains/parent/Input.sys.mjs b/remote/cdp/domains/parent/Input.sys.mjs
new file mode 100644
index 0000000000..b3b7fd253b
--- /dev/null
+++ b/remote/cdp/domains/parent/Input.sys.mjs
@@ -0,0 +1,168 @@
+/* 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/. */
+
+import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs";
+
+export class Input extends Domain {
+ // commands
+
+ /**
+ * Simulate key events.
+ *
+ * @param {Object} options
+ * - autoRepeat (not supported)
+ * - code (not supported)
+ * - key
+ * - isKeypad (not supported)
+ * - location (not supported)
+ * - modifiers
+ * - text (not supported)
+ * - type
+ * - unmodifiedText (not supported)
+ * - windowsVirtualKeyCode
+ * - nativeVirtualKeyCode (not supported)
+ * - keyIdentifier (not supported)
+ * - isSystemKey (not supported)
+ */
+ async dispatchKeyEvent(options = {}) {
+ // missing code, text, unmodifiedText, autorepeat, location, iskeypad
+ const { key, modifiers, type, windowsVirtualKeyCode } = options;
+ const { alt, ctrl, meta, shift } = Input.Modifier;
+
+ let domType;
+ if (type == "keyDown" || type == "rawKeyDown") {
+ // 'rawKeyDown' is passed as type by puppeteer for all non-text keydown events:
+ // See https://github.com/GoogleChrome/puppeteer/blob/2d99d85976dcb28cc6e3bad4b6a00cd61a67a2cf/lib/Input.js#L52
+ // For now we simply map rawKeyDown to keydown.
+ domType = "keydown";
+ } else if (type == "keyUp" || type == "char") {
+ // 'char' is fired as a single key event. Behind the scenes it will trigger keydown,
+ // keypress and keyup. `domType` will only be used as the event to wait for.
+ domType = "keyup";
+ } else {
+ throw new Error(`Unknown key event type ${type}`);
+ }
+
+ const { browser } = this.session.target;
+ const browserWindow = browser.ownerGlobal;
+
+ const EventUtils = this._getEventUtils(browserWindow);
+ const eventId = await this.executeInChild(
+ "_addContentEventListener",
+ domType
+ );
+
+ if (type == "char") {
+ // type == "char" is used when doing `await page.keyboard.type( 'I’m a list' );`
+ // the ’ character will be calling dispatchKeyEvent only once with type=char.
+ EventUtils.synthesizeKey(key, {}, browserWindow);
+ } else {
+ // Non printable keys should be prefixed with `KEY_`
+ const eventUtilsKey = key.length == 1 ? key : "KEY_" + key;
+ const eventInfo = {
+ keyCode: windowsVirtualKeyCode,
+ type: domType,
+ altKey: !!(modifiers & alt),
+ ctrlKey: !!(modifiers & ctrl),
+ metaKey: !!(modifiers & meta),
+ shiftKey: !!(modifiers & shift),
+ };
+ EventUtils.synthesizeKey(eventUtilsKey, eventInfo, browserWindow);
+ }
+
+ await this.executeInChild("_waitForContentEvent", eventId);
+ }
+
+ /**
+ * Simulate mouse events.
+ *
+ * @param {Object} options
+ * @param {string} options.type
+ * @param {number} options.x
+ * @param {number} options.y
+ * @param {number} options.modifiers
+ * @param {number} options.timestamp [Not Supported]
+ * @param {string} options.button
+ * @param {number} options.buttons [Not Supported]
+ * @param {string} options.clickCount
+ * @param {number} options.deltaX [Not Supported]
+ * @param {number} options.deltaY [Not Supported]
+ * @param {string} options.pointerType [Not Supported]
+ */
+ async dispatchMouseEvent(options = {}) {
+ const { button, clickCount, modifiers, type, x, y } = options;
+ const { alt, ctrl, meta, shift } = Input.Modifier;
+
+ let domType;
+ if (type === "mousePressed") {
+ domType = "mousedown";
+ } else if (type === "mouseReleased") {
+ domType = "mouseup";
+ } else if (type === "mouseMoved") {
+ domType = "mousemove";
+ } else {
+ throw new Error(`Mouse type is not supported: ${type}`);
+ }
+
+ if (domType === "mousedown" && button === "right") {
+ domType = "contextmenu";
+ }
+
+ const buttonID = Input.Button[button] || Input.Button.left;
+ const { browser } = this.session.target;
+ const currentWindow = browser.ownerGlobal;
+
+ const EventUtils = this._getEventUtils(currentWindow);
+ const eventId = await this.executeInChild(
+ "_addContentEventListener",
+ domType
+ );
+
+ EventUtils.synthesizeMouse(browser, x, y, {
+ type: domType,
+ button: buttonID,
+ clickCount: clickCount || 1,
+ altKey: !!(modifiers & alt),
+ ctrlKey: !!(modifiers & ctrl),
+ metaKey: !!(modifiers & meta),
+ shiftKey: !!(modifiers & shift),
+ });
+
+ await this.executeInChild("_waitForContentEvent", eventId);
+ }
+
+ /**
+ * Memoized EventUtils getter.
+ */
+ _getEventUtils(win) {
+ if (!this._eventUtils) {
+ this._eventUtils = {
+ window: win,
+ parent: win,
+ _EU_Ci: Ci,
+ _EU_Cc: Cc,
+ };
+ Services.scriptloader.loadSubScript(
+ "chrome://remote/content/external/EventUtils.js",
+ this._eventUtils
+ );
+ }
+ return this._eventUtils;
+ }
+}
+
+Input.Button = {
+ left: 0,
+ middle: 1,
+ right: 2,
+ back: 3,
+ forward: 4,
+};
+
+Input.Modifier = {
+ alt: 1,
+ ctrl: 2,
+ meta: 4,
+ shift: 8,
+};
diff --git a/remote/cdp/domains/parent/Network.sys.mjs b/remote/cdp/domains/parent/Network.sys.mjs
new file mode 100644
index 0000000000..bccebe7aed
--- /dev/null
+++ b/remote/cdp/domains/parent/Network.sys.mjs
@@ -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/. */
+
+import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs";
+
+const MAX_COOKIE_EXPIRY = Number.MAX_SAFE_INTEGER;
+
+const LOAD_CAUSE_STRINGS = {
+ [Ci.nsIContentPolicy.TYPE_INVALID]: "Invalid",
+ [Ci.nsIContentPolicy.TYPE_OTHER]: "Other",
+ [Ci.nsIContentPolicy.TYPE_SCRIPT]: "Script",
+ [Ci.nsIContentPolicy.TYPE_IMAGE]: "Img",
+ [Ci.nsIContentPolicy.TYPE_STYLESHEET]: "Stylesheet",
+ [Ci.nsIContentPolicy.TYPE_OBJECT]: "Object",
+ [Ci.nsIContentPolicy.TYPE_DOCUMENT]: "Document",
+ [Ci.nsIContentPolicy.TYPE_SUBDOCUMENT]: "Subdocument",
+ [Ci.nsIContentPolicy.TYPE_PING]: "Ping",
+ [Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST]: "Xhr",
+ [Ci.nsIContentPolicy.TYPE_OBJECT_SUBREQUEST]: "ObjectSubdoc",
+ [Ci.nsIContentPolicy.TYPE_DTD]: "Dtd",
+ [Ci.nsIContentPolicy.TYPE_FONT]: "Font",
+ [Ci.nsIContentPolicy.TYPE_MEDIA]: "Media",
+ [Ci.nsIContentPolicy.TYPE_WEBSOCKET]: "Websocket",
+ [Ci.nsIContentPolicy.TYPE_CSP_REPORT]: "Csp",
+ [Ci.nsIContentPolicy.TYPE_XSLT]: "Xslt",
+ [Ci.nsIContentPolicy.TYPE_BEACON]: "Beacon",
+ [Ci.nsIContentPolicy.TYPE_FETCH]: "Fetch",
+ [Ci.nsIContentPolicy.TYPE_IMAGESET]: "Imageset",
+ [Ci.nsIContentPolicy.TYPE_WEB_MANIFEST]: "WebManifest",
+ [Ci.nsIContentPolicy.TYPE_WEB_IDENTITY]: "Webidentity",
+};
+
+export class Network extends Domain {
+ constructor(session) {
+ super(session);
+ this.enabled = false;
+
+ this._onRequest = this._onRequest.bind(this);
+ this._onResponse = this._onResponse.bind(this);
+ }
+
+ destructor() {
+ this.disable();
+
+ super.destructor();
+ }
+
+ enable() {
+ if (this.enabled) {
+ return;
+ }
+ this.enabled = true;
+ this.session.networkObserver.startTrackingBrowserNetwork(
+ this.session.target.browser
+ );
+ this.session.networkObserver.on("request", this._onRequest);
+ this.session.networkObserver.on("response", this._onResponse);
+ }
+
+ disable() {
+ if (!this.enabled) {
+ return;
+ }
+ this.session.networkObserver.stopTrackingBrowserNetwork(
+ this.session.target.browser
+ );
+ this.session.networkObserver.off("request", this._onRequest);
+ this.session.networkObserver.off("response", this._onResponse);
+ this.enabled = false;
+ }
+
+ /**
+ * Deletes browser cookies with matching name and url or domain/path pair.
+ *
+ * @param {Object} options
+ * @param {string} name
+ * Name of the cookies to remove.
+ * @param {string=} url
+ * If specified, deletes all the cookies with the given name
+ * where domain and path match provided URL.
+ * @param {string=} domain
+ * If specified, deletes only cookies with the exact domain.
+ * @param {string=} path
+ * If specified, deletes only cookies with the exact path.
+ */
+ async deleteCookies(options = {}) {
+ const { domain, name, path = "/", url } = options;
+
+ if (typeof name != "string") {
+ throw new TypeError("name: string value expected");
+ }
+
+ if (!url && !domain) {
+ throw new TypeError(
+ "At least one of the url and domain needs to be specified"
+ );
+ }
+
+ // Retrieve host. Check domain first because it has precedence.
+ let hostname = domain || "";
+ if (!hostname.length) {
+ const cookieURL = new URL(url);
+ if (!["http:", "https:"].includes(cookieURL.protocol)) {
+ throw new TypeError("An http or https url must be specified");
+ }
+ hostname = cookieURL.hostname;
+ }
+
+ const cookiesFound = Services.cookies.getCookiesWithOriginAttributes(
+ JSON.stringify({}),
+ hostname
+ );
+
+ for (const cookie of cookiesFound) {
+ if (cookie.name == name && cookie.path.startsWith(path)) {
+ Services.cookies.remove(
+ cookie.host,
+ cookie.name,
+ cookie.path,
+ cookie.originAttributes
+ );
+ }
+ }
+ }
+
+ /**
+ * Activates emulation of network conditions.
+ *
+ * @param {Object} options
+ * @param {boolean} offline
+ * True to emulate internet disconnection.
+ */
+ emulateNetworkConditions(options = {}) {
+ const { offline } = options;
+
+ if (typeof offline != "boolean") {
+ throw new TypeError("offline: boolean value expected");
+ }
+
+ Services.io.offline = offline;
+ }
+
+ /**
+ * Returns all browser cookies.
+ *
+ * Depending on the backend support, will return detailed cookie information in the cookies field.
+ *
+ * @param {Object} options
+ *
+ * @return {Array<Cookie>}
+ * Array of cookie objects.
+ */
+ async getAllCookies(options = {}) {
+ const cookies = [];
+ for (const cookie of Services.cookies.cookies) {
+ cookies.push(_buildCookie(cookie));
+ }
+
+ return { cookies };
+ }
+
+ /**
+ * Returns all browser cookies for the current URL.
+ *
+ * @param {Object} options
+ * @param {Array<string>=} urls
+ * The list of URLs for which applicable cookies will be fetched.
+ * Defaults to the currently open URL.
+ *
+ * @return {Array<Cookie>}
+ * Array of cookie objects.
+ */
+ async getCookies(options = {}) {
+ const { urls = this._getDefaultUrls() } = options;
+
+ if (!Array.isArray(urls)) {
+ throw new TypeError("urls: array expected");
+ }
+
+ for (const [index, url] of urls.entries()) {
+ if (typeof url !== "string") {
+ throw new TypeError(`urls: string value expected at index ${index}`);
+ }
+ }
+
+ const cookies = [];
+ for (let url of urls) {
+ url = new URL(url);
+
+ const secureProtocol = ["https:", "wss:"].includes(url.protocol);
+
+ const cookiesFound = Services.cookies.getCookiesWithOriginAttributes(
+ JSON.stringify({}),
+ url.hostname
+ );
+
+ for (const cookie of cookiesFound) {
+ // Ignore secure cookies for non-secure protocols
+ if (cookie.isSecure && !secureProtocol) {
+ continue;
+ }
+
+ // Ignore cookies which do not match the given path
+ if (!url.pathname.startsWith(cookie.path)) {
+ continue;
+ }
+
+ const builtCookie = _buildCookie(cookie);
+ const duplicateCookie = cookies.some(value => {
+ return (
+ value.name === builtCookie.name &&
+ value.path === builtCookie.path &&
+ value.domain === builtCookie.domain
+ );
+ });
+
+ if (duplicateCookie) {
+ continue;
+ }
+
+ cookies.push(builtCookie);
+ }
+ }
+
+ return { cookies };
+ }
+
+ /**
+ * Sets a cookie with the given cookie data.
+ *
+ * Note that it may overwrite equivalent cookies if they exist.
+ *
+ * @param {Object} cookie
+ * @param {string} name
+ * Cookie name.
+ * @param {string} value
+ * Cookie value.
+ * @param {string=} domain
+ * Cookie domain.
+ * @param {number=} expires
+ * Cookie expiration date, session cookie if not set.
+ * @param {boolean=} httpOnly
+ * True if cookie is http-only.
+ * @param {string=} path
+ * Cookie path.
+ * @param {string=} sameSite
+ * Cookie SameSite type.
+ * @param {boolean=} secure
+ * True if cookie is secure.
+ * @param {string=} url
+ * The request-URI to associate with the setting of the cookie.
+ * This value can affect the default domain and path values of the
+ * created cookie.
+ *
+ * @return {boolean}
+ * True if successfully set cookie.
+ */
+ setCookie(cookie) {
+ if (typeof cookie.name != "string") {
+ throw new TypeError("name: string value expected");
+ }
+
+ if (typeof cookie.value != "string") {
+ throw new TypeError("value: string value expected");
+ }
+
+ if (
+ typeof cookie.url == "undefined" &&
+ typeof cookie.domain == "undefined"
+ ) {
+ throw new TypeError(
+ "At least one of the url and domain needs to be specified"
+ );
+ }
+
+ // Retrieve host. Check domain first because it has precedence.
+ let hostname = cookie.domain || "";
+ let cookieURL;
+ let schemeType = Ci.nsICookie.SCHEME_UNSET;
+ if (!hostname.length) {
+ try {
+ cookieURL = new URL(cookie.url);
+ } catch (e) {
+ return { success: false };
+ }
+
+ if (!["http:", "https:"].includes(cookieURL.protocol)) {
+ throw new TypeError(`Invalid protocol ${cookieURL.protocol}`);
+ }
+
+ if (cookieURL.protocol == "https:") {
+ cookie.secure = true;
+ schemeType = Ci.nsICookie.SCHEME_HTTPS;
+ } else {
+ schemeType = Ci.nsICookie.SCHEME_HTTP;
+ }
+
+ hostname = cookieURL.hostname;
+ }
+
+ if (typeof cookie.path == "undefined") {
+ cookie.path = "/";
+ }
+
+ let isSession = false;
+ if (typeof cookie.expires == "undefined") {
+ isSession = true;
+ cookie.expires = MAX_COOKIE_EXPIRY;
+ }
+
+ const sameSiteMap = new Map([
+ ["None", Ci.nsICookie.SAMESITE_NONE],
+ ["Lax", Ci.nsICookie.SAMESITE_LAX],
+ ["Strict", Ci.nsICookie.SAMESITE_STRICT],
+ ]);
+
+ let success = true;
+ try {
+ Services.cookies.add(
+ hostname,
+ cookie.path,
+ cookie.name,
+ cookie.value,
+ cookie.secure,
+ cookie.httpOnly || false,
+ isSession,
+ cookie.expires,
+ {} /* originAttributes */,
+ sameSiteMap.get(cookie.sameSite),
+ schemeType
+ );
+ } catch (e) {
+ success = false;
+ }
+
+ return { success };
+ }
+
+ /**
+ * Sets given cookies.
+ *
+ * @param {Object} options
+ * @param {Array.<Cookie>} cookies
+ * Cookies to be set.
+ */
+ setCookies(options = {}) {
+ const { cookies } = options;
+
+ if (!Array.isArray(cookies)) {
+ throw new TypeError("Invalid parameters (cookies: array expected)");
+ }
+
+ cookies.forEach(cookie => {
+ const { success } = this.setCookie(cookie);
+ if (!success) {
+ throw new Error("Invalid cookie fields");
+ }
+ });
+ }
+
+ /**
+ * Toggles ignoring cache for each request. If true, cache will not be used.
+ *
+ * @param {Object} options
+ * @param {boolean} options.cacheDisabled
+ * Cache disabled state.
+ */
+ async setCacheDisabled(options = {}) {
+ const { cacheDisabled = false } = options;
+
+ const { INHIBIT_CACHING, LOAD_BYPASS_CACHE, LOAD_NORMAL } = Ci.nsIRequest;
+
+ let loadFlags = LOAD_NORMAL;
+ if (cacheDisabled) {
+ loadFlags = LOAD_BYPASS_CACHE | INHIBIT_CACHING;
+ }
+
+ await this.executeInChild("_updateLoadFlags", loadFlags);
+ }
+
+ /**
+ * Allows overriding user agent with the given string.
+ *
+ * Redirected to Emulation.setUserAgentOverride.
+ */
+ setUserAgentOverride(options = {}) {
+ const { id } = this.session;
+ this.session.execute(id, "Emulation", "setUserAgentOverride", options);
+ }
+
+ _onRequest(eventName, httpChannel, data) {
+ const wrappedChannel = ChannelWrapper.get(httpChannel);
+ const urlFragment = httpChannel.URI.hasRef
+ ? "#" + httpChannel.URI.ref
+ : undefined;
+
+ const request = {
+ url: httpChannel.URI.specIgnoringRef,
+ urlFragment,
+ method: httpChannel.requestMethod,
+ headers: headersAsObject(data.headers),
+ postData: undefined,
+ hasPostData: false,
+ mixedContentType: undefined,
+ initialPriority: undefined,
+ referrerPolicy: undefined,
+ isLinkPreload: false,
+ };
+ this.emit("Network.requestWillBeSent", {
+ requestId: data.requestId,
+ loaderId: data.loaderId,
+ documentURL:
+ wrappedChannel.documentURL || httpChannel.URI.specIgnoringRef,
+ request,
+ timestamp: Date.now() / 1000,
+ wallTime: undefined,
+ initiator: undefined,
+ redirectResponse: undefined,
+ type: LOAD_CAUSE_STRINGS[data.cause] || "unknown",
+ frameId: data.frameId.toString(),
+ hasUserGesture: undefined,
+ });
+ }
+
+ _onResponse(eventName, httpChannel, data) {
+ const wrappedChannel = ChannelWrapper.get(httpChannel);
+ const headers = headersAsObject(data.headers);
+
+ this.emit("Network.responseReceived", {
+ requestId: data.requestId,
+ loaderId: data.loaderId,
+ timestamp: Date.now() / 1000,
+ type: LOAD_CAUSE_STRINGS[data.cause] || "unknown",
+ response: {
+ url: httpChannel.URI.spec,
+ status: data.status,
+ statusText: data.statusText,
+ headers,
+ mimeType: wrappedChannel.contentType,
+ requestHeaders: headersAsObject(data.requestHeaders),
+ connectionReused: undefined,
+ connectionId: undefined,
+ remoteIPAddress: data.remoteIPAddress,
+ remotePort: data.remotePort,
+ fromDiskCache: data.fromCache,
+ encodedDataLength: undefined,
+ protocol: httpChannel.protocolVersion,
+ securityDetails: data.securityDetails,
+ // unknown, neutral, insecure, secure, info, insecure-broken
+ securityState: "unknown",
+ },
+ frameId: data.frameId.toString(),
+ });
+ }
+
+ /**
+ * Creates an array of all Urls in the page context
+ *
+ * @param {Array<string>=} urls
+ */
+ _getDefaultUrls() {
+ const urls = this.session.target.browsingContext
+ .getAllBrowsingContextsInSubtree()
+ .map(context => context.currentURI.spec);
+
+ return urls;
+ }
+}
+
+/**
+ * Creates a CDP Network.Cookie from our internal cookie values
+ *
+ * @param {nsICookie} cookie
+ *
+ * @returns {Network.Cookie}
+ * A CDP Cookie
+ */
+function _buildCookie(cookie) {
+ const data = {
+ name: cookie.name,
+ value: cookie.value,
+ domain: cookie.host,
+ path: cookie.path,
+ expires: cookie.isSession ? -1 : cookie.expiry,
+ // The size is the combined length of both the cookie name and value
+ size: cookie.name.length + cookie.value.length,
+ httpOnly: cookie.isHttpOnly,
+ secure: cookie.isSecure,
+ session: cookie.isSession,
+ };
+
+ if (cookie.sameSite) {
+ const sameSiteMap = new Map([
+ [Ci.nsICookie.SAMESITE_LAX, "Lax"],
+ [Ci.nsICookie.SAMESITE_STRICT, "Strict"],
+ ]);
+
+ data.sameSite = sameSiteMap.get(cookie.sameSite);
+ }
+
+ return data;
+}
+
+/**
+ * Given a array of possibly repeating header names, merge the values for
+ * duplicate headers into a comma-separated list, or in some cases a
+ * newline-separated list.
+ *
+ * e.g. { "Cache-Control": "no-cache,no-store" }
+ *
+ * Based on
+ * https://hg.mozilla.org/mozilla-central/file/56c09d42f411246e407fe30418c27e67a6a44d29/netwerk/protocol/http/nsHttpHeaderArray.h
+ *
+ * @param {Array} headers
+ * Array of {name, value}
+ * @returns {Object}
+ * Object where each key is a header name.
+ */
+function headersAsObject(headers) {
+ const rv = {};
+ headers.forEach(({ name, value }) => {
+ name = name.toLowerCase();
+ if (rv[name]) {
+ const separator = [
+ "set-cookie",
+ "www-authenticate",
+ "proxy-authenticate",
+ ].includes(name)
+ ? "\n"
+ : ",";
+ rv[name] += `${separator}${value}`;
+ } else {
+ rv[name] = value;
+ }
+ });
+ return rv;
+}
diff --git a/remote/cdp/domains/parent/Page.sys.mjs b/remote/cdp/domains/parent/Page.sys.mjs
new file mode 100644
index 0000000000..fd2a747721
--- /dev/null
+++ b/remote/cdp/domains/parent/Page.sys.mjs
@@ -0,0 +1,775 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ clearInterval: "resource://gre/modules/Timer.sys.mjs",
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ setInterval: "resource://gre/modules/Timer.sys.mjs",
+
+ DialogHandler:
+ "chrome://remote/content/cdp/domains/parent/page/DialogHandler.sys.mjs",
+ PollPromise: "chrome://remote/content/shared/Sync.sys.mjs",
+ streamRegistry: "chrome://remote/content/cdp/domains/parent/IO.sys.mjs",
+ TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
+ UnsupportedError: "chrome://remote/content/cdp/Error.sys.mjs",
+ windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ OS: "resource://gre/modules/osfile.jsm",
+});
+
+const MAX_CANVAS_DIMENSION = 32767;
+const MAX_CANVAS_AREA = 472907776;
+
+const PRINT_MAX_SCALE_VALUE = 2.0;
+const PRINT_MIN_SCALE_VALUE = 0.1;
+
+const PDF_TRANSFER_MODES = {
+ base64: "ReturnAsBase64",
+ stream: "ReturnAsStream",
+};
+
+const TIMEOUT_SET_HISTORY_INDEX = 1000;
+
+export class Page extends Domain {
+ constructor(session) {
+ super(session);
+
+ this._onDialogLoaded = this._onDialogLoaded.bind(this);
+ this._onRequest = this._onRequest.bind(this);
+
+ this.enabled = false;
+
+ this.session.networkObserver.startTrackingBrowserNetwork(
+ this.session.target.browser
+ );
+ this.session.networkObserver.on("request", this._onRequest);
+ }
+
+ destructor() {
+ // Flip a flag to avoid to disable the content domain from this.disable()
+ this._isDestroyed = false;
+ this.disable();
+
+ this.session.networkObserver.off("request", this._onRequest);
+ this.session.networkObserver.stopTrackingBrowserNetwork(
+ this.session.target.browser
+ );
+ super.destructor();
+ }
+
+ // commands
+
+ /**
+ * Navigates current page to given URL.
+ *
+ * @param {Object} options
+ * @param {string} options.url
+ * destination URL
+ * @param {string=} options.frameId
+ * frame id to navigate (not supported),
+ * if not specified navigate top frame
+ * @param {string=} options.referrer
+ * referred URL (optional)
+ * @param {string=} options.transitionType
+ * intended transition type
+ * @return {Object}
+ * - frameId {string} frame id that has navigated (or failed to)
+ * - errorText {string=} error message if navigation has failed
+ * - loaderId {string} (not supported)
+ */
+ async navigate(options = {}) {
+ const { url, frameId, referrer, transitionType } = options;
+ if (typeof url != "string") {
+ throw new TypeError("url: string value expected");
+ }
+ let validURL;
+ try {
+ validURL = Services.io.newURI(url);
+ } catch (e) {
+ throw new Error("Error: Cannot navigate to invalid URL");
+ }
+ const topFrameId = this.session.browsingContext.id.toString();
+ if (frameId && frameId != topFrameId) {
+ throw new lazy.UnsupportedError("frameId not supported");
+ }
+
+ const hitsNetwork = ["https", "http"].includes(validURL.scheme);
+ let networkLessLoaderId;
+ if (!hitsNetwork) {
+ // This navigation will not hit the network, use a randomly generated id.
+ const uuid = Services.uuid.generateUUID().toString();
+ networkLessLoaderId = uuid.substring(1, uuid.length - 1);
+
+ // Update the content process map of loader ids.
+ await this.executeInChild("_updateLoaderId", {
+ frameId: this.session.browsingContext.id,
+ loaderId: networkLessLoaderId,
+ });
+ }
+
+ const currentURI = this.session.browsingContext.currentURI;
+
+ const isSameDocumentNavigation =
+ // The "host", "query" and "ref" getters can throw if the URLs are not
+ // http/https, so verify first that both currentURI and validURL are
+ // using http/https.
+ hitsNetwork &&
+ ["https", "http"].includes(currentURI.scheme) &&
+ currentURI.host === validURL.host &&
+ currentURI.query === validURL.query &&
+ !!validURL.ref;
+
+ const requestDone = new Promise(resolve => {
+ if (isSameDocumentNavigation) {
+ // Per CDP documentation, same-document navigations should not emit any
+ // loader id (https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-navigate)
+ resolve({});
+ return;
+ }
+
+ if (!hitsNetwork) {
+ // This navigation will not hit the network, use a randomly generated id.
+ resolve({ navigationRequestId: networkLessLoaderId });
+ return;
+ }
+ let navigationRequestId, redirectedRequestId;
+ const _onNavigationRequest = function(_type, _ch, data) {
+ const {
+ url: requestURL,
+ requestId,
+ redirectedFrom = null,
+ isNavigationRequest,
+ } = data;
+ if (!isNavigationRequest) {
+ return;
+ }
+ if (validURL.spec === requestURL) {
+ navigationRequestId = redirectedRequestId = requestId;
+ } else if (redirectedFrom === redirectedRequestId) {
+ redirectedRequestId = requestId;
+ }
+ };
+
+ const _onRequestFinished = function(_type, _ch, data) {
+ const { requestId, errorCode } = data;
+ if (
+ redirectedRequestId !== requestId ||
+ errorCode == "NS_BINDING_REDIRECTED"
+ ) {
+ // handle next request in redirection chain
+ return;
+ }
+ this.session.networkObserver.off("request", _onNavigationRequest);
+ this.session.networkObserver.off("requestfinished", _onRequestFinished);
+ resolve({ errorCode, navigationRequestId });
+ }.bind(this);
+
+ this.session.networkObserver.on("request", _onNavigationRequest);
+ this.session.networkObserver.on("requestfinished", _onRequestFinished);
+ });
+
+ const opts = {
+ loadFlags: transitionToLoadFlag(transitionType),
+ referrerURI: referrer,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ };
+ this.session.browsingContext.loadURI(url, opts);
+ // clients expect loaderId == requestId for a document navigation request
+ const { navigationRequestId: loaderId, errorCode } = await requestDone;
+ const result = {
+ frameId: topFrameId,
+ loaderId,
+ };
+ if (errorCode) {
+ result.errorText = errorCode;
+ }
+ return result;
+ }
+
+ /**
+ * Capture page screenshot.
+ *
+ * @param {Object} options
+ * @param {Viewport=} options.clip
+ * Capture the screenshot of a given region only.
+ * @param {string=} options.format
+ * Image compression format. Defaults to "png".
+ * @param {number=} options.quality
+ * Compression quality from range [0..100] (jpeg only). Defaults to 80.
+ *
+ * @return {string}
+ * Base64-encoded image data.
+ */
+ async captureScreenshot(options = {}) {
+ const { clip, format = "png", quality = 80 } = options;
+
+ if (options.fromSurface) {
+ throw new lazy.UnsupportedError("fromSurface not supported");
+ }
+
+ let rect;
+ let scale = await this.executeInChild("_devicePixelRatio");
+
+ if (clip) {
+ for (const prop of ["x", "y", "width", "height", "scale"]) {
+ if (clip[prop] == undefined) {
+ throw new TypeError(`clip.${prop}: double value expected`);
+ }
+ }
+
+ const contentRect = await this.executeInChild("_contentRect");
+
+ // For invalid scale values default to full page
+ if (clip.scale <= 0) {
+ Object.assign(clip, {
+ x: 0,
+ y: 0,
+ width: contentRect.width,
+ height: contentRect.height,
+ scale: 1,
+ });
+ } else {
+ if (clip.x < 0 || clip.x > contentRect.width - 1) {
+ clip.x = 0;
+ }
+ if (clip.y < 0 || clip.y > contentRect.height - 1) {
+ clip.y = 0;
+ }
+ if (clip.width <= 0) {
+ clip.width = contentRect.width;
+ }
+ if (clip.height <= 0) {
+ clip.height = contentRect.height;
+ }
+ }
+
+ rect = new DOMRect(clip.x, clip.y, clip.width, clip.height);
+ scale *= clip.scale;
+ } else {
+ // If no specific clipping region has been specified,
+ // fallback to the layout (fixed) viewport, and the
+ // default pixel ratio.
+ const {
+ pageX,
+ pageY,
+ clientWidth,
+ clientHeight,
+ } = await this.executeInChild("_layoutViewport");
+
+ rect = new DOMRect(pageX, pageY, clientWidth, clientHeight);
+ }
+
+ let canvasWidth = rect.width * scale;
+ let canvasHeight = rect.height * scale;
+
+ // Cap the screenshot size based on maximum allowed canvas sizes.
+ // Using higher dimensions would trigger exceptions in Gecko.
+ //
+ // See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#Maximum_canvas_size
+ if (canvasWidth > MAX_CANVAS_DIMENSION) {
+ rect.width = Math.floor(MAX_CANVAS_DIMENSION / scale);
+ canvasWidth = rect.width * scale;
+ }
+ if (canvasHeight > MAX_CANVAS_DIMENSION) {
+ rect.height = Math.floor(MAX_CANVAS_DIMENSION / scale);
+ canvasHeight = rect.height * scale;
+ }
+ // If the area is larger, reduce the height to keep the full width.
+ if (canvasWidth * canvasHeight > MAX_CANVAS_AREA) {
+ rect.height = Math.floor(MAX_CANVAS_AREA / (canvasWidth * scale));
+ canvasHeight = rect.height * scale;
+ }
+
+ const { browsingContext, window } = this.session.target;
+ const snapshot = await browsingContext.currentWindowGlobal.drawSnapshot(
+ rect,
+ scale,
+ "rgb(255,255,255)"
+ );
+
+ const canvas = window.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "canvas"
+ );
+ canvas.width = canvasWidth;
+ canvas.height = canvasHeight;
+
+ const ctx = canvas.getContext("2d");
+ ctx.drawImage(snapshot, 0, 0);
+
+ // Bug 1574935 - Huge dimensions can trigger an OOM because multiple copies
+ // of the bitmap will exist in memory. Force the removal of the snapshot
+ // because it is no longer needed.
+ snapshot.close();
+
+ const url = canvas.toDataURL(`image/${format}`, quality / 100);
+ if (!url.startsWith(`data:image/${format}`)) {
+ throw new lazy.UnsupportedError(`Unsupported MIME type: image/${format}`);
+ }
+
+ // only return the base64 encoded data without the data URL prefix
+ const data = url.substring(url.indexOf(",") + 1);
+
+ return { data };
+ }
+
+ async enable() {
+ if (this.enabled) {
+ return;
+ }
+
+ this.enabled = true;
+
+ const { browser } = this.session.target;
+ this._dialogHandler = new lazy.DialogHandler(browser);
+ this._dialogHandler.on("dialog-loaded", this._onDialogLoaded);
+ await this.executeInChild("enable");
+ }
+
+ async disable() {
+ if (!this.enabled) {
+ return;
+ }
+
+ this._dialogHandler.destructor();
+ this._dialogHandler = null;
+ this.enabled = false;
+
+ if (!this._isDestroyed) {
+ // Only call disable in the content domain if we are not destroying the domain.
+ // If we are destroying the domain, the content domains will be destroyed
+ // independently after firing the remote:destroy event.
+ await this.executeInChild("disable");
+ }
+ }
+
+ async bringToFront() {
+ const { tab, window } = this.session.target;
+
+ // Focus the window, and select the corresponding tab
+ await lazy.windowManager.focusWindow(window);
+ await lazy.TabManager.selectTab(tab);
+ }
+
+ /**
+ * Return metrics relating to the layouting of the page.
+ *
+ * The returned object contains the following entries:
+ *
+ * layoutViewport:
+ * {number} pageX
+ * Horizontal offset relative to the document (CSS pixels)
+ * {number} pageY
+ * Vertical offset relative to the document (CSS pixels)
+ * {number} clientWidth
+ * Width (CSS pixels), excludes scrollbar if present
+ * {number} clientHeight
+ * Height (CSS pixels), excludes scrollbar if present
+ *
+ * visualViewport:
+ * {number} offsetX
+ * Horizontal offset relative to the layout viewport (CSS pixels)
+ * {number} offsetY
+ * Vertical offset relative to the layout viewport (CSS pixels)
+ * {number} pageX
+ * Horizontal offset relative to the document (CSS pixels)
+ * {number} pageY
+ * Vertical offset relative to the document (CSS pixels)
+ * {number} clientWidth
+ * Width (CSS pixels), excludes scrollbar if present
+ * {number} clientHeight
+ * Height (CSS pixels), excludes scrollbar if present
+ * {number} scale
+ * Scale relative to the ideal viewport (size at width=device-width)
+ * {number} zoom
+ * Page zoom factor (CSS to device independent pixels ratio)
+ *
+ * contentSize:
+ * {number} x
+ * X coordinate
+ * {number} y
+ * Y coordinate
+ * {number} width
+ * Width of scrollable area
+ * {number} height
+ * Height of scrollable area
+ *
+ * @return {Promise}
+ * @resolves {layoutViewport, visualViewport, contentSize}
+ */
+ async getLayoutMetrics() {
+ return {
+ layoutViewport: await this.executeInChild("_layoutViewport"),
+ contentSize: await this.executeInChild("_contentRect"),
+ };
+ }
+
+ /**
+ * Returns navigation history for the current page.
+ *
+ * @return {currentIndex:number, entries:Array<NavigationEntry>}
+ */
+ async getNavigationHistory() {
+ const { window } = this.session.target;
+
+ return new Promise(resolve => {
+ function updateSessionHistory(sessionHistory) {
+ const entries = sessionHistory.entries.map(entry => {
+ return {
+ id: entry.ID,
+ url: entry.url,
+ userTypedURL: entry.originalURI || entry.url,
+ title: entry.title,
+ // TODO: Bug 1609514
+ transitionType: null,
+ };
+ });
+
+ resolve({
+ currentIndex: sessionHistory.index,
+ entries,
+ });
+ }
+
+ lazy.SessionStore.getSessionHistory(
+ window.gBrowser.selectedTab,
+ updateSessionHistory
+ );
+ });
+ }
+
+ /**
+ * Interact with the currently opened JavaScript dialog (alert, confirm,
+ * prompt) for this page. This will always close the dialog, either accepting
+ * or rejecting it, with the optional prompt filled.
+ *
+ * @param {Object} options
+ * @param {boolean=} options.accept
+ * for "confirm", "prompt", "beforeunload" dialogs true will accept
+ * the dialog, false will cancel it. For "alert" dialogs, true or
+ * false closes the dialog in the same way.
+ * @param {string=} options.promptText
+ * for "prompt" dialogs, used to fill the prompt input.
+ */
+ async handleJavaScriptDialog(options = {}) {
+ const { accept, promptText } = options;
+
+ if (!this.enabled) {
+ throw new Error("Page domain is not enabled");
+ }
+ await this._dialogHandler.handleJavaScriptDialog({ accept, promptText });
+ }
+
+ /**
+ * Navigates current page to the given history entry.
+ *
+ * @param {Object} options
+ * @param {number} options.entryId
+ * Unique id of the entry to navigate to.
+ */
+ async navigateToHistoryEntry(options = {}) {
+ const { entryId } = options;
+
+ const index = await this._getIndexForHistoryEntryId(entryId);
+
+ if (index == null) {
+ throw new Error("No entry with passed id");
+ }
+
+ const { window } = this.session.target;
+ window.gBrowser.gotoIndex(index);
+
+ // On some platforms the requested index isn't set immediately.
+ await lazy.PollPromise(
+ async (resolve, reject) => {
+ const currentIndex = await this._getCurrentHistoryIndex();
+ if (currentIndex == index) {
+ resolve();
+ } else {
+ reject();
+ }
+ },
+ { timeout: TIMEOUT_SET_HISTORY_INDEX }
+ );
+ }
+
+ /**
+ * Print page as PDF.
+ *
+ * @param {Object} options
+ * @param {boolean=} options.displayHeaderFooter
+ * Display header and footer. Defaults to false.
+ * @param {string=} options.footerTemplate (not supported)
+ * HTML template for the print footer.
+ * @param {string=} options.headerTemplate (not supported)
+ * HTML template for the print header. Should use the same format
+ * as the footerTemplate.
+ * @param {boolean=} options.ignoreInvalidPageRanges
+ * Whether to silently ignore invalid but successfully parsed page ranges,
+ * such as '3-2'. Defaults to false.
+ * @param {boolean=} options.landscape
+ * Paper orientation. Defaults to false.
+ * @param {number=} options.marginBottom
+ * Bottom margin in inches. Defaults to 1cm (~0.4 inches).
+ * @param {number=} options.marginLeft
+ * Left margin in inches. Defaults to 1cm (~0.4 inches).
+ * @param {number=} options.marginRight
+ * Right margin in inches. Defaults to 1cm (~0.4 inches).
+ * @param {number=} options.marginTop
+ * Top margin in inches. Defaults to 1cm (~0.4 inches).
+ * @param {string=} options.pageRanges (not supported)
+ * Paper ranges to print, e.g., '1-5, 8, 11-13'.
+ * Defaults to the empty string, which means print all pages.
+ * @param {number=} options.paperHeight
+ * Paper height in inches. Defaults to 11 inches.
+ * @param {number=} options.paperWidth
+ * Paper width in inches. Defaults to 8.5 inches.
+ * @param {boolean=} options.preferCSSPageSize
+ * Whether or not to prefer page size as defined by CSS.
+ * Defaults to false, in which case the content will be scaled
+ * to fit the paper size.
+ * @param {boolean=} options.printBackground
+ * Print background graphics. Defaults to false.
+ * @param {number=} options.scale
+ * Scale of the webpage rendering. Defaults to 1.
+ * @param {string=} options.transferMode
+ * Return as base64-encoded string (ReturnAsBase64),
+ * or stream (ReturnAsStream). Defaults to ReturnAsBase64.
+ *
+ * @return {Promise<{data:string, stream:string}>
+ * Based on the transferMode setting data is a base64-encoded string,
+ * or stream is a handle to a OS.File stream.
+ */
+ async printToPDF(options = {}) {
+ const {
+ displayHeaderFooter = false,
+ // Bug 1601570 - Implement templates for header and footer
+ // headerTemplate = "",
+ // footerTemplate = "",
+ landscape = false,
+ marginBottom = 0.39,
+ marginLeft = 0.39,
+ marginRight = 0.39,
+ marginTop = 0.39,
+ // Bug 1601571 - Implement handling of page ranges
+ // TODO: pageRanges = "",
+ // TODO: ignoreInvalidPageRanges = false,
+ paperHeight = 11.0,
+ paperWidth = 8.5,
+ preferCSSPageSize = false,
+ printBackground = false,
+ scale = 1.0,
+ transferMode = PDF_TRANSFER_MODES.base64,
+ } = options;
+
+ if (marginBottom < 0) {
+ throw new TypeError("marginBottom is negative");
+ }
+ if (marginLeft < 0) {
+ throw new TypeError("marginLeft is negative");
+ }
+ if (marginRight < 0) {
+ throw new TypeError("marginRight is negative");
+ }
+ if (marginTop < 0) {
+ throw new TypeError("marginTop is negative");
+ }
+ if (scale < PRINT_MIN_SCALE_VALUE || scale > PRINT_MAX_SCALE_VALUE) {
+ throw new TypeError("scale is outside [0.1 - 2] range");
+ }
+ if (paperHeight <= 0) {
+ throw new TypeError("paperHeight is zero or negative");
+ }
+ if (paperWidth <= 0) {
+ throw new TypeError("paperWidth is zero or negative");
+ }
+
+ // Create a unique filename for the temporary PDF file
+ const basePath = lazy.OS.Path.join(
+ lazy.OS.Constants.Path.tmpDir,
+ "remote-agent.pdf"
+ );
+ const { file, path: filePath } = await lazy.OS.File.openUnique(basePath);
+ await file.close();
+
+ const psService = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
+ Ci.nsIPrintSettingsService
+ );
+
+ const printSettings = psService.createNewPrintSettings();
+ printSettings.isInitializedFromPrinter = true;
+ printSettings.isInitializedFromPrefs = true;
+ printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
+ printSettings.printerName = "";
+ printSettings.printSilent = true;
+ printSettings.outputDestination =
+ Ci.nsIPrintSettings.kOutputDestinationFile;
+ printSettings.toFileName = filePath;
+
+ printSettings.paperSizeUnit = Ci.nsIPrintSettings.kPaperSizeInches;
+ printSettings.paperWidth = paperWidth;
+ printSettings.paperHeight = paperHeight;
+
+ printSettings.marginBottom = marginBottom;
+ printSettings.marginLeft = marginLeft;
+ printSettings.marginRight = marginRight;
+ printSettings.marginTop = marginTop;
+
+ printSettings.printBGColors = printBackground;
+ printSettings.printBGImages = printBackground;
+ printSettings.scaling = scale;
+ printSettings.shrinkToFit = preferCSSPageSize;
+
+ if (!displayHeaderFooter) {
+ printSettings.headerStrCenter = "";
+ printSettings.headerStrLeft = "";
+ printSettings.headerStrRight = "";
+ printSettings.footerStrCenter = "";
+ printSettings.footerStrLeft = "";
+ printSettings.footerStrRight = "";
+ }
+
+ if (landscape) {
+ printSettings.orientation = Ci.nsIPrintSettings.kLandscapeOrientation;
+ }
+
+ const { linkedBrowser } = this.session.target.tab;
+
+ await linkedBrowser.browsingContext.print(printSettings);
+
+ // Bug 1603739 - With e10s enabled the promise returned by print() resolves
+ // too early, which means the file hasn't been completely written.
+ await new Promise(resolve => {
+ const DELAY_CHECK_FILE_COMPLETELY_WRITTEN = 100;
+
+ let lastSize = 0;
+ const timerId = lazy.setInterval(async () => {
+ const fileInfo = await lazy.OS.File.stat(filePath);
+ if (lastSize > 0 && fileInfo.size == lastSize) {
+ lazy.clearInterval(timerId);
+ resolve();
+ }
+ lastSize = fileInfo.size;
+ }, DELAY_CHECK_FILE_COMPLETELY_WRITTEN);
+ });
+
+ const fp = await lazy.OS.File.open(filePath);
+
+ const retval = { data: null, stream: null };
+ if (transferMode == PDF_TRANSFER_MODES.stream) {
+ retval.stream = lazy.streamRegistry.add(fp);
+ } else {
+ // return all data as a base64 encoded string
+ let bytes;
+ try {
+ bytes = await fp.read();
+ } finally {
+ fp.close();
+ await lazy.OS.File.remove(filePath);
+ }
+
+ // Each UCS2 character has an upper byte of 0 and a lower byte matching
+ // the binary data
+ retval.data = btoa(String.fromCharCode.apply(null, bytes));
+ }
+
+ return retval;
+ }
+
+ /**
+ * Intercept file chooser requests and transfer control to protocol clients.
+ *
+ * When file chooser interception is enabled,
+ * the native file chooser dialog is not shown.
+ * Instead, a protocol event Page.fileChooserOpened is emitted.
+ *
+ * @param {Object} options
+ * @param {boolean=} options.enabled
+ * Enabled state of file chooser interception.
+ */
+ setInterceptFileChooserDialog(options = {}) {}
+
+ _getCurrentHistoryIndex() {
+ const { window } = this.session.target;
+
+ return new Promise(resolve => {
+ lazy.SessionStore.getSessionHistory(
+ window.gBrowser.selectedTab,
+ history => {
+ resolve(history.index);
+ }
+ );
+ });
+ }
+
+ _getIndexForHistoryEntryId(id) {
+ const { window } = this.session.target;
+
+ return new Promise(resolve => {
+ function updateSessionHistory(sessionHistory) {
+ sessionHistory.entries.forEach((entry, index) => {
+ if (entry.ID == id) {
+ resolve(index);
+ }
+ });
+
+ resolve(null);
+ }
+
+ lazy.SessionStore.getSessionHistory(
+ window.gBrowser.selectedTab,
+ updateSessionHistory
+ );
+ });
+ }
+
+ /**
+ * Emit the proper CDP event javascriptDialogOpening when a javascript dialog
+ * opens for the current target.
+ */
+ _onDialogLoaded(e, data) {
+ const { message, type } = data;
+ // XXX: We rely on the tabmodal-dialog-loaded event (see DialogHandler.jsm)
+ // which is inconsistent with the name "javascriptDialogOpening".
+ // For correctness we should rely on an event fired _before_ the prompt is
+ // visible, such as DOMWillOpenModalDialog. However the payload of this
+ // event does not contain enough data to populate javascriptDialogOpening.
+ //
+ // Since the event is fired asynchronously, this should not have an impact
+ // on the actual tests relying on this API.
+ this.emit("Page.javascriptDialogOpening", { message, type });
+ }
+
+ /**
+ * Handles HTTP request to propagate loaderId to events emitted from
+ * content process
+ */
+ _onRequest(_type, _ch, data) {
+ if (!data.loaderId) {
+ return;
+ }
+ this.executeInChild("_updateLoaderId", {
+ loaderId: data.loaderId,
+ frameId: data.frameId,
+ });
+ }
+}
+
+function transitionToLoadFlag(transitionType) {
+ switch (transitionType) {
+ case "reload":
+ return Ci.nsIWebNavigation.LOAD_FLAGS_IS_REFRESH;
+ case "link":
+ default:
+ return Ci.nsIWebNavigation.LOAD_FLAGS_IS_LINK;
+ }
+}
diff --git a/remote/cdp/domains/parent/Security.sys.mjs b/remote/cdp/domains/parent/Security.sys.mjs
new file mode 100644
index 0000000000..73967d73cb
--- /dev/null
+++ b/remote/cdp/domains/parent/Security.sys.mjs
@@ -0,0 +1,58 @@
+/* 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+});
+
+XPCOMUtils.defineLazyServiceGetters(lazy, {
+ sss: ["@mozilla.org/ssservice;1", "nsISiteSecurityService"],
+ certOverrideService: [
+ "@mozilla.org/security/certoverride;1",
+ "nsICertOverrideService",
+ ],
+});
+
+const CERT_PINNING_ENFORCEMENT_PREF = "security.cert_pinning.enforcement_level";
+const HSTS_PRELOAD_LIST_PREF = "network.stricttransportsecurity.preloadlist";
+
+export class Security extends Domain {
+ destructor() {
+ this.setIgnoreCertificateErrors({ ignore: false });
+ }
+
+ /**
+ * Enable/disable whether all certificate errors should be ignored
+ *
+ * @param {Object} options
+ * @param {boolean=} options.ignore
+ * if true, all certificate errors will be ignored.
+ */
+ setIgnoreCertificateErrors(options = {}) {
+ const { ignore } = options;
+
+ if (ignore) {
+ // make it possible to register certificate overrides for domains
+ // that use HSTS or HPKP
+ lazy.Preferences.set(HSTS_PRELOAD_LIST_PREF, false);
+ lazy.Preferences.set(CERT_PINNING_ENFORCEMENT_PREF, 0);
+ } else {
+ lazy.Preferences.reset(HSTS_PRELOAD_LIST_PREF);
+ lazy.Preferences.reset(CERT_PINNING_ENFORCEMENT_PREF);
+
+ // clear collected HSTS and HPKP state
+ lazy.sss.clearAll();
+ }
+
+ lazy.certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData(
+ ignore
+ );
+ }
+}
diff --git a/remote/cdp/domains/parent/Target.sys.mjs b/remote/cdp/domains/parent/Target.sys.mjs
new file mode 100644
index 0000000000..5f0229ce4d
--- /dev/null
+++ b/remote/cdp/domains/parent/Target.sys.mjs
@@ -0,0 +1,193 @@
+/* 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/. */
+
+import { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ContextualIdentityService:
+ "resource://gre/modules/ContextualIdentityService.sys.mjs",
+ MainProcessTarget:
+ "chrome://remote/content/cdp/targets/MainProcessTarget.sys.mjs",
+ TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
+ TabSession: "chrome://remote/content/cdp/sessions/TabSession.sys.mjs",
+ windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs",
+});
+
+let browserContextIds = 1;
+
+export class Target extends Domain {
+ constructor(session) {
+ super(session);
+
+ this._onTargetCreated = this._onTargetCreated.bind(this);
+ this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
+ }
+
+ getBrowserContexts() {
+ return {
+ browserContextIds: [],
+ };
+ }
+
+ createBrowserContext() {
+ const identity = lazy.ContextualIdentityService.create(
+ "remote-agent-" + browserContextIds++
+ );
+ return { browserContextId: identity.userContextId };
+ }
+
+ disposeBrowserContext(options = {}) {
+ const { browserContextId } = options;
+
+ lazy.ContextualIdentityService.remove(browserContextId);
+ lazy.ContextualIdentityService.closeContainerTabs(browserContextId);
+ }
+
+ getTargets() {
+ const { targetList } = this.session.target;
+
+ const targetInfos = [];
+ for (const target of targetList) {
+ if (target instanceof lazy.MainProcessTarget) {
+ continue;
+ }
+
+ targetInfos.push(this._getTargetInfo(target));
+ }
+
+ return { targetInfos };
+ }
+
+ setDiscoverTargets(options = {}) {
+ const { discover } = options;
+ const { targetList } = this.session.target;
+ if (discover) {
+ targetList.on("target-created", this._onTargetCreated);
+ targetList.on("target-destroyed", this._onTargetDestroyed);
+ } else {
+ targetList.off("target-created", this._onTargetCreated);
+ targetList.off("target-destroyed", this._onTargetDestroyed);
+ }
+ for (const target of targetList) {
+ this._onTargetCreated("target-created", target);
+ }
+ }
+
+ async createTarget(options = {}) {
+ const { browserContextId } = options;
+ const { targetList, window } = this.session.target;
+ const onTarget = targetList.once("target-created");
+ const tab = await lazy.TabManager.addTab({
+ focus: true,
+ userContextId: browserContextId,
+ window,
+ });
+ const target = await onTarget;
+ if (tab.linkedBrowser != target.browser) {
+ throw new Error(
+ "Unexpected tab opened: " + tab.linkedBrowser.currentURI.spec
+ );
+ }
+ return { targetId: target.id };
+ }
+
+ async closeTarget(options = {}) {
+ const { targetId } = options;
+ const { targetList } = this.session.target;
+ const target = targetList.getById(targetId);
+
+ if (!target) {
+ throw new Error(`Unable to find target with id '${targetId}'`);
+ }
+
+ await lazy.TabManager.removeTab(target.tab);
+ }
+
+ async activateTarget(options = {}) {
+ const { targetId } = options;
+ const { targetList, window } = this.session.target;
+ const target = targetList.getById(targetId);
+
+ if (!target) {
+ throw new Error(`Unable to find target with id '${targetId}'`);
+ }
+
+ // Focus the window, and select the corresponding tab
+ await lazy.windowManager.focusWindow(window);
+ await lazy.TabManager.selectTab(target.tab);
+ }
+
+ attachToTarget(options = {}) {
+ const { targetId } = options;
+ const { targetList } = this.session.target;
+ const target = targetList.getById(targetId);
+
+ if (!target) {
+ throw new Error(`Unable to find target with id '${targetId}'`);
+ }
+
+ const tabSession = new lazy.TabSession(
+ this.session.connection,
+ target,
+ Services.uuid
+ .generateUUID()
+ .toString()
+ .slice(1, -1)
+ );
+ this.session.connection.registerSession(tabSession);
+
+ this._emitAttachedToTarget(target, tabSession);
+
+ return {
+ sessionId: tabSession.id,
+ };
+ }
+
+ setAutoAttach() {}
+
+ sendMessageToTarget(options = {}) {
+ const { sessionId, message } = options;
+ const { connection } = this.session;
+ connection.sendMessageToTarget(sessionId, message);
+ }
+
+ /**
+ * Internal methods: the following methods are not part of CDP;
+ * note the _ prefix.
+ */
+
+ _emitAttachedToTarget(target, tabSession) {
+ const targetInfo = this._getTargetInfo(target);
+ this.emit("Target.attachedToTarget", {
+ targetInfo,
+ sessionId: tabSession.id,
+ waitingForDebugger: false,
+ });
+ }
+
+ _getTargetInfo(target) {
+ return {
+ targetId: target.id,
+ type: target.type,
+ title: target.title,
+ url: target.url,
+ // TODO: Correctly determine if target is attached (bug 1680780)
+ attached: target.id == this.session.target.id,
+ browserContextId: target.browserContextId,
+ };
+ }
+
+ _onTargetCreated(eventName, target) {
+ const targetInfo = this._getTargetInfo(target);
+ this.emit("Target.targetCreated", { targetInfo });
+ }
+
+ _onTargetDestroyed(eventName, target) {
+ this.emit("Target.targetDestroyed", {
+ targetId: target.id,
+ });
+ }
+}
diff --git a/remote/cdp/domains/parent/page/DialogHandler.sys.mjs b/remote/cdp/domains/parent/page/DialogHandler.sys.mjs
new file mode 100644
index 0000000000..a142165b4b
--- /dev/null
+++ b/remote/cdp/domains/parent/page/DialogHandler.sys.mjs
@@ -0,0 +1,140 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
+});
+
+const DIALOG_TYPES = {
+ ALERT: "alert",
+ BEFOREUNLOAD: "beforeunload",
+ CONFIRM: "confirm",
+ PROMPT: "prompt",
+};
+
+/**
+ * Helper dedicated to detect and interact with browser dialogs such as `alert`,
+ * `confirm` etc. The current implementation only supports tabmodal dialogs,
+ * not full window dialogs.
+ *
+ * Emits "dialog-loaded" when a javascript dialog is opened for the current
+ * browser.
+ *
+ * @param {BrowserElement} browser
+ */
+export class DialogHandler {
+ constructor(browser) {
+ lazy.EventEmitter.decorate(this);
+ this._dialog = null;
+ this._browser = browser;
+
+ this._onCommonDialogLoaded = this._onCommonDialogLoaded.bind(this);
+ this._onTabDialogLoaded = this._onTabDialogLoaded.bind(this);
+
+ Services.obs.addObserver(
+ this._onCommonDialogLoaded,
+ "common-dialog-loaded"
+ );
+ Services.obs.addObserver(this._onTabDialogLoaded, "tabmodal-dialog-loaded");
+ }
+
+ destructor() {
+ this._dialog = null;
+ this._pageTarget = null;
+
+ Services.obs.removeObserver(
+ this._onCommonDialogLoaded,
+ "common-dialog-loaded"
+ );
+ Services.obs.removeObserver(
+ this._onTabDialogLoaded,
+ "tabmodal-dialog-loaded"
+ );
+ }
+
+ async handleJavaScriptDialog({ accept, promptText }) {
+ if (!this._dialog) {
+ throw new Error("No dialog available for handleJavaScriptDialog");
+ }
+
+ const type = this._getDialogType();
+ if (promptText && type === "prompt") {
+ this._dialog.ui.loginTextbox.value = promptText;
+ }
+
+ const onDialogClosed = new Promise(r => {
+ this._browser.addEventListener("DOMModalDialogClosed", r, {
+ once: true,
+ });
+ });
+
+ // 0 corresponds to the OK callback, 1 to the CANCEL callback.
+ if (accept) {
+ this._dialog.ui.button0.click();
+ } else {
+ this._dialog.ui.button1.click();
+ }
+
+ await onDialogClosed;
+
+ // Resetting dialog to null here might be racy and lead to errors if the
+ // content page is triggering several prompts in a row.
+ // See Bug 1569578.
+ this._dialog = null;
+ }
+
+ _getDialogType() {
+ const { inPermitUnload, promptType } = this._dialog.args;
+
+ if (inPermitUnload) {
+ return DIALOG_TYPES.BEFOREUNLOAD;
+ }
+
+ switch (promptType) {
+ case "alert":
+ return DIALOG_TYPES.ALERT;
+ case "confirm":
+ return DIALOG_TYPES.CONFIRM;
+ case "prompt":
+ return DIALOG_TYPES.PROMPT;
+ default:
+ throw new Error("Unsupported dialog type: " + promptType);
+ }
+ }
+
+ _onCommonDialogLoaded(dialogWindow) {
+ const dialogs = this._browser.tabDialogBox.getContentDialogManager()
+ .dialogs;
+ const dialog = dialogs.find(d => d.frameContentWindow === dialogWindow);
+
+ if (!dialog) {
+ // The dialog is not for the current tab.
+ return;
+ }
+
+ this._dialog = dialogWindow.Dialog;
+ const message = this._dialog.args.text;
+ const type = this._getDialogType();
+
+ this.emit("dialog-loaded", { message, type });
+ }
+
+ _onTabDialogLoaded(promptContainer) {
+ const prompts = this._browser.tabModalPromptBox.listPrompts();
+ const prompt = prompts.find(p => p.ui.promptContainer === promptContainer);
+
+ if (!prompt) {
+ // The dialog is not for the current tab.
+ return;
+ }
+
+ this._dialog = prompt;
+ const message = this._dialog.args.text;
+ const type = this._getDialogType();
+
+ this.emit("dialog-loaded", { message, type });
+ }
+}