1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5"use strict";
6
7var EXPORTED_SYMBOLS = ["ExecutionContext"];
8
9const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(
10  Ci.nsIUUIDGenerator
11);
12
13const TYPED_ARRAY_CLASSES = [
14  "Uint8Array",
15  "Uint8ClampedArray",
16  "Uint16Array",
17  "Uint32Array",
18  "Int8Array",
19  "Int16Array",
20  "Int32Array",
21  "Float32Array",
22  "Float64Array",
23];
24
25function uuid() {
26  return uuidGen
27    .generateUUID()
28    .toString()
29    .slice(1, -1);
30}
31
32/**
33 * This class represent a debuggable context onto which we can evaluate Javascript.
34 * This is typically a document, but it could also be a worker, an add-on, ... or
35 * any kind of context involving JS scripts.
36 *
37 * @param {Debugger} dbg
38 *   A Debugger instance that we can use to inspect the given global.
39 * @param {GlobalObject} debuggee
40 *   The debuggable context's global object. This is typically the document window
41 *   object. But it can also be any global object, like a worker global scope object.
42 */
43class ExecutionContext {
44  constructor(dbg, debuggee, id, isDefault) {
45    this._debugger = dbg;
46    this._debuggee = this._debugger.addDebuggee(debuggee);
47
48    // Here, we assume that debuggee is a window object and we will propably have
49    // to adapt that once we cover workers or contexts that aren't a document.
50    this.window = debuggee;
51    this.windowId = debuggee.windowUtils.currentInnerWindowID;
52    this.id = id;
53    this.frameId = debuggee.docShell.browsingContext.id.toString();
54    this.isDefault = isDefault;
55
56    // objectId => Debugger.Object
57    this._remoteObjects = new Map();
58  }
59
60  destructor() {
61    this._debugger.removeDebuggee(this._debuggee);
62  }
63
64  hasRemoteObject(objectId) {
65    return this._remoteObjects.has(objectId);
66  }
67
68  getRemoteObject(objectId) {
69    return this._remoteObjects.get(objectId);
70  }
71
72  getRemoteObjectByNodeId(nodeId) {
73    for (const value of this._remoteObjects.values()) {
74      if (value.nodeId == nodeId) {
75        return value;
76      }
77    }
78
79    return null;
80  }
81
82  releaseObject(objectId) {
83    return this._remoteObjects.delete(objectId);
84  }
85
86  /**
87   * Add a new debuggerObj to the object cache.
88   *
89   * Whenever an object is returned as reference, a new entry is added
90   * to the internal object cache. It means the same underlying object or node
91   * can be represented via multiple references.
92   */
93  setRemoteObject(debuggerObj) {
94    const objectId = uuid();
95
96    // TODO: Wrap Symbol into an object,
97    // which would allow us to set the objectId.
98    if (typeof debuggerObj == "object") {
99      debuggerObj.objectId = objectId;
100    }
101
102    // For node objects add an unique identifier.
103    if (
104      debuggerObj instanceof Debugger.Object &&
105      Node.isInstance(debuggerObj.unsafeDereference())
106    ) {
107      debuggerObj.nodeId = uuid();
108      // We do not differentiate between backendNodeId and nodeId (yet)
109      debuggerObj.backendNodeId = debuggerObj.nodeId;
110    }
111
112    this._remoteObjects.set(objectId, debuggerObj);
113
114    return objectId;
115  }
116
117  /**
118   * Evaluate a Javascript expression.
119   *
120   * @param {String} expression
121   *   The JS expression to evaluate against the JS context.
122   * @param {boolean} options.awaitPromise
123   *     Whether execution should `await` for resulting value
124   *     and return once awaited promise is resolved.
125   * @param {boolean} returnByValue
126   *     Whether the result is expected to be a JSON object
127   *     that should be sent by value.
128   *
129   * @return {Object} A multi-form object depending if the execution
130   *   succeed or failed. If the expression failed to evaluate,
131   *   it will return an object with an `exceptionDetails` attribute
132   *   matching the `ExceptionDetails` CDP type. Otherwise it will
133   *   return an object with `result` attribute whose type is
134   *   `RemoteObject` CDP type.
135   */
136  async evaluate(expression, awaitPromise, returnByValue) {
137    let rv = this._debuggee.executeInGlobal(expression);
138    if (!rv) {
139      return {
140        exceptionDetails: {
141          text: "Evaluation terminated!",
142        },
143      };
144    }
145
146    if (rv.throw) {
147      return this._returnError(rv.throw);
148    }
149
150    let result = rv.return;
151
152    if (result && result.isPromise && awaitPromise) {
153      if (result.promiseState === "fulfilled") {
154        result = result.promiseValue;
155      } else if (result.promiseState === "rejected") {
156        return this._returnError(result.promiseReason);
157      } else {
158        try {
159          const promiseResult = await result.unsafeDereference();
160          result = this._debuggee.makeDebuggeeValue(promiseResult);
161        } catch (e) {
162          // The promise has been rejected
163          return this._returnError(e);
164        }
165      }
166    }
167
168    if (returnByValue) {
169      result = this._toRemoteObjectByValue(result);
170    } else {
171      result = this._toRemoteObject(result);
172    }
173
174    return { result };
175  }
176
177  /**
178   * Given a Debugger.Object reference for an Exception, return a JSON object
179   * describing the exception by following CDP ExceptionDetails specification.
180   */
181  _returnError(exception) {
182    if (
183      this._debuggee.executeInGlobalWithBindings("exception instanceof Error", {
184        exception,
185      }).return
186    ) {
187      const text = this._debuggee.executeInGlobalWithBindings(
188        "exception.message",
189        { exception }
190      ).return;
191      return {
192        exceptionDetails: {
193          text,
194        },
195      };
196    }
197
198    // If that isn't an Error, consider the exception as a JS value
199    return {
200      exceptionDetails: {
201        exception: this._toRemoteObject(exception),
202      },
203    };
204  }
205
206  async callFunctionOn(
207    functionDeclaration,
208    callArguments = [],
209    returnByValue = false,
210    awaitPromise = false,
211    objectId = null
212  ) {
213    // Map the given objectId to a JS reference.
214    let thisArg = null;
215    if (objectId) {
216      thisArg = this.getRemoteObject(objectId);
217      if (!thisArg) {
218        throw new Error(`Unable to get target object with id: ${objectId}`);
219      }
220    }
221
222    // First evaluate the function
223    const fun = this._debuggee.executeInGlobal("(" + functionDeclaration + ")");
224    if (!fun) {
225      return {
226        exceptionDetails: {
227          text: "Evaluation terminated!",
228        },
229      };
230    }
231    if (fun.throw) {
232      return this._returnError(fun.throw);
233    }
234
235    // Then map all input arguments, which are matching CDP's CallArguments type,
236    // into JS values
237    const args = callArguments.map(arg => this._fromCallArgument(arg));
238
239    // Finally, call the function with these arguments
240    const rv = fun.return.apply(thisArg, args);
241    if (rv.throw) {
242      return this._returnError(rv.throw);
243    }
244
245    let result = rv.return;
246
247    if (result && result.isPromise && awaitPromise) {
248      if (result.promiseState === "fulfilled") {
249        result = result.promiseValue;
250      } else if (result.promiseState === "rejected") {
251        return this._returnError(result.promiseReason);
252      } else {
253        try {
254          const promiseResult = await result.unsafeDereference();
255          result = this._debuggee.makeDebuggeeValue(promiseResult);
256        } catch (e) {
257          // The promise has been rejected
258          return this._returnError(e);
259        }
260      }
261    }
262
263    if (returnByValue) {
264      result = this._toRemoteObjectByValue(result);
265    } else {
266      result = this._toRemoteObject(result);
267    }
268
269    return { result };
270  }
271
272  getProperties({ objectId, ownProperties }) {
273    let debuggerObj = this.getRemoteObject(objectId);
274    if (!debuggerObj) {
275      throw new Error("Could not find object with given id");
276    }
277
278    const result = [];
279    const serializeObject = (debuggerObj, isOwn) => {
280      for (const propertyName of debuggerObj.getOwnPropertyNames()) {
281        const descriptor = debuggerObj.getOwnPropertyDescriptor(propertyName);
282        result.push({
283          name: propertyName,
284
285          configurable: descriptor.configurable,
286          enumerable: descriptor.enumerable,
287          writable: descriptor.writable,
288          value: this._toRemoteObject(descriptor.value),
289          get: descriptor.get
290            ? this._toRemoteObject(descriptor.get)
291            : undefined,
292          set: descriptor.set
293            ? this._toRemoteObject(descriptor.set)
294            : undefined,
295
296          isOwn,
297        });
298      }
299    };
300
301    // When `ownProperties` is set to true, we only iterate over own properties.
302    // Otherwise, we also iterate over propreties inherited from the prototype chain.
303    serializeObject(debuggerObj, true);
304
305    if (!ownProperties) {
306      while (true) {
307        debuggerObj = debuggerObj.proto;
308        if (!debuggerObj) {
309          break;
310        }
311        serializeObject(debuggerObj, false);
312      }
313    }
314
315    return {
316      result,
317    };
318  }
319
320  /**
321   * Given a CDP `CallArgument`, return a JS value that represent this argument.
322   * Note that `CallArgument` is actually very similar to `RemoteObject`
323   */
324  _fromCallArgument(arg) {
325    if (arg.objectId) {
326      if (!this.hasRemoteObject(arg.objectId)) {
327        throw new Error("Could not find object with given id");
328      }
329      return this.getRemoteObject(arg.objectId);
330    }
331
332    if (arg.unserializableValue) {
333      switch (arg.unserializableValue) {
334        case "-0":
335          return -0;
336        case "Infinity":
337          return Infinity;
338        case "-Infinity":
339          return -Infinity;
340        case "NaN":
341          return NaN;
342        default:
343          if (/^\d+n$/.test(arg.unserializableValue)) {
344            // eslint-disable-next-line no-undef
345            return BigInt(arg.unserializableValue.slice(0, -1));
346          }
347          throw new Error("Couldn't parse value object in call argument");
348      }
349    }
350
351    return this._deserialize(arg.value);
352  }
353
354  /**
355   * Given a JS value, create a copy of it within the debugee compartment.
356   */
357  _deserialize(obj) {
358    if (typeof obj !== "object") {
359      return obj;
360    }
361    const result = this._debuggee.executeInGlobalWithBindings(
362      "JSON.parse(obj)",
363      { obj: JSON.stringify(obj) }
364    );
365    if (result.throw) {
366      throw new Error("Unable to deserialize object");
367    }
368    return result.return;
369  }
370
371  /**
372   * Given a `Debugger.Object` object, return a JSON-serializable description of it
373   * matching `RemoteObject` CDP type.
374   *
375   * @param {Debugger.Object} debuggerObj
376   *  The object to serialize
377   * @return {RemoteObject}
378   *  The serialized description of the given object
379   */
380  _toRemoteObject(debuggerObj) {
381    const result = {};
382
383    // First handle all non-primitive values which are going to be wrapped by the
384    // Debugger API into Debugger.Object instances
385    if (debuggerObj instanceof Debugger.Object) {
386      const rawObj = debuggerObj.unsafeDereference();
387
388      result.objectId = this.setRemoteObject(debuggerObj);
389      result.type = typeof rawObj;
390
391      // Map the Debugger API `class` attribute to CDP `subtype`
392      const cls = debuggerObj.class;
393      if (debuggerObj.isProxy) {
394        result.subtype = "proxy";
395      } else if (cls == "Array") {
396        result.subtype = "array";
397      } else if (cls == "RegExp") {
398        result.subtype = "regexp";
399      } else if (cls == "Date") {
400        result.subtype = "date";
401      } else if (cls == "Map") {
402        result.subtype = "map";
403      } else if (cls == "Set") {
404        result.subtype = "set";
405      } else if (cls == "WeakMap") {
406        result.subtype = "weakmap";
407      } else if (cls == "WeakSet") {
408        result.subtype = "weakset";
409      } else if (cls == "Error") {
410        result.subtype = "error";
411      } else if (cls == "Promise") {
412        result.subtype = "promise";
413      } else if (TYPED_ARRAY_CLASSES.includes(cls)) {
414        result.subtype = "typedarray";
415      } else if (Node.isInstance(rawObj)) {
416        result.subtype = "node";
417        result.className = ChromeUtils.getClassName(rawObj);
418        result.description = rawObj.localName || rawObj.nodeName;
419        if (rawObj.id) {
420          result.description += `#${rawObj.id}`;
421        }
422      }
423      return result;
424    }
425
426    // Now, handle all values that Debugger API isn't wrapping into Debugger.API.
427    // This is all the primitive JS types.
428    result.type = typeof debuggerObj;
429
430    // Symbol and BigInt are primitive values but aren't serializable.
431    // CDP expects them to be considered as objects, with an objectId to later inspect
432    // them.
433    if (result.type == "symbol") {
434      result.description = debuggerObj.toString();
435      result.objectId = this.setRemoteObject(debuggerObj);
436
437      return result;
438    }
439
440    // A few primitive type can't be serialized and CDP has special case for them
441    if (Object.is(debuggerObj, NaN)) {
442      result.unserializableValue = "NaN";
443    } else if (Object.is(debuggerObj, -0)) {
444      result.unserializableValue = "-0";
445    } else if (Object.is(debuggerObj, Infinity)) {
446      result.unserializableValue = "Infinity";
447    } else if (Object.is(debuggerObj, -Infinity)) {
448      result.unserializableValue = "-Infinity";
449    } else if (result.type == "bigint") {
450      result.unserializableValue = `${debuggerObj}n`;
451    }
452
453    if (result.unserializableValue) {
454      result.description = result.unserializableValue;
455      return result;
456    }
457
458    // Otherwise, we serialize the primitive values as-is via `value` attribute
459    result.value = debuggerObj;
460
461    // null is special as it has a dedicated subtype
462    if (debuggerObj === null) {
463      result.subtype = "null";
464    }
465
466    return result;
467  }
468
469  /**
470   * Given a `Debugger.Object` object, return a JSON-serializable description of it
471   * matching `RemoteObject` CDP type.
472   *
473   * @param {Debugger.Object} debuggerObj
474   *  The object to serialize
475   * @return {RemoteObject}
476   *  The serialized description of the given object
477   */
478  _toRemoteObjectByValue(debuggerObj) {
479    const type = typeof debuggerObj;
480
481    if (type == "undefined") {
482      return { type };
483    }
484
485    let unserializableValue = undefined;
486    if (Object.is(debuggerObj, -0)) {
487      unserializableValue = "-0";
488    } else if (Object.is(debuggerObj, NaN)) {
489      unserializableValue = "NaN";
490    } else if (Object.is(debuggerObj, Infinity)) {
491      unserializableValue = "Infinity";
492    } else if (Object.is(debuggerObj, -Infinity)) {
493      unserializableValue = "-Infinity";
494    } else if (typeof debuggerObj == "bigint") {
495      unserializableValue = `${debuggerObj}n`;
496    }
497
498    if (unserializableValue) {
499      return {
500        type,
501        unserializableValue,
502        description: unserializableValue,
503      };
504    }
505
506    const value = this._serialize(debuggerObj);
507    return {
508      type: typeof value,
509      value,
510      description: value != null ? value.toString() : value,
511    };
512  }
513
514  /**
515   * Convert a given `Debugger.Object` to an object.
516   *
517   * @param {Debugger.Object} obj
518   *  The object to convert
519   *
520   * @return {Object}
521   *  The converted object
522   */
523  _serialize(debuggerObj) {
524    const result = this._debuggee.executeInGlobalWithBindings(
525      "JSON.stringify(e)",
526      { e: debuggerObj }
527    );
528    if (result.throw) {
529      throw new Error("Object is not serializable");
530    }
531
532    return JSON.parse(result.return);
533  }
534}
535