tor-browser

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

script.sys.mjs (33078B)


      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 { RootBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/RootBiDiModule.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
     11  BrowsingContextListener:
     12    "chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs",
     13  ContextDescriptorType:
     14    "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs",
     15  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
     16  generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
     17  NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs",
     18  OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
     19  pprint: "chrome://remote/content/shared/Format.sys.mjs",
     20  processExtraData:
     21    "chrome://remote/content/webdriver-bidi/modules/Intercept.sys.mjs",
     22  RealmType: "chrome://remote/content/shared/Realm.sys.mjs",
     23  SessionDataMethod:
     24    "chrome://remote/content/shared/messagehandler/sessiondata/SessionData.sys.mjs",
     25  setDefaultAndAssertSerializationOptions:
     26    "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
     27  UserContextManager:
     28    "chrome://remote/content/shared/UserContextManager.sys.mjs",
     29  WindowGlobalMessageHandler:
     30    "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs",
     31 });
     32 
     33 /**
     34 * @typedef {string} ScriptEvaluateResultType
     35 */
     36 
     37 /**
     38 * Enum of possible evaluation result types.
     39 *
     40 * @readonly
     41 * @enum {ScriptEvaluateResultType}
     42 */
     43 const ScriptEvaluateResultType = {
     44  Exception: "exception",
     45  Success: "success",
     46 };
     47 
     48 /**
     49 * An object that holds information about the preload script.
     50 *
     51 * @typedef PreloadScript
     52 *
     53 * @property {Array<ChannelValue>=} arguments
     54 *    The arguments to pass to the function call.
     55 * @property {Array<string>=} navigables
     56 *    The list of navigable browser ids where
     57 *    the preload script should be executed.
     58 * @property {string} functionDeclaration
     59 *    The expression to evaluate.
     60 * @property {string=} sandbox
     61 *    The name of the sandbox.
     62 * @property {Array<string>=} userContexts
     63 *    The list of internal user context ids where
     64 *    the preload script should be executed.
     65 */
     66 
     67 class ScriptModule extends RootBiDiModule {
     68  #contextListener;
     69  #preloadScriptMap;
     70  #realmInfoMap;
     71  #subscribedEvents;
     72 
     73  constructor(messageHandler) {
     74    super(messageHandler);
     75 
     76    this.#contextListener = new lazy.BrowsingContextListener();
     77    this.#contextListener.on("attached", this.#onContextAttached);
     78 
     79    // Map in which the keys are UUIDs, and the values are structs
     80    // of the type PreloadScript.
     81    this.#preloadScriptMap = new Map();
     82 
     83    // Map with browsing contexts as keys and realm info object
     84    // as values.
     85    this.#realmInfoMap = new WeakMap();
     86 
     87    // Set of event names which have active subscriptions.
     88    this.#subscribedEvents = new Set();
     89  }
     90 
     91  destroy() {
     92    this.#contextListener.off("attached", this.#onContextAttached);
     93    this.#contextListener.destroy();
     94 
     95    this.#preloadScriptMap = null;
     96    this.#realmInfoMap = null;
     97    this.#subscribedEvents = null;
     98  }
     99 
    100  /**
    101   * Used as return value for script.addPreloadScript command.
    102   *
    103   * @typedef AddPreloadScriptResult
    104   *
    105   * @property {string} script
    106   *    The unique id associated with added preload script.
    107   */
    108 
    109  /**
    110   * @typedef ChannelProperties
    111   *
    112   * @property {string} channel
    113   *     The channel id.
    114   * @property {SerializationOptions=} serializationOptions
    115   *     An object which holds the information of how the result of evaluation
    116   *     in case of ECMAScript objects should be serialized.
    117   * @property {OwnershipModel=} ownership
    118   *     The ownership model to use for the results of this evaluation. Defaults
    119   *     to `OwnershipModel.None`.
    120   */
    121 
    122  /**
    123   * Represents a channel used to send custom messages from preload script
    124   * to clients.
    125   *
    126   * @typedef ChannelValue
    127   *
    128   * @property {'channel'} type
    129   * @property {ChannelProperties} value
    130   */
    131 
    132  /**
    133   * Adds a preload script, which runs on creation of a new Window,
    134   * before any author-defined script have run.
    135   *
    136   * @param {object=} options
    137   * @param {Array<ChannelValue>=} options.arguments
    138   *     The arguments to pass to the function call.
    139   * @param {Array<string>=} options.contexts
    140   *     The list of the browsing context ids.
    141   * @param {string} options.functionDeclaration
    142   *     The expression to evaluate.
    143   * @param {string=} options.sandbox
    144   *     The name of the sandbox. If the value is null or empty
    145   *     string, the default realm will be used.
    146   * @param {Array<string>=} options.userContexts
    147   *     The list of the user context ids.
    148   *
    149   * @returns {AddPreloadScriptResult}
    150   *
    151   * @throws {InvalidArgumentError}
    152   *     If any of the arguments does not have the expected type.
    153   */
    154  async addPreloadScript(options = {}) {
    155    const {
    156      arguments: commandArguments = [],
    157      contexts: contextIds = null,
    158      functionDeclaration,
    159      sandbox = null,
    160      userContexts: userContextIds = null,
    161    } = options;
    162    let userContexts = null;
    163    let navigables = null;
    164 
    165    if (contextIds !== null) {
    166      lazy.assert.isNonEmptyArray(
    167        contextIds,
    168        lazy.pprint`Expected "contexts" to be a non-empty array, got ${contextIds}`
    169      );
    170 
    171      for (const contextId of contextIds) {
    172        lazy.assert.string(
    173          contextId,
    174          lazy.pprint`Expected elements of "contexts" to be a string, got ${contextId}`
    175        );
    176      }
    177    } else if (userContextIds !== null) {
    178      lazy.assert.isNonEmptyArray(
    179        userContextIds,
    180        lazy.pprint`Expected "userContextIds" to be a non-empty array, got ${userContextIds}`
    181      );
    182 
    183      for (const userContextId of userContextIds) {
    184        lazy.assert.string(
    185          userContextId,
    186          lazy.pprint`Expected elements of "userContexts" to be a string, got ${userContextId}`
    187        );
    188      }
    189    }
    190 
    191    lazy.assert.string(
    192      functionDeclaration,
    193      lazy.pprint`Expected "functionDeclaration" to be a string, got ${functionDeclaration}`
    194    );
    195 
    196    if (sandbox != null) {
    197      lazy.assert.string(
    198        sandbox,
    199        lazy.pprint`Expected "sandbox" to be a string, got ${sandbox}`
    200      );
    201    }
    202 
    203    lazy.assert.array(
    204      commandArguments,
    205      lazy.pprint`Expected "arguments" to be an array, got ${commandArguments}`
    206    );
    207 
    208    commandArguments.forEach(({ type, value }) => {
    209      lazy.assert.that(
    210        t => t === "channel",
    211        lazy.pprint`Expected argument "type" to be "channel", got ${type}`
    212      )(type);
    213      this.#assertChannelArgument(value);
    214    });
    215 
    216    if (contextIds !== null && userContextIds !== null) {
    217      throw new lazy.error.InvalidArgumentError(
    218        `Providing both "contexts" and "userContexts" arguments is not supported`
    219      );
    220    }
    221 
    222    if (contextIds !== null) {
    223      navigables = new Set();
    224 
    225      for (const contextId of contextIds) {
    226        const context = this._getNavigable(contextId);
    227 
    228        lazy.assert.topLevel(
    229          context,
    230          lazy.pprint`Browsing context with id ${contextId} is not top-level`
    231        );
    232 
    233        navigables.add(context.browserId);
    234      }
    235    } else if (userContextIds !== null) {
    236      userContexts = new Set();
    237 
    238      for (const userContextId of userContextIds) {
    239        const internalId =
    240          lazy.UserContextManager.getInternalIdById(userContextId);
    241 
    242        if (internalId === null) {
    243          throw new lazy.error.NoSuchUserContextError(
    244            `User context with id: ${userContextId} doesn't exist`
    245          );
    246        }
    247 
    248        userContexts.add(internalId);
    249      }
    250    }
    251 
    252    const script = lazy.generateUUID();
    253    const preloadScript = {
    254      arguments: commandArguments,
    255      contexts: navigables,
    256      functionDeclaration,
    257      sandbox,
    258      userContexts,
    259    };
    260 
    261    this.#preloadScriptMap.set(script, preloadScript);
    262 
    263    const preloadScriptDataItem = {
    264      category: "preload-script",
    265      moduleName: "_configuration",
    266      values: [
    267        {
    268          ...preloadScript,
    269          script,
    270        },
    271      ],
    272    };
    273 
    274    if (navigables === null && userContexts === null) {
    275      await this.messageHandler.addSessionDataItem({
    276        ...preloadScriptDataItem,
    277        contextDescriptor: {
    278          type: lazy.ContextDescriptorType.All,
    279        },
    280      });
    281    } else {
    282      const preloadScriptDataItems = [];
    283 
    284      if (navigables === null) {
    285        for (const id of userContexts) {
    286          preloadScriptDataItems.push({
    287            ...preloadScriptDataItem,
    288            contextDescriptor: {
    289              type: lazy.ContextDescriptorType.UserContext,
    290              id,
    291            },
    292            method: lazy.SessionDataMethod.Add,
    293          });
    294        }
    295      } else {
    296        for (const id of navigables) {
    297          preloadScriptDataItems.push({
    298            ...preloadScriptDataItem,
    299            contextDescriptor: {
    300              type: lazy.ContextDescriptorType.TopBrowsingContext,
    301              id,
    302            },
    303            method: lazy.SessionDataMethod.Add,
    304          });
    305        }
    306      }
    307 
    308      await this.messageHandler.updateSessionData(preloadScriptDataItems);
    309    }
    310 
    311    return { script };
    312  }
    313 
    314  /**
    315   * Used to represent a frame of a JavaScript stack trace.
    316   *
    317   * @typedef StackFrame
    318   *
    319   * @property {number} columnNumber
    320   * @property {string} functionName
    321   * @property {number} lineNumber
    322   * @property {string} url
    323   */
    324 
    325  /**
    326   * Used to represent a JavaScript stack at a point in script execution.
    327   *
    328   * @typedef StackTrace
    329   *
    330   * @property {Array<StackFrame>} callFrames
    331   */
    332 
    333  /**
    334   * Used to represent a JavaScript exception.
    335   *
    336   * @typedef ExceptionDetails
    337   *
    338   * @property {number} columnNumber
    339   * @property {RemoteValue} exception
    340   * @property {number} lineNumber
    341   * @property {StackTrace} stackTrace
    342   * @property {string} text
    343   */
    344 
    345  /**
    346   * Used as return value for script.evaluate, as one of the available variants
    347   * {ScriptEvaluateResultException} or {ScriptEvaluateResultSuccess}.
    348   *
    349   * @typedef ScriptEvaluateResult
    350   */
    351 
    352  /**
    353   * Used as return value for script.evaluate when the script completes with a
    354   * thrown exception.
    355   *
    356   * @typedef ScriptEvaluateResultException
    357   *
    358   * @property {ExceptionDetails} exceptionDetails
    359   * @property {string} realm
    360   * @property {ScriptEvaluateResultType} [type=ScriptEvaluateResultType.Exception]
    361   */
    362 
    363  /**
    364   * Used as return value for script.evaluate when the script completes
    365   * normally.
    366   *
    367   * @typedef ScriptEvaluateResultSuccess
    368   *
    369   * @property {string} realm
    370   * @property {RemoteValue} result
    371   * @property {ScriptEvaluateResultType} [type=ScriptEvaluateResultType.Success]
    372   */
    373 
    374  /**
    375   * Calls a provided function with given arguments and scope in the provided
    376   * target, which is either a realm or a browsing context.
    377   *
    378   * @param {object=} options
    379   * @param {Array<RemoteValue>=} options.arguments
    380   *     The arguments to pass to the function call.
    381   * @param {boolean} options.awaitPromise
    382   *     Determines if the command should wait for the return value of the
    383   *     expression to resolve, if this return value is a Promise.
    384   * @param {string} options.functionDeclaration
    385   *     The expression to evaluate.
    386   * @param {OwnershipModel=} options.resultOwnership
    387   *     The ownership model to use for the results of this evaluation. Defaults
    388   *     to `OwnershipModel.None`.
    389   * @param {SerializationOptions=} options.serializationOptions
    390   *     An object which holds the information of how the result of evaluation
    391   *     in case of ECMAScript objects should be serialized.
    392   * @param {object} options.target
    393   *     The target for the evaluation, which either matches the definition for
    394   *     a RealmTarget or for ContextTarget.
    395   * @param {RemoteValue=} options.this
    396   *     The value of the this keyword for the function call.
    397   * @param {boolean=} options.userActivation
    398   *     Determines whether execution should be treated as initiated by user.
    399   *     Defaults to `false`.
    400   *
    401   * @returns {ScriptEvaluateResult}
    402   *
    403   * @throws {InvalidArgumentError}
    404   *     If any of the arguments does not have the expected type.
    405   * @throws {NoSuchFrameError}
    406   *     If the target cannot be found.
    407   */
    408  async callFunction(options = {}) {
    409    const {
    410      arguments: commandArguments = null,
    411      awaitPromise,
    412      functionDeclaration,
    413      resultOwnership = lazy.OwnershipModel.None,
    414      serializationOptions,
    415      target = {},
    416      this: thisParameter = null,
    417      userActivation = false,
    418    } = options;
    419 
    420    lazy.assert.string(
    421      functionDeclaration,
    422      lazy.pprint`Expected "functionDeclaration" to be a string, got ${functionDeclaration}`
    423    );
    424 
    425    lazy.assert.boolean(
    426      awaitPromise,
    427      lazy.pprint`Expected "awaitPromise" to be a boolean, got ${awaitPromise}`
    428    );
    429 
    430    lazy.assert.boolean(
    431      userActivation,
    432      lazy.pprint`Expected "userActivation" to be a boolean, got ${userActivation}`
    433    );
    434 
    435    this.#assertResultOwnership(resultOwnership);
    436 
    437    if (commandArguments != null) {
    438      lazy.assert.array(
    439        commandArguments,
    440        lazy.pprint`Expected "arguments" to be an array, got ${commandArguments}`
    441      );
    442      commandArguments.forEach(({ type, value }) => {
    443        if (type === "channel") {
    444          this.#assertChannelArgument(value);
    445        }
    446      });
    447    }
    448 
    449    const { contextId, realmId, sandbox } = this.#assertTarget(target);
    450    const context = await this.#getContextFromTarget({
    451      contextId,
    452      realmId,
    453      supportsChromeScope: true,
    454    });
    455 
    456    const serializationOptionsWithDefaults =
    457      lazy.setDefaultAndAssertSerializationOptions(serializationOptions);
    458 
    459    const evaluationResult = await this._forwardToWindowGlobal(
    460      "callFunctionDeclaration",
    461      context.id,
    462      {
    463        awaitPromise,
    464        commandArguments,
    465        functionDeclaration,
    466        realmId,
    467        resultOwnership,
    468        sandbox,
    469        serializationOptions: serializationOptionsWithDefaults,
    470        thisParameter,
    471        userActivation,
    472      }
    473    );
    474 
    475    return this.#buildReturnValue(evaluationResult);
    476  }
    477 
    478  /**
    479   * The script.disown command disowns the given handles. This does not
    480   * guarantee the handled object will be garbage collected, as there can be
    481   * other handles or strong ECMAScript references.
    482   *
    483   * @param {object=} options
    484   * @param {Array<string>} options.handles
    485   *     Array of handle ids to disown.
    486   * @param {object} options.target
    487   *     The target owning the handles, which either matches the definition for
    488   *     a RealmTarget or for ContextTarget.
    489   */
    490  async disown(options = {}) {
    491    const { handles, target = {} } = options;
    492 
    493    lazy.assert.array(
    494      handles,
    495      lazy.pprint`Expected "handles" to be an array, got ${handles}`
    496    );
    497    handles.forEach(handle => {
    498      lazy.assert.string(
    499        handle,
    500        lazy.pprint`Expected "handles" to be an array of strings, got ${handle}`
    501      );
    502    });
    503 
    504    const { contextId, realmId, sandbox } = this.#assertTarget(target);
    505    const context = await this.#getContextFromTarget({ contextId, realmId });
    506    await this._forwardToWindowGlobal("disownHandles", context.id, {
    507      handles,
    508      realmId,
    509      sandbox,
    510    });
    511  }
    512 
    513  /**
    514   * Evaluate a provided expression in the provided target, which is either a
    515   * realm or a browsing context.
    516   *
    517   * @param {object=} options
    518   * @param {boolean} options.awaitPromise
    519   *     Determines if the command should wait for the return value of the
    520   *     expression to resolve, if this return value is a Promise.
    521   * @param {string} options.expression
    522   *     The expression to evaluate.
    523   * @param {OwnershipModel=} options.resultOwnership
    524   *     The ownership model to use for the results of this evaluation. Defaults
    525   *     to `OwnershipModel.None`.
    526   * @param {SerializationOptions=} options.serializationOptions
    527   *     An object which holds the information of how the result of evaluation
    528   *     in case of ECMAScript objects should be serialized.
    529   * @param {object} options.target
    530   *     The target for the evaluation, which either matches the definition for
    531   *     a RealmTarget or for ContextTarget.
    532   * @param {boolean=} options.userActivation
    533   *     Determines whether execution should be treated as initiated by user.
    534   *     Defaults to `false`.
    535   *
    536   * @returns {ScriptEvaluateResult}
    537   *
    538   * @throws {InvalidArgumentError}
    539   *     If any of the arguments does not have the expected type.
    540   * @throws {NoSuchFrameError}
    541   *     If the target cannot be found.
    542   */
    543  async evaluate(options = {}) {
    544    const {
    545      awaitPromise,
    546      expression: source,
    547      resultOwnership = lazy.OwnershipModel.None,
    548      serializationOptions,
    549      target = {},
    550      userActivation = false,
    551    } = options;
    552 
    553    lazy.assert.string(
    554      source,
    555      lazy.pprint`Expected "expression" to be a string, got ${source}`
    556    );
    557 
    558    lazy.assert.boolean(
    559      awaitPromise,
    560      lazy.pprint`Expected "awaitPromise" to be a boolean, got ${awaitPromise}`
    561    );
    562 
    563    lazy.assert.boolean(
    564      userActivation,
    565      lazy.pprint`Expected "userActivation" to be a boolean, got ${userActivation}`
    566    );
    567 
    568    this.#assertResultOwnership(resultOwnership);
    569 
    570    const { contextId, realmId, sandbox } = this.#assertTarget(target);
    571    const context = await this.#getContextFromTarget({
    572      contextId,
    573      realmId,
    574      supportsChromeScope: true,
    575    });
    576 
    577    const serializationOptionsWithDefaults =
    578      lazy.setDefaultAndAssertSerializationOptions(serializationOptions);
    579 
    580    const evaluationResult = await this._forwardToWindowGlobal(
    581      "evaluateExpression",
    582      context.id,
    583      {
    584        awaitPromise,
    585        expression: source,
    586        realmId,
    587        resultOwnership,
    588        sandbox,
    589        serializationOptions: serializationOptionsWithDefaults,
    590        userActivation,
    591      }
    592    );
    593 
    594    return this.#buildReturnValue(evaluationResult);
    595  }
    596 
    597  /**
    598   * An object that holds basic information about a realm.
    599   *
    600   * @typedef BaseRealmInfo
    601   *
    602   * @property {string} id
    603   *     The realm unique identifier.
    604   * @property {string} origin
    605   *     The serialization of an origin.
    606   */
    607 
    608  /**
    609   *
    610   * @typedef WindowRealmInfoProperties
    611   *
    612   * @property {string} context
    613   *     The browsing context id, associated with the realm.
    614   * @property {string=} sandbox
    615   *     The name of the sandbox. If the value is null or empty
    616   *     string, the default realm will be returned.
    617   * @property {RealmType.Window} type
    618   *     The window realm type.
    619   */
    620 
    621  /* eslint-disable jsdoc/valid-types */
    622  /**
    623   * An object that holds information about a window realm.
    624   *
    625   * @typedef {BaseRealmInfo & WindowRealmInfoProperties} WindowRealmInfo
    626   */
    627  /* eslint-enable jsdoc/valid-types */
    628 
    629  /**
    630   * An object that holds information about a realm.
    631   *
    632   * @typedef {WindowRealmInfo} RealmInfo
    633   */
    634 
    635  /**
    636   * An object that holds a list of realms.
    637   *
    638   * @typedef ScriptGetRealmsResult
    639   *
    640   * @property {Array<RealmInfo>} realms
    641   *     List of realms.
    642   */
    643 
    644  /**
    645   * Returns a list of all realms, optionally filtered to realms
    646   * of a specific type, or to the realms associated with
    647   * a specified browsing context.
    648   *
    649   * @param {object=} options
    650   * @param {string=} options.context
    651   *     The id of the browsing context to filter
    652   *     only realms associated with it. If not provided, return realms
    653   *     associated with all browsing contexts.
    654   * @param {RealmType=} options.type
    655   *     Type of realm to filter.
    656   *     If not provided, return realms of all types.
    657   *
    658   * @returns {ScriptGetRealmsResult}
    659   *
    660   * @throws {InvalidArgumentError}
    661   *     If any of the arguments does not have the expected type.
    662   * @throws {NoSuchFrameError}
    663   *     If the context cannot be found.
    664   */
    665  async getRealms(options = {}) {
    666    const { context: contextId = null, type = null } = options;
    667    const destination = {};
    668 
    669    if (contextId !== null) {
    670      lazy.assert.string(
    671        contextId,
    672        lazy.pprint`Expected "context" to be a string, got ${contextId}`
    673      );
    674      destination.id = this._getNavigable(contextId).id;
    675    } else {
    676      destination.contextDescriptor = {
    677        type: lazy.ContextDescriptorType.All,
    678      };
    679    }
    680 
    681    if (type !== null) {
    682      const supportedRealmTypes = Object.values(lazy.RealmType);
    683      if (!supportedRealmTypes.includes(type)) {
    684        throw new lazy.error.InvalidArgumentError(
    685          `Expected "type" to be one of ${supportedRealmTypes}, got ${type}`
    686        );
    687      }
    688 
    689      // Remove this check when other realm types are supported
    690      if (type !== lazy.RealmType.Window) {
    691        throw new lazy.error.UnsupportedOperationError(
    692          `Unsupported "type": ${type}. Only "type" ${lazy.RealmType.Window} is currently supported.`
    693        );
    694      }
    695    }
    696 
    697    return { realms: await this.#getRealmInfos(destination) };
    698  }
    699 
    700  /**
    701   * Removes a preload script.
    702   *
    703   * @param {object=} options
    704   * @param {string} options.script
    705   *     The unique id associated with a preload script.
    706   *
    707   * @throws {InvalidArgumentError}
    708   *     If any of the arguments does not have the expected type.
    709   * @throws {NoSuchScriptError}
    710   *     If the script cannot be found.
    711   */
    712  async removePreloadScript(options = {}) {
    713    const { script } = options;
    714 
    715    lazy.assert.string(
    716      script,
    717      lazy.pprint`Expected "script" to be a string, got ${script}`
    718    );
    719 
    720    if (!this.#preloadScriptMap.has(script)) {
    721      throw new lazy.error.NoSuchScriptError(
    722        `Preload script with id ${script} not found`
    723      );
    724    }
    725 
    726    const preloadScript = this.#preloadScriptMap.get(script);
    727    const sessionDataItem = {
    728      category: "preload-script",
    729      moduleName: "_configuration",
    730      values: [
    731        {
    732          ...preloadScript,
    733          script,
    734        },
    735      ],
    736    };
    737 
    738    if (
    739      preloadScript.contexts === null &&
    740      preloadScript.userContexts === null
    741    ) {
    742      await this.messageHandler.removeSessionDataItem({
    743        ...sessionDataItem,
    744        contextDescriptor: {
    745          type: lazy.ContextDescriptorType.All,
    746        },
    747      });
    748    } else {
    749      const sessionDataItemToUpdate = [];
    750 
    751      if (preloadScript.contexts === null) {
    752        for (const id of preloadScript.userContexts) {
    753          sessionDataItemToUpdate.push({
    754            ...sessionDataItem,
    755            contextDescriptor: {
    756              type: lazy.ContextDescriptorType.UserContext,
    757              id,
    758            },
    759            method: lazy.SessionDataMethod.Remove,
    760          });
    761        }
    762      } else {
    763        for (const id of preloadScript.contexts) {
    764          sessionDataItemToUpdate.push({
    765            ...sessionDataItem,
    766            contextDescriptor: {
    767              type: lazy.ContextDescriptorType.TopBrowsingContext,
    768              id,
    769            },
    770            method: lazy.SessionDataMethod.Remove,
    771          });
    772        }
    773      }
    774 
    775      await this.messageHandler.updateSessionData(sessionDataItemToUpdate);
    776    }
    777 
    778    this.#preloadScriptMap.delete(script);
    779  }
    780 
    781  #assertChannelArgument(value) {
    782    lazy.assert.object(
    783      value,
    784      lazy.pprint`Expected channel argument to be an object, got ${value}`
    785    );
    786    const {
    787      channel,
    788      ownership = lazy.OwnershipModel.None,
    789      serializationOptions,
    790    } = value;
    791    lazy.assert.string(
    792      channel,
    793      lazy.pprint`Expected channel argument "channel" to be a string, got ${channel}`
    794    );
    795    lazy.setDefaultAndAssertSerializationOptions(serializationOptions);
    796    lazy.assert.that(
    797      ownershipValue =>
    798        [lazy.OwnershipModel.None, lazy.OwnershipModel.Root].includes(
    799          ownershipValue
    800        ),
    801      `Expected channel argument "ownership" to be one of ${Object.values(
    802        lazy.OwnershipModel
    803      )}, ` + lazy.pprint`got ${ownership}`
    804    )(ownership);
    805 
    806    return true;
    807  }
    808 
    809  #assertResultOwnership(resultOwnership) {
    810    if (
    811      ![lazy.OwnershipModel.None, lazy.OwnershipModel.Root].includes(
    812        resultOwnership
    813      )
    814    ) {
    815      throw new lazy.error.InvalidArgumentError(
    816        `Expected "resultOwnership" to be one of ${Object.values(
    817          lazy.OwnershipModel
    818        )}, ` + lazy.pprint`got ${resultOwnership}`
    819      );
    820    }
    821  }
    822 
    823  #assertTarget(target) {
    824    lazy.assert.object(
    825      target,
    826      lazy.pprint`Expected "target" to be an object, got ${target}`
    827    );
    828 
    829    const { context: contextId = null, sandbox = null } = target;
    830    let { realm: realmId = null } = target;
    831 
    832    if (contextId != null) {
    833      lazy.assert.string(
    834        contextId,
    835        lazy.pprint`Expected target "context" to be a string, got ${contextId}`
    836      );
    837 
    838      if (sandbox != null) {
    839        lazy.assert.string(
    840          sandbox,
    841          lazy.pprint`Expected target "sandbox" to be a string, got ${sandbox}`
    842        );
    843      }
    844 
    845      // Ignore realm if context is provided.
    846      realmId = null;
    847    } else if (realmId != null) {
    848      lazy.assert.string(
    849        realmId,
    850        lazy.pprint`Expected target "realm" to be a string, got ${realmId}`
    851      );
    852    } else {
    853      throw new lazy.error.InvalidArgumentError(`No context or realm provided`);
    854    }
    855 
    856    return { contextId, realmId, sandbox };
    857  }
    858 
    859  #buildReturnValue(evaluationResult) {
    860    evaluationResult = lazy.processExtraData(
    861      this.messageHandler.sessionId,
    862      evaluationResult
    863    );
    864 
    865    const rv = { realm: evaluationResult.realmId };
    866    switch (evaluationResult.evaluationStatus) {
    867      // TODO: Compare with EvaluationStatus.Normal after Bug 1774444 is fixed.
    868      case "normal":
    869        rv.type = ScriptEvaluateResultType.Success;
    870        rv.result = evaluationResult.result;
    871        break;
    872      // TODO: Compare with EvaluationStatus.Throw after Bug 1774444 is fixed.
    873      case "throw":
    874        rv.type = ScriptEvaluateResultType.Exception;
    875        rv.exceptionDetails = evaluationResult.exceptionDetails;
    876        break;
    877      default:
    878        throw new lazy.error.UnsupportedOperationError(
    879          `Unsupported evaluation status ${evaluationResult.evaluationStatus}`
    880        );
    881    }
    882    return rv;
    883  }
    884 
    885  async #getContextFromTarget({
    886    contextId,
    887    realmId,
    888    supportsChromeScope = false,
    889  }) {
    890    if (contextId !== null) {
    891      return this._getNavigable(contextId, { supportsChromeScope });
    892    }
    893 
    894    const destination = {
    895      contextDescriptor: {
    896        type: lazy.ContextDescriptorType.All,
    897      },
    898    };
    899    const realmInfos = await this.#getRealmInfos(destination);
    900    const realm = realmInfos.find(info => info.realm == realmId);
    901 
    902    if (realm && realm.context !== null) {
    903      return this._getNavigable(realm.context, { supportsChromeScope });
    904    }
    905 
    906    throw new lazy.error.NoSuchFrameError(`Realm with id ${realmId} not found`);
    907  }
    908 
    909  async #getRealmInfos(destination) {
    910    let realms = await this.messageHandler.forwardCommand({
    911      moduleName: "script",
    912      commandName: "getWindowRealms",
    913      destination: {
    914        type: lazy.WindowGlobalMessageHandler.type,
    915        ...destination,
    916      },
    917      retryOnAbort: true,
    918    });
    919 
    920    const isBroadcast = !!destination.contextDescriptor;
    921    if (!isBroadcast) {
    922      realms = [realms];
    923    }
    924 
    925    return realms
    926      .flat()
    927      .map(realm => {
    928        // Resolve browsing context to a TabManager id.
    929        realm.context = lazy.NavigableManager.getIdForBrowsingContext(
    930          realm.context
    931        );
    932        return realm;
    933      })
    934      .filter(realm => realm.context !== null);
    935  }
    936 
    937  #hasEventSubscriptionToContextCreated(browsingContext) {
    938    const sessionData =
    939      this.messageHandler.sessionData.getSessionDataForContext(
    940        "browsingContext",
    941        "event",
    942        browsingContext
    943      );
    944 
    945    return sessionData.some(
    946      item => item.value === "browsingContext.contextCreated"
    947    );
    948  }
    949 
    950  #onContextAttached = (eventName, data) => {
    951    const { browsingContext } = data;
    952    // If there is a subscription for "browsingContext.contextCreated" event,
    953    // do not send "script.realmCreated" event yet and
    954    // wait until the "browsingContext.contextCreated" event is submitted
    955    if (
    956      this.#realmInfoMap.has(browsingContext) &&
    957      !this.#hasEventSubscriptionToContextCreated(browsingContext)
    958    ) {
    959      this.#sendDelayedRealmCreatedEvent(browsingContext);
    960    }
    961  };
    962 
    963  #onContextCreatedSubmitted = (eventName, { browsingContext }) => {
    964    if (this.#realmInfoMap.has(browsingContext)) {
    965      this.#sendDelayedRealmCreatedEvent(browsingContext);
    966    }
    967  };
    968 
    969  #onRealmCreated = (eventName, { realmInfo }) => {
    970    // Resolve browsing context to a TabManager id.
    971    const context = lazy.NavigableManager.getIdForBrowsingContext(
    972      realmInfo.context
    973    );
    974 
    975    // Do not emit the event, if the browsing context is gone or not created yet.
    976    if (context === null) {
    977      // Save the realm info to send it when the browsing context is ready.
    978      this.#realmInfoMap.set(realmInfo.context, realmInfo);
    979      return;
    980    }
    981    this.#sendRealmCreatedEvent(realmInfo, realmInfo.context, context);
    982  };
    983 
    984  #onRealmDestroyed = (eventName, { realm, context }) => {
    985    this._emitEventForBrowsingContext(context.id, "script.realmDestroyed", {
    986      realm,
    987    });
    988  };
    989 
    990  #sendDelayedRealmCreatedEvent(browsingContext) {
    991    const realmInfo = this.#realmInfoMap.get(browsingContext);
    992    // Resolve browsing context to a TabManager id.
    993    const browsingContextId = lazy.NavigableManager.getIdForBrowsingContext(
    994      realmInfo.context
    995    );
    996    this.#sendRealmCreatedEvent(realmInfo, browsingContext, browsingContextId);
    997    this.#realmInfoMap.delete(browsingContext);
    998  }
    999 
   1000  #sendRealmCreatedEvent(realmInfo, context, browsingContextId) {
   1001    realmInfo.context = browsingContextId;
   1002 
   1003    this._emitEventForBrowsingContext(
   1004      context.id,
   1005      "script.realmCreated",
   1006      realmInfo
   1007    );
   1008  }
   1009 
   1010  #startListeningOnRealmCreated() {
   1011    if (!this.#subscribedEvents.has("script.realmCreated")) {
   1012      this.messageHandler.on("realm-created", this.#onRealmCreated);
   1013      this.messageHandler.on(
   1014        "browsingContext._contextCreatedEmitted",
   1015        this.#onContextCreatedSubmitted
   1016      );
   1017      this.#contextListener.startListening();
   1018    }
   1019  }
   1020 
   1021  #stopListeningOnRealmCreated() {
   1022    if (this.#subscribedEvents.has("script.realmCreated")) {
   1023      this.messageHandler.off("realm-created", this.#onRealmCreated);
   1024      this.messageHandler.off(
   1025        "browsingContext._contextCreatedEmitted",
   1026        this.#onContextCreatedSubmitted
   1027      );
   1028      this.#contextListener.stopListening();
   1029    }
   1030  }
   1031 
   1032  #startListeningOnRealmDestroyed() {
   1033    if (!this.#subscribedEvents.has("script.realmDestroyed")) {
   1034      this.messageHandler.on("realm-destroyed", this.#onRealmDestroyed);
   1035    }
   1036  }
   1037 
   1038  #stopListeningOnRealmDestroyed() {
   1039    if (this.#subscribedEvents.has("script.realmDestroyed")) {
   1040      this.messageHandler.off("realm-destroyed", this.#onRealmDestroyed);
   1041    }
   1042  }
   1043 
   1044  #subscribeEvent(event) {
   1045    switch (event) {
   1046      case "script.realmCreated": {
   1047        this.#startListeningOnRealmCreated();
   1048        this.#subscribedEvents.add(event);
   1049        break;
   1050      }
   1051      case "script.realmDestroyed": {
   1052        this.#startListeningOnRealmDestroyed();
   1053        this.#subscribedEvents.add(event);
   1054        break;
   1055      }
   1056    }
   1057  }
   1058 
   1059  #unsubscribeEvent(event) {
   1060    switch (event) {
   1061      case "script.realmCreated": {
   1062        this.#stopListeningOnRealmCreated();
   1063        this.#subscribedEvents.delete(event);
   1064        break;
   1065      }
   1066      case "script.realmDestroyed": {
   1067        this.#stopListeningOnRealmDestroyed();
   1068        this.#subscribedEvents.delete(event);
   1069        break;
   1070      }
   1071    }
   1072  }
   1073 
   1074  _applySessionData(params) {
   1075    // TODO: Bug 1775231. Move this logic to a shared module or an abstract
   1076    // class.
   1077    const { category } = params;
   1078    if (category === "event") {
   1079      const filteredSessionData = params.sessionData.filter(item =>
   1080        this.messageHandler.matchesContext(item.contextDescriptor)
   1081      );
   1082      for (const event of this.#subscribedEvents.values()) {
   1083        const hasSessionItem = filteredSessionData.some(
   1084          item => item.value === event
   1085        );
   1086        // If there are no session items for this context, we should unsubscribe from the event.
   1087        if (!hasSessionItem) {
   1088          this.#unsubscribeEvent(event);
   1089        }
   1090      }
   1091 
   1092      // Subscribe to all events, which have an item in SessionData.
   1093      for (const { value } of filteredSessionData) {
   1094        this.#subscribeEvent(value);
   1095      }
   1096    }
   1097  }
   1098 
   1099  static get supportedEvents() {
   1100    return ["script.message", "script.realmCreated", "script.realmDestroyed"];
   1101  }
   1102 }
   1103 
   1104 export const script = ScriptModule;