tor-browser

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

input.sys.mjs (10070B)


      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  LayoutUtils: "resource://gre/modules/LayoutUtils.sys.mjs",
     11 
     12  AnimationFramePromise: "chrome://remote/content/shared/Sync.sys.mjs",
     13  assertTargetInViewPort:
     14    "chrome://remote/content/shared/webdriver/Actions.sys.mjs",
     15  dom: "chrome://remote/content/shared/DOM.sys.mjs",
     16  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
     17  event: "chrome://remote/content/shared/webdriver/Event.sys.mjs",
     18  FilePickerListener:
     19    "chrome://remote/content/shared/listeners/FilePickerListener.sys.mjs",
     20  OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
     21  setDefaultSerializationOptions:
     22    "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
     23 });
     24 
     25 class InputModule extends WindowGlobalBiDiModule {
     26  #filePickerListener;
     27  #subscribedEvents;
     28 
     29  constructor(messageHandler) {
     30    super(messageHandler);
     31 
     32    this.#filePickerListener = new lazy.FilePickerListener();
     33    this.#filePickerListener.on(
     34      "file-picker-opening",
     35      this.#onFilePickerOpening
     36    );
     37 
     38    // Set of event names which have active subscriptions.
     39    this.#subscribedEvents = new Set();
     40  }
     41 
     42  destroy() {
     43    this.#filePickerListener.off(
     44      "file-picker-opening",
     45      this.#onFilePickerOpening
     46    );
     47    this.#subscribedEvents = null;
     48  }
     49 
     50  async setFiles(options) {
     51    const { element: sharedReference, files } = options;
     52 
     53    const element =
     54      await this.#deserializeElementSharedReference(sharedReference);
     55 
     56    if (
     57      !HTMLInputElement.isInstance(element) ||
     58      element.type !== "file" ||
     59      element.disabled
     60    ) {
     61      throw new lazy.error.UnableToSetFileInputError(
     62        `Element needs to be an <input> element with type "file" and not disabled`
     63      );
     64    }
     65 
     66    if (files.length > 1 && !element.hasAttribute("multiple")) {
     67      throw new lazy.error.UnableToSetFileInputError(
     68        `Element should have an attribute "multiple" set when trying to set more than 1 file`
     69      );
     70    }
     71 
     72    const fileObjects = [];
     73    for (const file of files) {
     74      try {
     75        fileObjects.push(await File.createFromFileName(file));
     76      } catch (e) {
     77        throw new lazy.error.UnsupportedOperationError(
     78          `Failed to add file ${file} (${e})`
     79        );
     80      }
     81    }
     82 
     83    const selectedFiles = Array.from(element.files);
     84 
     85    const intersection = fileObjects.filter(fileObject =>
     86      selectedFiles.some(
     87        selectedFile =>
     88          // Compare file fields to identify if the files are equal.
     89          // TODO: Bug 1883856. Add check for full path or use a different way
     90          // to compare files when it's available.
     91          selectedFile.name === fileObject.name &&
     92          selectedFile.size === fileObject.size &&
     93          selectedFile.type === fileObject.type
     94      )
     95    );
     96 
     97    if (
     98      intersection.length === selectedFiles.length &&
     99      selectedFiles.length === fileObjects.length
    100    ) {
    101      lazy.event.cancel(element);
    102    } else {
    103      element.mozSetFileArray(fileObjects);
    104 
    105      lazy.event.input(element);
    106      lazy.event.change(element);
    107    }
    108  }
    109 
    110  async #deserializeElementSharedReference(sharedReference) {
    111    if (typeof sharedReference?.sharedId !== "string") {
    112      throw new lazy.error.InvalidArgumentError(
    113        `Expected "element" to be a SharedReference, got: ${sharedReference}`
    114      );
    115    }
    116 
    117    const realm = this.messageHandler.getRealm();
    118 
    119    const element = this.deserialize(sharedReference, realm);
    120    if (!lazy.dom.isElement(element)) {
    121      throw new lazy.error.NoSuchElementError(
    122        `No element found for shared id: ${sharedReference.sharedId}`
    123      );
    124    }
    125 
    126    return element;
    127  }
    128 
    129  #onFilePickerOpening = (eventName, data) => {
    130    const { element } = data;
    131    if (element.ownerGlobal.browsingContext != this.messageHandler.context) {
    132      return;
    133    }
    134 
    135    const realm = this.messageHandler.getRealm();
    136 
    137    const serializedNode = this.serialize(
    138      element,
    139      lazy.setDefaultSerializationOptions(),
    140      lazy.OwnershipModel.None,
    141      realm
    142    );
    143 
    144    this.emitEvent("input.fileDialogOpened", {
    145      context: this.messageHandler.context,
    146      element: serializedNode,
    147      multiple: element.multiple,
    148    });
    149  };
    150 
    151  #startListingOnFilePickerOpened() {
    152    if (!this.#subscribedEvents.has("script.FilePickerOpened")) {
    153      this.#filePickerListener.startListening();
    154    }
    155  }
    156 
    157  #stopListingOnFilePickerOpened() {
    158    if (this.#subscribedEvents.has("script.FilePickerOpened")) {
    159      this.#filePickerListener.stopListening();
    160    }
    161  }
    162 
    163  #subscribeEvent(event) {
    164    switch (event) {
    165      case "input.fileDialogOpened": {
    166        this.#startListingOnFilePickerOpened();
    167        this.#subscribedEvents.add(event);
    168        break;
    169      }
    170    }
    171  }
    172 
    173  #unsubscribeEvent(event) {
    174    switch (event) {
    175      case "input.fileDialogOpened": {
    176        this.#stopListingOnFilePickerOpened();
    177        this.#subscribedEvents.delete(event);
    178        break;
    179      }
    180    }
    181  }
    182 
    183  _applySessionData(params) {
    184    // TODO: Bug 1775231. Move this logic to a shared module or an abstract
    185    // class.
    186    const { category } = params;
    187    if (category === "event") {
    188      const filteredSessionData = params.sessionData.filter(item =>
    189        this.messageHandler.matchesContext(item.contextDescriptor)
    190      );
    191      for (const event of this.#subscribedEvents.values()) {
    192        const hasSessionItem = filteredSessionData.some(
    193          item => item.value === event
    194        );
    195        // If there are no session items for this context, we should unsubscribe from the event.
    196        if (!hasSessionItem) {
    197          this.#unsubscribeEvent(event);
    198        }
    199      }
    200 
    201      // Subscribe to all events, which have an item in SessionData.
    202      for (const { value } of filteredSessionData) {
    203        this.#subscribeEvent(value);
    204      }
    205    }
    206  }
    207 
    208  _assertInViewPort(options) {
    209    const { target } = options;
    210 
    211    return lazy.assertTargetInViewPort(target, this.messageHandler.window);
    212  }
    213 
    214  async _dispatchEvent(options) {
    215    const { eventName, details } = options;
    216 
    217    const windowUtils = this.messageHandler.window.windowUtils;
    218    const microTaskLevel = windowUtils.microTaskLevel;
    219    // Since we're being called as a webidl callback,
    220    // CallbackObjectBase::CallSetup::CallSetup has increased the microtask
    221    // level. Undo that temporarily so that microtask handling works closer
    222    // the way it would work when dispatching events natively.
    223    windowUtils.microTaskLevel = 0;
    224 
    225    try {
    226      switch (eventName) {
    227        case "synthesizeKeyDown":
    228          lazy.event.sendKeyDown(details.eventData, this.messageHandler.window);
    229          break;
    230        case "synthesizeKeyUp":
    231          lazy.event.sendKeyUp(details.eventData, this.messageHandler.window);
    232          break;
    233        case "synthesizeMouseAtPoint":
    234          await lazy.event.synthesizeMouseAtPoint(
    235            details.x,
    236            details.y,
    237            details.eventData,
    238            this.messageHandler.window
    239          );
    240          break;
    241        case "synthesizeMultiTouch":
    242          lazy.event.synthesizeMultiTouch(
    243            details.eventData,
    244            this.messageHandler.window
    245          );
    246          break;
    247        case "synthesizeWheelAtPoint":
    248          await lazy.event.synthesizeWheelAtPoint(
    249            details.x,
    250            details.y,
    251            details.eventData,
    252            this.messageHandler.window
    253          );
    254          break;
    255        default:
    256          throw new Error(
    257            `${eventName} is not a supported type for dispatching`
    258          );
    259      }
    260    } catch (e) {
    261      if (e.message.includes("NS_ERROR_FAILURE")) {
    262        // Dispatching the event failed. Inform the RootTransport
    263        // to retry dispatching the event.
    264        throw new DOMException(
    265          `Failed to dispatch event "${eventName}": ${e.message}`,
    266          "AbortError"
    267        );
    268      }
    269 
    270      throw e;
    271    } finally {
    272      windowUtils.microTaskLevel = microTaskLevel;
    273    }
    274  }
    275 
    276  async _finalizeAction() {
    277    // Terminate the current wheel transaction if there is one. Wheel
    278    // transactions should not live longer than a single action chain.
    279    await ChromeUtils.endWheelTransaction(this.messageHandler.window);
    280 
    281    // Wait for the next animation frame to make sure the page's content
    282    // was updated.
    283    await lazy.AnimationFramePromise(this.messageHandler.window);
    284  }
    285 
    286  async _getClientRects(options) {
    287    const { element: reference } = options;
    288 
    289    const element = await this.#deserializeElementSharedReference(reference);
    290    const rects = element.getClientRects();
    291 
    292    // To avoid serialization and deserialization of DOMRect and DOMRectList
    293    // convert to plain object and Array.
    294    return [...rects].map(rect => {
    295      const { x, y, width, height, top, right, bottom, left } = rect;
    296      return { x, y, width, height, top, right, bottom, left };
    297    });
    298  }
    299 
    300  async _getElementOrigin(options) {
    301    const { origin } = options;
    302 
    303    const reference = origin.element;
    304    this.#deserializeElementSharedReference(reference);
    305 
    306    return reference;
    307  }
    308 
    309  _getInViewCentrePoint(options) {
    310    const { rect } = options;
    311 
    312    return lazy.dom.getInViewCentrePoint(rect, this.messageHandler.window);
    313  }
    314 
    315  /**
    316   * Convert a position or rect in browser coordinates of CSS units.
    317   */
    318  _toBrowserWindowCoordinates(options) {
    319    const { position } = options;
    320 
    321    const [x, y] = position;
    322    const window = this.messageHandler.window;
    323    const dpr = window.devicePixelRatio;
    324 
    325    const val = lazy.LayoutUtils.rectToTopLevelWidgetRect(window, {
    326      left: x,
    327      top: y,
    328      height: 0,
    329      width: 0,
    330    });
    331 
    332    return [val.x / dpr, val.y / dpr];
    333  }
    334 }
    335 
    336 export const input = InputModule;