tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

script.sys.mjs (13715B)


      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 import { WindowGlobalBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/WindowGlobalBiDiModule.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
     11  getFramesFromStack: "chrome://remote/content/shared/Stack.sys.mjs",
     12  isChromeFrame: "chrome://remote/content/shared/Stack.sys.mjs",
     13  OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
     14  setDefaultSerializationOptions:
     15    "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
     16  stringify: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
     17 });
     18 
     19 /**
     20 * @typedef {string} EvaluationStatus
     21 */
     22 
     23 /**
     24 * Enum of possible evaluation states.
     25 *
     26 * @readonly
     27 * @enum {EvaluationStatus}
     28 */
     29 const EvaluationStatus = {
     30  Normal: "normal",
     31  Throw: "throw",
     32 };
     33 
     34 class ScriptModule extends WindowGlobalBiDiModule {
     35  destroy() {}
     36 
     37  #buildExceptionDetails(
     38    exception,
     39    stack,
     40    realm,
     41    resultOwnership,
     42    seenNodeIds
     43  ) {
     44    exception = this.#toRawObject(exception);
     45 
     46    // A stacktrace is mandatory to build exception details and a missing stack
     47    // means we encountered an unexpected issue. Throw with an explicit error.
     48    if (!stack) {
     49      throw new Error(
     50        `Missing stack, unable to build exceptionDetails for exception: ${lazy.stringify(
     51          exception
     52        )}`
     53      );
     54    }
     55 
     56    const frames = lazy.getFramesFromStack(stack) || [];
     57    const callFrames = frames
     58      // Remove chrome/internal frames
     59      .filter(frame => !lazy.isChromeFrame(frame))
     60      // Translate frames from getFramesFromStack to frames expected by
     61      // WebDriver BiDi.
     62      .map(frame => {
     63        return {
     64          columnNumber: frame.columnNumber - 1,
     65          functionName: frame.functionName,
     66          lineNumber: frame.lineNumber - 1,
     67          url: frame.filename,
     68        };
     69      });
     70 
     71    return {
     72      columnNumber: stack.column - 1,
     73      exception: this.serialize(
     74        exception,
     75        lazy.setDefaultSerializationOptions(),
     76        resultOwnership,
     77        realm,
     78        { seenNodeIds }
     79      ),
     80      lineNumber: stack.line - 1,
     81      stackTrace: { callFrames },
     82      text: lazy.stringify(exception),
     83    };
     84  }
     85 
     86  async #buildReturnValue(
     87    rv,
     88    realm,
     89    awaitPromise,
     90    resultOwnership,
     91    serializationOptions
     92  ) {
     93    let evaluationStatus, exception, result, stack;
     94 
     95    if ("return" in rv) {
     96      evaluationStatus = EvaluationStatus.Normal;
     97      if (
     98        awaitPromise &&
     99        // Only non-primitive return values are wrapped in Debugger.Object.
    100        rv.return instanceof Debugger.Object &&
    101        rv.return.isPromise
    102      ) {
    103        try {
    104          // Force wrapping the promise resolution result in a Debugger.Object
    105          // wrapper for consistency with the synchronous codepath.
    106          const asyncResult = await rv.return.unsafeDereference();
    107          result = realm.globalObjectReference.makeDebuggeeValue(asyncResult);
    108        } catch (asyncException) {
    109          evaluationStatus = EvaluationStatus.Throw;
    110          exception =
    111            realm.globalObjectReference.makeDebuggeeValue(asyncException);
    112 
    113          // If the returned promise was rejected by calling its reject callback
    114          // the stack will be available on promiseResolutionSite.
    115          // Otherwise, (eg. rejected Promise chained with a then() call) we
    116          // fallback on the promiseAllocationSite.
    117          stack =
    118            rv.return.promiseResolutionSite || rv.return.promiseAllocationSite;
    119        }
    120      } else {
    121        // rv.return is a Debugger.Object or a primitive.
    122        result = rv.return;
    123      }
    124    } else if ("throw" in rv) {
    125      // rv.throw will be set if the evaluation synchronously failed, either if
    126      // the script contains a syntax error or throws an exception.
    127      evaluationStatus = EvaluationStatus.Throw;
    128      exception = rv.throw;
    129      stack = rv.stack;
    130    }
    131 
    132    const seenNodeIds = new Map();
    133    switch (evaluationStatus) {
    134      case EvaluationStatus.Normal: {
    135        const dataSuccess = this.serialize(
    136          this.#toRawObject(result),
    137          serializationOptions,
    138          resultOwnership,
    139          realm,
    140          { seenNodeIds }
    141        );
    142 
    143        return {
    144          evaluationStatus,
    145          realmId: realm.id,
    146          result: dataSuccess,
    147          _extraData: { seenNodeIds },
    148        };
    149      }
    150      case EvaluationStatus.Throw: {
    151        const dataThrow = this.#buildExceptionDetails(
    152          exception,
    153          stack,
    154          realm,
    155          resultOwnership,
    156          seenNodeIds
    157        );
    158 
    159        return {
    160          evaluationStatus,
    161          exceptionDetails: dataThrow,
    162          realmId: realm.id,
    163          _extraData: { seenNodeIds },
    164        };
    165      }
    166      default:
    167        throw new lazy.error.UnsupportedOperationError(
    168          `Unsupported completion value for expression evaluation`
    169        );
    170    }
    171  }
    172 
    173  /**
    174   * Emit "script.message" event with provided data.
    175   *
    176   * @param {Realm} realm
    177   * @param {ChannelProperties} channelProperties
    178   * @param {RemoteValue} message
    179   */
    180  #emitScriptMessage = (realm, channelProperties, message) => {
    181    const {
    182      channel,
    183      ownership: ownershipType = lazy.OwnershipModel.None,
    184      serializationOptions,
    185    } = channelProperties;
    186 
    187    const seenNodeIds = new Map();
    188    const data = this.serialize(
    189      this.#toRawObject(message),
    190      lazy.setDefaultSerializationOptions(serializationOptions),
    191      ownershipType,
    192      realm,
    193      { seenNodeIds }
    194    );
    195 
    196    this.emitEvent("script.message", {
    197      channel,
    198      data,
    199      source: this.#getSource(realm),
    200      _extraData: { seenNodeIds },
    201    });
    202  };
    203 
    204  #getSource(realm) {
    205    return {
    206      realm: realm.id,
    207      context: this.messageHandler.context,
    208    };
    209  }
    210 
    211  #toRawObject(maybeDebuggerObject) {
    212    if (maybeDebuggerObject instanceof Debugger.Object) {
    213      // Retrieve the referent for the provided Debugger.object.
    214      // See https://firefox-source-docs.mozilla.org/devtools-user/debugger-api/debugger.object/index.html
    215      const rawObject = maybeDebuggerObject.unsafeDereference();
    216 
    217      // TODO: Getters for Maps and Sets iterators return "Opaque" objects and
    218      // are not iterable. RemoteValue.sys.mjs' serializer should handle calling
    219      // waiveXrays on Maps/Sets/... and then unwaiveXrays on entries but since
    220      // we serialize with maxDepth=1, calling waiveXrays once on the root
    221      // object allows to return correctly serialized values.
    222      return Cu.waiveXrays(rawObject);
    223    }
    224 
    225    // If maybeDebuggerObject was not a Debugger.Object, it is a primitive value
    226    // which can be used as is.
    227    return maybeDebuggerObject;
    228  }
    229 
    230  /**
    231   * Call a function in the current window global.
    232   *
    233   * @param {object} options
    234   * @param {boolean} options.awaitPromise
    235   *     Determines if the command should wait for the return value of the
    236   *     expression to resolve, if this return value is a Promise.
    237   * @param {Array<RemoteValue>=} options.commandArguments
    238   *     The arguments to pass to the function call.
    239   * @param {string} options.functionDeclaration
    240   *     The body of the function to call.
    241   * @param {string=} options.realmId
    242   *     The id of the realm.
    243   * @param {OwnershipModel} options.resultOwnership
    244   *     The ownership model to use for the results of this evaluation.
    245   * @param {string=} options.sandbox
    246   *     The name of the sandbox.
    247   * @param {SerializationOptions=} options.serializationOptions
    248   *     An object which holds the information of how the result of evaluation
    249   *     in case of ECMAScript objects should be serialized.
    250   * @param {RemoteValue=} options.thisParameter
    251   *     The value of the this keyword for the function call.
    252   * @param {boolean=} options.userActivation
    253   *     Determines whether execution should be treated as initiated by user.
    254   *
    255   * @returns {object}
    256   *     - evaluationStatus {EvaluationStatus} One of "normal", "throw".
    257   *     - exceptionDetails {ExceptionDetails=} the details of the exception if
    258   *     the evaluation status was "throw".
    259   *     - result {RemoteValue=} the result of the evaluation serialized as a
    260   *     RemoteValue if the evaluation status was "normal".
    261   */
    262  async callFunctionDeclaration(options) {
    263    const {
    264      awaitPromise,
    265      commandArguments = null,
    266      functionDeclaration,
    267      realmId = null,
    268      resultOwnership,
    269      sandbox: sandboxName = null,
    270      serializationOptions,
    271      thisParameter = null,
    272      userActivation,
    273    } = options;
    274 
    275    const realm = this.messageHandler.getRealm({ realmId, sandboxName });
    276 
    277    const deserializedArguments =
    278      commandArguments !== null
    279        ? commandArguments.map(arg =>
    280            this.deserialize(arg, realm, {
    281              emitScriptMessage: this.#emitScriptMessage,
    282            })
    283          )
    284        : [];
    285 
    286    const deserializedThis =
    287      thisParameter !== null
    288        ? this.deserialize(thisParameter, realm, {
    289            emitScriptMessage: this.#emitScriptMessage,
    290          })
    291        : null;
    292 
    293    realm.userActivationEnabled = userActivation;
    294 
    295    const rv = realm.executeInGlobalWithBindings(
    296      functionDeclaration,
    297      deserializedArguments,
    298      deserializedThis
    299    );
    300 
    301    return this.#buildReturnValue(
    302      rv,
    303      realm,
    304      awaitPromise,
    305      resultOwnership,
    306      serializationOptions
    307    );
    308  }
    309 
    310  /**
    311   * Delete the provided handles from the realm corresponding to the provided
    312   * sandbox name.
    313   *
    314   * @param {object=} options
    315   * @param {Array<string>} options.handles
    316   *     Array of handle ids to disown.
    317   * @param {string=} options.realmId
    318   *     The id of the realm.
    319   * @param {string=} options.sandbox
    320   *     The name of the sandbox.
    321   */
    322  disownHandles(options) {
    323    const { handles, realmId = null, sandbox: sandboxName = null } = options;
    324    const realm = this.messageHandler.getRealm({ realmId, sandboxName });
    325    for (const handle of handles) {
    326      realm.removeObjectHandle(handle);
    327    }
    328  }
    329 
    330  /**
    331   * Evaluate a provided expression in the current window global.
    332   *
    333   * @param {object} options
    334   * @param {boolean} options.awaitPromise
    335   *     Determines if the command should wait for the return value of the
    336   *     expression to resolve, if this return value is a Promise.
    337   * @param {string} options.expression
    338   *     The expression to evaluate.
    339   * @param {string=} options.realmId
    340   *     The id of the realm.
    341   * @param {OwnershipModel} options.resultOwnership
    342   *     The ownership model to use for the results of this evaluation.
    343   * @param {string=} options.sandbox
    344   *     The name of the sandbox.
    345   * @param {boolean=} options.userActivation
    346   *     Determines whether execution should be treated as initiated by user.
    347   *
    348   * @returns {object}
    349   *     - evaluationStatus {EvaluationStatus} One of "normal", "throw".
    350   *     - exceptionDetails {ExceptionDetails=} the details of the exception if
    351   *     the evaluation status was "throw".
    352   *     - result {RemoteValue=} the result of the evaluation serialized as a
    353   *     RemoteValue if the evaluation status was "normal".
    354   */
    355  async evaluateExpression(options) {
    356    const {
    357      awaitPromise,
    358      expression,
    359      realmId = null,
    360      resultOwnership,
    361      sandbox: sandboxName = null,
    362      serializationOptions,
    363      userActivation,
    364    } = options;
    365 
    366    const realm = this.messageHandler.getRealm({ realmId, sandboxName });
    367 
    368    realm.userActivationEnabled = userActivation;
    369 
    370    const rv = realm.executeInGlobal(expression);
    371 
    372    return this.#buildReturnValue(
    373      rv,
    374      realm,
    375      awaitPromise,
    376      resultOwnership,
    377      serializationOptions
    378    );
    379  }
    380 
    381  /**
    382   * Get realms for the current window global.
    383   *
    384   * @returns {Array<object>}
    385   *     - context {BrowsingContext} The browsing context, associated with the realm.
    386   *     - origin {string} The serialization of an origin.
    387   *     - realm {string} The realm unique identifier.
    388   *     - sandbox {string=} The name of the sandbox.
    389   *     - type {RealmType.Window} The window realm type.
    390   */
    391  getWindowRealms() {
    392    return Array.from(this.messageHandler.realms.values()).map(realm => {
    393      const { context, origin, realm: id, sandbox, type } = realm.getInfo();
    394      return { context, origin, realm: id, sandbox, type };
    395    });
    396  }
    397 
    398  /**
    399   * Internal commands
    400   */
    401 
    402  _applySessionData() {}
    403 
    404  /**
    405   * Evaluate a provided list of preload scripts in the current window global.
    406   *
    407   * @param {object} options
    408   * @param {Array<string>} options.scripts
    409   *     The list of scripts to evaluate.
    410   */
    411  _evaluatePreloadScripts(options) {
    412    const { scripts } = options;
    413 
    414    for (const script of scripts) {
    415      const {
    416        arguments: commandArguments,
    417        functionDeclaration,
    418        sandbox,
    419      } = script;
    420      const realm = this.messageHandler.getRealm({ sandboxName: sandbox });
    421      const deserializedArguments = commandArguments.map(arg =>
    422        this.deserialize(arg, realm, {
    423          emitScriptMessage: this.#emitScriptMessage,
    424        })
    425      );
    426      const rv = realm.executeInGlobalWithBindings(
    427        functionDeclaration,
    428        deserializedArguments
    429      );
    430 
    431      if ("throw" in rv) {
    432        const exception = this.#toRawObject(rv.throw);
    433        realm.reportError(lazy.stringify(exception), rv.stack);
    434      }
    435    }
    436  }
    437 }
    438 
    439 export const script = ScriptModule;