tor-browser

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

input.sys.mjs (12352B)


      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  actions: "chrome://remote/content/shared/webdriver/Actions.sys.mjs",
     11  assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
     12  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
     13  event: "chrome://remote/content/shared/webdriver/Event.sys.mjs",
     14  NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs",
     15  pprint: "chrome://remote/content/shared/Format.sys.mjs",
     16 });
     17 
     18 class InputModule extends RootBiDiModule {
     19  #actionsOptions;
     20  #inputStates;
     21 
     22  constructor(messageHandler) {
     23    super(messageHandler);
     24 
     25    // Browsing context => input state.
     26    // Bug 1821460: Move to WebDriver Session and share with Marionette.
     27    this.#inputStates = new WeakMap();
     28 
     29    // Options for actions to pass through performActions and releaseActions.
     30    this.#actionsOptions = {
     31      // Callbacks as defined in the WebDriver specification.
     32      getElementOrigin: this.#getElementOrigin.bind(this),
     33      isElementOrigin: this.#isElementOrigin.bind(this),
     34 
     35      // Custom callbacks.
     36      assertInViewPort: this.#assertInViewPort.bind(this),
     37      dispatchEvent: this.#dispatchEvent.bind(this),
     38      getClientRects: this.#getClientRects.bind(this),
     39      getInViewCentrePoint: this.#getInViewCentrePoint.bind(this),
     40      toBrowserWindowCoordinates: this.#toBrowserWindowCoordinates.bind(this),
     41    };
     42  }
     43 
     44  destroy() {}
     45 
     46  /**
     47   * Assert that the target coordinates are within the visible viewport.
     48   *
     49   * @param {Array.<number>} target
     50   *     Coordinates [x, y] of the target relative to the viewport.
     51   * @param {BrowsingContext} context
     52   *     The browsing context to dispatch the event to.
     53   *
     54   * @returns {Promise<undefined>}
     55   *     Promise that rejects, if the coordinates are not within
     56   *     the visible viewport.
     57   *
     58   * @throws {MoveTargetOutOfBoundsError}
     59   *     If target is outside the viewport.
     60   */
     61  #assertInViewPort(target, context) {
     62    return this._forwardToWindowGlobal("_assertInViewPort", context.id, {
     63      target,
     64    });
     65  }
     66 
     67  /**
     68   * Dispatch an event.
     69   *
     70   * @param {string} eventName
     71   *     Name of the event to be dispatched.
     72   * @param {BrowsingContext} context
     73   *     The browsing context to dispatch the event to.
     74   * @param {object} details
     75   *     Details of the event to be dispatched.
     76   *
     77   * @returns {Promise}
     78   *     Promise that resolves once the event is dispatched.
     79   */
     80  async #dispatchEvent(eventName, context, details) {
     81    details.eventData.asyncEnabled =
     82      (eventName === "synthesizeWheelAtPoint" &&
     83        lazy.actions.useAsyncWheelEvents) ||
     84      (eventName == "synthesizeMouseAtPoint" &&
     85        lazy.actions.useAsyncMouseEvents);
     86 
     87    // TODO: Call the _dispatchEvent method of the windowglobal module once
     88    // chrome support was added for the message handler.
     89    if (details.eventData.asyncEnabled) {
     90      if (!context || context.isDiscarded) {
     91        const id = lazy.NavigableManager.getIdForBrowsingContext(context);
     92        throw new lazy.error.NoSuchFrameError(
     93          `Browsing Context with id ${id} not found`
     94        );
     95      }
     96 
     97      switch (eventName) {
     98        case "synthesizeMouseAtPoint":
     99          await lazy.event.synthesizeMouseAtPoint(
    100            details.x,
    101            details.y,
    102            details.eventData,
    103            context.topChromeWindow
    104          );
    105          break;
    106        case "synthesizeWheelAtPoint":
    107          await lazy.event.synthesizeWheelAtPoint(
    108            details.x,
    109            details.y,
    110            details.eventData,
    111            context.topChromeWindow
    112          );
    113          break;
    114        default:
    115          throw new Error(
    116            `${eventName} is not a supported type for dispatching`
    117          );
    118      }
    119    } else {
    120      await this._forwardToWindowGlobal("_dispatchEvent", context.id, {
    121        eventName,
    122        details,
    123      });
    124    }
    125  }
    126 
    127  /**
    128   * Finalize an action command.
    129   *
    130   * @param {BrowsingContext} context
    131   *     The browsing context to forward the command to.
    132   */
    133  async #finalizeAction(context) {
    134    try {
    135      await this._forwardToWindowGlobal("_finalizeAction", context.id);
    136    } catch (e) {
    137      // Ignore the error if the underlying browsing context is already gone.
    138      if (e.name !== "DiscardedBrowsingContextError") {
    139        throw e;
    140      }
    141    }
    142  }
    143 
    144  /**
    145   * Retrieve the list of client rects for the element.
    146   *
    147   * @param {Node} node
    148   *     The web element reference to retrieve the rects from.
    149   * @param {BrowsingContext} context
    150   *     The browsing context to dispatch the event to.
    151   *
    152   * @returns {Promise<Array<Map.<string, number>>>}
    153   *     Promise that resolves to a list of DOMRect-like objects.
    154   */
    155  #getClientRects(node, context) {
    156    return this._forwardToWindowGlobal("_getClientRects", context.id, {
    157      element: node,
    158    });
    159  }
    160 
    161  /**
    162   * Retrieves the Node reference of the origin.
    163   *
    164   * @param {ElementOrigin} origin
    165   *     Reference to the element origin of the action.
    166   * @param {BrowsingContext} context
    167   *     The browsing context to dispatch the event to.
    168   *
    169   * @returns {Promise<SharedReference>}
    170   *     Promise that resolves to the shared reference.
    171   */
    172  #getElementOrigin(origin, context) {
    173    return this._forwardToWindowGlobal("_getElementOrigin", context.id, {
    174      origin,
    175    });
    176  }
    177 
    178  /**
    179   * Retrieves the action's input state.
    180   *
    181   * @param {BrowsingContext} context
    182   *     The Browsing Context to retrieve the input state for.
    183   *
    184   * @returns {Actions.InputState}
    185   *     The action's input state.
    186   */
    187  #getInputState(context) {
    188    // Bug 1821460: Fetch top-level browsing context.
    189    let inputState = this.#inputStates.get(context);
    190 
    191    if (inputState === undefined) {
    192      inputState = new lazy.actions.State();
    193      this.#inputStates.set(context, inputState);
    194    }
    195 
    196    return inputState;
    197  }
    198 
    199  /**
    200   * Retrieve the in-view center point for the rect and visible viewport.
    201   *
    202   * @param {DOMRect} rect
    203   *     Size and position of the rectangle to check.
    204   * @param {BrowsingContext} context
    205   *     The browsing context to dispatch the event to.
    206   *
    207   * @returns {Promise<Map.<string, number>>}
    208   *     X and Y coordinates that denotes the in-view centre point of
    209   *     `rect`.
    210   */
    211  #getInViewCentrePoint(rect, context) {
    212    return this._forwardToWindowGlobal("_getInViewCentrePoint", context.id, {
    213      rect,
    214    });
    215  }
    216 
    217  /**
    218   * Checks if the given object is a valid element origin.
    219   *
    220   * @param {object} origin
    221   *     The object to check.
    222   *
    223   * @returns {boolean}
    224   *     True, if the object references a shared reference.
    225   */
    226  #isElementOrigin(origin) {
    227    return (
    228      origin?.type === "element" && typeof origin.element?.sharedId === "string"
    229    );
    230  }
    231 
    232  /**
    233   * Resets the action's input state.
    234   *
    235   * @param {BrowsingContext} context
    236   *     The Browsing Context to reset the input state for.
    237   */
    238  #resetInputState(context) {
    239    // Bug 1821460: Fetch top-level browsing context.
    240    if (this.#inputStates.has(context)) {
    241      this.#inputStates.delete(context);
    242    }
    243  }
    244 
    245  /**
    246   * Convert a position or rect in browser coordinates of CSS units.
    247   *
    248   * @param {object} position - Object with the coordinates to convert.
    249   * @param {number} position.x - X coordinate.
    250   * @param {number} position.y - Y coordinate.
    251   * @param {BrowsingContext} context - The Browsing Context to convert the
    252   *     coordinates for.
    253   */
    254  #toBrowserWindowCoordinates(position, context) {
    255    return this._forwardToWindowGlobal(
    256      "_toBrowserWindowCoordinates",
    257      context.id,
    258      { position }
    259    );
    260  }
    261 
    262  /**
    263   * Perform a series of grouped actions at the specified points in time.
    264   *
    265   * @param {object} options
    266   * @param {Array<?>} options.actions
    267   *     Array of objects that each represent an action sequence.
    268   * @param {string} options.context
    269   *     Id of the browsing context to reset the input state.
    270   *
    271   * @throws {InvalidArgumentError}
    272   *     If <var>context</var> is not valid type.
    273   * @throws {MoveTargetOutOfBoundsError}
    274   *     If target is outside the viewport.
    275   * @throws {NoSuchFrameError}
    276   *     If the browsing context cannot be found.
    277   * @throws {NoSuchElementError}
    278   *     If an element that is used as part of the action chain is unknown.
    279   */
    280  async performActions(options = {}) {
    281    const { actions, context: contextId } = options;
    282 
    283    lazy.assert.string(
    284      contextId,
    285      lazy.pprint`Expected "context" to be a string, got ${contextId}`
    286    );
    287 
    288    const context = this._getNavigable(contextId);
    289 
    290    // Bug 1821460: Fetch top-level browsing context.
    291    const inputState = this.#getInputState(context);
    292    const actionsOptions = { ...this.#actionsOptions, context };
    293 
    294    const actionChain = await lazy.actions.Chain.fromJSON(
    295      inputState,
    296      actions,
    297      actionsOptions
    298    );
    299 
    300    // Enqueue to serialize access to input state.
    301    await inputState.enqueueAction(() =>
    302      actionChain.dispatch(inputState, actionsOptions)
    303    );
    304 
    305    // Process async follow-up tasks in content before the reply is sent.
    306    await this.#finalizeAction(context);
    307  }
    308 
    309  /**
    310   * Reset the input state in the provided browsing context.
    311   *
    312   * @param {object=} options
    313   * @param {string} options.context
    314   *     Id of the browsing context to reset the input state.
    315   *
    316   * @throws {InvalidArgumentError}
    317   *     If <var>context</var> is not valid type.
    318   * @throws {NoSuchFrameError}
    319   *     If the browsing context cannot be found.
    320   */
    321  async releaseActions(options = {}) {
    322    const { context: contextId } = options;
    323 
    324    lazy.assert.string(
    325      contextId,
    326      lazy.pprint`Expected "context" to be a string, got ${contextId}`
    327    );
    328 
    329    const context = this._getNavigable(contextId);
    330 
    331    // Bug 1821460: Fetch top-level browsing context.
    332    const inputState = this.#getInputState(context);
    333    const actionsOptions = { ...this.#actionsOptions, context };
    334 
    335    // Enqueue to serialize access to input state.
    336    await inputState.enqueueAction(() => {
    337      const undoActions = inputState.inputCancelList.reverse();
    338      return undoActions.dispatch(inputState, actionsOptions);
    339    });
    340 
    341    this.#resetInputState(context);
    342 
    343    // Process async follow-up tasks in content before the reply is sent.
    344    await this.#finalizeAction(context);
    345  }
    346 
    347  /**
    348   * Sets the file property of a given input element with type file to a set of file paths.
    349   *
    350   * @param {object=} options
    351   * @param {string} options.context
    352   *     Id of the browsing context to set the file property
    353   *     of a given input element.
    354   * @param {SharedReference} options.element
    355   *     A reference to a node, which is used as
    356   *     a target for setting files.
    357   * @param {Array<string>} options.files
    358   *     A list of file paths which should be set.
    359   *
    360   * @throws {InvalidArgumentError}
    361   *     Raised if an argument is of an invalid type or value.
    362   * @throws {NoSuchElementError}
    363   *     If the input element cannot be found.
    364   * @throws {NoSuchFrameError}
    365   *     If the browsing context cannot be found.
    366   * @throws {UnableToSetFileInputError}
    367   *     If the set of file paths was not set to the input element.
    368   */
    369  async setFiles(options = {}) {
    370    const { context: contextId, element, files } = options;
    371 
    372    lazy.assert.string(
    373      contextId,
    374      lazy.pprint`Expected "context" to be a string, got ${contextId}`
    375    );
    376 
    377    const context = this._getNavigable(contextId);
    378 
    379    lazy.assert.array(
    380      files,
    381      lazy.pprint`Expected "files" to be an array, got ${files}`
    382    );
    383 
    384    for (const file of files) {
    385      lazy.assert.string(
    386        file,
    387        lazy.pprint`Expected an element of "files" to be a string, got ${file}`
    388      );
    389    }
    390 
    391    await this._forwardToWindowGlobal("setFiles", context.id, {
    392      element,
    393      files,
    394    });
    395  }
    396 
    397  static get supportedEvents() {
    398    return ["input.fileDialogOpened"];
    399  }
    400 }
    401 
    402 export const input = InputModule;