summaryrefslogtreecommitdiffstats
path: root/remote/cdp/domains/content/runtime/ExecutionContext.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'remote/cdp/domains/content/runtime/ExecutionContext.sys.mjs')
-rw-r--r--remote/cdp/domains/content/runtime/ExecutionContext.sys.mjs564
1 files changed, 564 insertions, 0 deletions
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..4d394f6bf9
--- /dev/null
+++ b/remote/cdp/domains/content/runtime/ExecutionContext.sys.mjs
@@ -0,0 +1,564 @@
+/* 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, {
+ generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
+});
+
+const TYPED_ARRAY_CLASSES = [
+ "Uint8Array",
+ "Uint8ClampedArray",
+ "Uint16Array",
+ "Uint32Array",
+ "Int8Array",
+ "Int16Array",
+ "Int32Array",
+ "Float32Array",
+ "Float64Array",
+];
+
+// Bug 1786299: Puppeteer expects specific error messages.
+const ERROR_CYCLIC_REFERENCE = "Object reference chain is too long";
+const ERROR_CANNOT_RETURN_BY_VALUE = "Object couldn't be returned by value";
+
+function randomInt() {
+ return crypto.getRandomValues(new Uint32Array(1))[0];
+}
+
+/**
+ * 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 = lazy.generateUUID();
+
+ // 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 = randomInt();
+ // 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} 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.
+ *
+ * @returns {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(this._debuggee.makeDebuggeeValue(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(this._debuggee.makeDebuggeeValue(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
+ * @returns {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
+ * @returns {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} debuggerObj
+ * The object to convert
+ *
+ * @returns {object}
+ * The converted object
+ */
+ _serialize(debuggerObj) {
+ const result = this._debuggee.executeInGlobalWithBindings(
+ `
+ JSON.stringify(e, (key, value) => {
+ if (typeof value === "symbol") {
+ // CDP cannot return Symbols
+ throw new Error();
+ }
+
+ return value;
+ });
+ `,
+ { e: debuggerObj }
+ );
+ if (result.throw) {
+ const exception = this._toRawObject(result.throw);
+ if (exception.message === "cyclic object value") {
+ throw new Error(ERROR_CYCLIC_REFERENCE);
+ }
+
+ throw new Error(ERROR_CANNOT_RETURN_BY_VALUE);
+ }
+
+ return JSON.parse(result.return);
+ }
+
+ _toRawObject(maybeDebuggerObject) {
+ if (maybeDebuggerObject instanceof Debugger.Object) {
+ // Retrieve the referent for the provided Debugger.object.
+ // See https://firefox-source-docs.mozilla.org/devtools-user/debugger-api/debugger.object/index.html
+ const rawObject = maybeDebuggerObject.unsafeDereference();
+ return Cu.waiveXrays(rawObject);
+ }
+
+ // If maybeDebuggerObject was not a Debugger.Object, it is a primitive value
+ // which can be used as is.
+ return maybeDebuggerObject;
+ }
+}