tor-browser

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

Input.ts (16657B)


      1 /**
      2 * @license
      3 * Copyright 2017 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
      8 
      9 import type {Point} from '../api/ElementHandle.js';
     10 import {
     11  Keyboard,
     12  Mouse,
     13  MouseButton,
     14  Touchscreen,
     15  type TouchHandle,
     16  type KeyboardTypeOptions,
     17  type KeyDownOptions,
     18  type KeyPressOptions,
     19  type MouseClickOptions,
     20  type MouseMoveOptions,
     21  type MouseOptions,
     22  type MouseWheelOptions,
     23 } from '../api/Input.js';
     24 import {UnsupportedOperation} from '../common/Errors.js';
     25 import {TouchError} from '../common/Errors.js';
     26 import type {KeyInput} from '../common/USKeyboardLayout.js';
     27 
     28 import type {BidiPage} from './Page.js';
     29 
     30 const enum InputId {
     31  Mouse = '__puppeteer_mouse',
     32  Keyboard = '__puppeteer_keyboard',
     33  Wheel = '__puppeteer_wheel',
     34  Finger = '__puppeteer_finger',
     35 }
     36 
     37 enum SourceActionsType {
     38  None = 'none',
     39  Key = 'key',
     40  Pointer = 'pointer',
     41  Wheel = 'wheel',
     42 }
     43 
     44 enum ActionType {
     45  Pause = 'pause',
     46  KeyDown = 'keyDown',
     47  KeyUp = 'keyUp',
     48  PointerUp = 'pointerUp',
     49  PointerDown = 'pointerDown',
     50  PointerMove = 'pointerMove',
     51  Scroll = 'scroll',
     52 }
     53 
     54 const getBidiKeyValue = (key: KeyInput) => {
     55  switch (key) {
     56    case '\r':
     57    case '\n':
     58      key = 'Enter';
     59      break;
     60  }
     61  // Measures the number of code points rather than UTF-16 code units.
     62  if ([...key].length === 1) {
     63    return key;
     64  }
     65  switch (key) {
     66    case 'Cancel':
     67      return '\uE001';
     68    case 'Help':
     69      return '\uE002';
     70    case 'Backspace':
     71      return '\uE003';
     72    case 'Tab':
     73      return '\uE004';
     74    case 'Clear':
     75      return '\uE005';
     76    case 'Enter':
     77      return '\uE007';
     78    case 'Shift':
     79    case 'ShiftLeft':
     80      return '\uE008';
     81    case 'Control':
     82    case 'ControlLeft':
     83      return '\uE009';
     84    case 'Alt':
     85    case 'AltLeft':
     86      return '\uE00A';
     87    case 'Pause':
     88      return '\uE00B';
     89    case 'Escape':
     90      return '\uE00C';
     91    case 'PageUp':
     92      return '\uE00E';
     93    case 'PageDown':
     94      return '\uE00F';
     95    case 'End':
     96      return '\uE010';
     97    case 'Home':
     98      return '\uE011';
     99    case 'ArrowLeft':
    100      return '\uE012';
    101    case 'ArrowUp':
    102      return '\uE013';
    103    case 'ArrowRight':
    104      return '\uE014';
    105    case 'ArrowDown':
    106      return '\uE015';
    107    case 'Insert':
    108      return '\uE016';
    109    case 'Delete':
    110      return '\uE017';
    111    case 'NumpadEqual':
    112      return '\uE019';
    113    case 'Numpad0':
    114      return '\uE01A';
    115    case 'Numpad1':
    116      return '\uE01B';
    117    case 'Numpad2':
    118      return '\uE01C';
    119    case 'Numpad3':
    120      return '\uE01D';
    121    case 'Numpad4':
    122      return '\uE01E';
    123    case 'Numpad5':
    124      return '\uE01F';
    125    case 'Numpad6':
    126      return '\uE020';
    127    case 'Numpad7':
    128      return '\uE021';
    129    case 'Numpad8':
    130      return '\uE022';
    131    case 'Numpad9':
    132      return '\uE023';
    133    case 'NumpadMultiply':
    134      return '\uE024';
    135    case 'NumpadAdd':
    136      return '\uE025';
    137    case 'NumpadSubtract':
    138      return '\uE027';
    139    case 'NumpadDecimal':
    140      return '\uE028';
    141    case 'NumpadDivide':
    142      return '\uE029';
    143    case 'F1':
    144      return '\uE031';
    145    case 'F2':
    146      return '\uE032';
    147    case 'F3':
    148      return '\uE033';
    149    case 'F4':
    150      return '\uE034';
    151    case 'F5':
    152      return '\uE035';
    153    case 'F6':
    154      return '\uE036';
    155    case 'F7':
    156      return '\uE037';
    157    case 'F8':
    158      return '\uE038';
    159    case 'F9':
    160      return '\uE039';
    161    case 'F10':
    162      return '\uE03A';
    163    case 'F11':
    164      return '\uE03B';
    165    case 'F12':
    166      return '\uE03C';
    167    case 'Meta':
    168    case 'MetaLeft':
    169      return '\uE03D';
    170    case 'ShiftRight':
    171      return '\uE050';
    172    case 'ControlRight':
    173      return '\uE051';
    174    case 'AltRight':
    175      return '\uE052';
    176    case 'MetaRight':
    177      return '\uE053';
    178    case 'Digit0':
    179      return '0';
    180    case 'Digit1':
    181      return '1';
    182    case 'Digit2':
    183      return '2';
    184    case 'Digit3':
    185      return '3';
    186    case 'Digit4':
    187      return '4';
    188    case 'Digit5':
    189      return '5';
    190    case 'Digit6':
    191      return '6';
    192    case 'Digit7':
    193      return '7';
    194    case 'Digit8':
    195      return '8';
    196    case 'Digit9':
    197      return '9';
    198    case 'KeyA':
    199      return 'a';
    200    case 'KeyB':
    201      return 'b';
    202    case 'KeyC':
    203      return 'c';
    204    case 'KeyD':
    205      return 'd';
    206    case 'KeyE':
    207      return 'e';
    208    case 'KeyF':
    209      return 'f';
    210    case 'KeyG':
    211      return 'g';
    212    case 'KeyH':
    213      return 'h';
    214    case 'KeyI':
    215      return 'i';
    216    case 'KeyJ':
    217      return 'j';
    218    case 'KeyK':
    219      return 'k';
    220    case 'KeyL':
    221      return 'l';
    222    case 'KeyM':
    223      return 'm';
    224    case 'KeyN':
    225      return 'n';
    226    case 'KeyO':
    227      return 'o';
    228    case 'KeyP':
    229      return 'p';
    230    case 'KeyQ':
    231      return 'q';
    232    case 'KeyR':
    233      return 'r';
    234    case 'KeyS':
    235      return 's';
    236    case 'KeyT':
    237      return 't';
    238    case 'KeyU':
    239      return 'u';
    240    case 'KeyV':
    241      return 'v';
    242    case 'KeyW':
    243      return 'w';
    244    case 'KeyX':
    245      return 'x';
    246    case 'KeyY':
    247      return 'y';
    248    case 'KeyZ':
    249      return 'z';
    250    case 'Semicolon':
    251      return ';';
    252    case 'Equal':
    253      return '=';
    254    case 'Comma':
    255      return ',';
    256    case 'Minus':
    257      return '-';
    258    case 'Period':
    259      return '.';
    260    case 'Slash':
    261      return '/';
    262    case 'Backquote':
    263      return '`';
    264    case 'BracketLeft':
    265      return '[';
    266    case 'Backslash':
    267      return '\\';
    268    case 'BracketRight':
    269      return ']';
    270    case 'Quote':
    271      return '"';
    272    default:
    273      throw new Error(`Unknown key: "${key}"`);
    274  }
    275 };
    276 
    277 /**
    278 * @internal
    279 */
    280 export class BidiKeyboard extends Keyboard {
    281  #page: BidiPage;
    282 
    283  constructor(page: BidiPage) {
    284    super();
    285    this.#page = page;
    286  }
    287 
    288  override async down(
    289    key: KeyInput,
    290    _options?: Readonly<KeyDownOptions>,
    291  ): Promise<void> {
    292    await this.#page.mainFrame().browsingContext.performActions([
    293      {
    294        type: SourceActionsType.Key,
    295        id: InputId.Keyboard,
    296        actions: [
    297          {
    298            type: ActionType.KeyDown,
    299            value: getBidiKeyValue(key),
    300          },
    301        ],
    302      },
    303    ]);
    304  }
    305 
    306  override async up(key: KeyInput): Promise<void> {
    307    await this.#page.mainFrame().browsingContext.performActions([
    308      {
    309        type: SourceActionsType.Key,
    310        id: InputId.Keyboard,
    311        actions: [
    312          {
    313            type: ActionType.KeyUp,
    314            value: getBidiKeyValue(key),
    315          },
    316        ],
    317      },
    318    ]);
    319  }
    320 
    321  override async press(
    322    key: KeyInput,
    323    options: Readonly<KeyPressOptions> = {},
    324  ): Promise<void> {
    325    const {delay = 0} = options;
    326    const actions: Bidi.Input.KeySourceAction[] = [
    327      {
    328        type: ActionType.KeyDown,
    329        value: getBidiKeyValue(key),
    330      },
    331    ];
    332    if (delay > 0) {
    333      actions.push({
    334        type: ActionType.Pause,
    335        duration: delay,
    336      });
    337    }
    338    actions.push({
    339      type: ActionType.KeyUp,
    340      value: getBidiKeyValue(key),
    341    });
    342    await this.#page.mainFrame().browsingContext.performActions([
    343      {
    344        type: SourceActionsType.Key,
    345        id: InputId.Keyboard,
    346        actions,
    347      },
    348    ]);
    349  }
    350 
    351  override async type(
    352    text: string,
    353    options: Readonly<KeyboardTypeOptions> = {},
    354  ): Promise<void> {
    355    const {delay = 0} = options;
    356    // This spread separates the characters into code points rather than UTF-16
    357    // code units.
    358    const values = ([...text] as KeyInput[]).map(getBidiKeyValue);
    359    const actions: Bidi.Input.KeySourceAction[] = [];
    360    if (delay <= 0) {
    361      for (const value of values) {
    362        actions.push(
    363          {
    364            type: ActionType.KeyDown,
    365            value,
    366          },
    367          {
    368            type: ActionType.KeyUp,
    369            value,
    370          },
    371        );
    372      }
    373    } else {
    374      for (const value of values) {
    375        actions.push(
    376          {
    377            type: ActionType.KeyDown,
    378            value,
    379          },
    380          {
    381            type: ActionType.Pause,
    382            duration: delay,
    383          },
    384          {
    385            type: ActionType.KeyUp,
    386            value,
    387          },
    388        );
    389      }
    390    }
    391    await this.#page.mainFrame().browsingContext.performActions([
    392      {
    393        type: SourceActionsType.Key,
    394        id: InputId.Keyboard,
    395        actions,
    396      },
    397    ]);
    398  }
    399 
    400  override async sendCharacter(char: string): Promise<void> {
    401    // Measures the number of code points rather than UTF-16 code units.
    402    if ([...char].length > 1) {
    403      throw new Error('Cannot send more than 1 character.');
    404    }
    405    const frame = await this.#page.focusedFrame();
    406    await frame.isolatedRealm().evaluate(async char => {
    407      document.execCommand('insertText', false, char);
    408    }, char);
    409  }
    410 }
    411 
    412 /**
    413 * @internal
    414 */
    415 export interface BidiMouseClickOptions extends MouseClickOptions {
    416  origin?: Bidi.Input.Origin;
    417 }
    418 
    419 /**
    420 * @internal
    421 */
    422 export interface BidiMouseMoveOptions extends MouseMoveOptions {
    423  origin?: Bidi.Input.Origin;
    424 }
    425 
    426 /**
    427 * @internal
    428 */
    429 export interface BidiTouchMoveOptions {
    430  origin?: Bidi.Input.Origin;
    431 }
    432 
    433 const getBidiButton = (button: MouseButton) => {
    434  switch (button) {
    435    case MouseButton.Left:
    436      return 0;
    437    case MouseButton.Middle:
    438      return 1;
    439    case MouseButton.Right:
    440      return 2;
    441    case MouseButton.Back:
    442      return 3;
    443    case MouseButton.Forward:
    444      return 4;
    445  }
    446 };
    447 
    448 /**
    449 * @internal
    450 */
    451 export class BidiMouse extends Mouse {
    452  #page: BidiPage;
    453  #lastMovePoint: Point = {x: 0, y: 0};
    454 
    455  constructor(page: BidiPage) {
    456    super();
    457    this.#page = page;
    458  }
    459 
    460  override async reset(): Promise<void> {
    461    this.#lastMovePoint = {x: 0, y: 0};
    462    await this.#page.mainFrame().browsingContext.releaseActions();
    463  }
    464 
    465  override async move(
    466    x: number,
    467    y: number,
    468    options: Readonly<BidiMouseMoveOptions> = {},
    469  ): Promise<void> {
    470    const from = this.#lastMovePoint;
    471    const to = {
    472      x: Math.round(x),
    473      y: Math.round(y),
    474    };
    475    const actions: Bidi.Input.PointerSourceAction[] = [];
    476    const steps = options.steps ?? 0;
    477    for (let i = 0; i < steps; ++i) {
    478      actions.push({
    479        type: ActionType.PointerMove,
    480        x: from.x + (to.x - from.x) * (i / steps),
    481        y: from.y + (to.y - from.y) * (i / steps),
    482        origin: options.origin,
    483      });
    484    }
    485    actions.push({
    486      type: ActionType.PointerMove,
    487      ...to,
    488      origin: options.origin,
    489    });
    490    // https://w3c.github.io/webdriver-bidi/#command-input-performActions:~:text=input.PointerMoveAction%20%3D%20%7B%0A%20%20type%3A%20%22pointerMove%22%2C%0A%20%20x%3A%20js%2Dint%2C
    491    this.#lastMovePoint = to;
    492    await this.#page.mainFrame().browsingContext.performActions([
    493      {
    494        type: SourceActionsType.Pointer,
    495        id: InputId.Mouse,
    496        actions,
    497      },
    498    ]);
    499  }
    500 
    501  override async down(options: Readonly<MouseOptions> = {}): Promise<void> {
    502    await this.#page.mainFrame().browsingContext.performActions([
    503      {
    504        type: SourceActionsType.Pointer,
    505        id: InputId.Mouse,
    506        actions: [
    507          {
    508            type: ActionType.PointerDown,
    509            button: getBidiButton(options.button ?? MouseButton.Left),
    510          },
    511        ],
    512      },
    513    ]);
    514  }
    515 
    516  override async up(options: Readonly<MouseOptions> = {}): Promise<void> {
    517    await this.#page.mainFrame().browsingContext.performActions([
    518      {
    519        type: SourceActionsType.Pointer,
    520        id: InputId.Mouse,
    521        actions: [
    522          {
    523            type: ActionType.PointerUp,
    524            button: getBidiButton(options.button ?? MouseButton.Left),
    525          },
    526        ],
    527      },
    528    ]);
    529  }
    530 
    531  override async click(
    532    x: number,
    533    y: number,
    534    options: Readonly<BidiMouseClickOptions> = {},
    535  ): Promise<void> {
    536    const actions: Bidi.Input.PointerSourceAction[] = [
    537      {
    538        type: ActionType.PointerMove,
    539        x: Math.round(x),
    540        y: Math.round(y),
    541        origin: options.origin,
    542      },
    543    ];
    544    const pointerDownAction = {
    545      type: ActionType.PointerDown,
    546      button: getBidiButton(options.button ?? MouseButton.Left),
    547    } as const;
    548    const pointerUpAction = {
    549      type: ActionType.PointerUp,
    550      button: pointerDownAction.button,
    551    } as const;
    552    for (let i = 1; i < (options.count ?? 1); ++i) {
    553      actions.push(pointerDownAction, pointerUpAction);
    554    }
    555    actions.push(pointerDownAction);
    556    if (options.delay) {
    557      actions.push({
    558        type: ActionType.Pause,
    559        duration: options.delay,
    560      });
    561    }
    562    actions.push(pointerUpAction);
    563    await this.#page.mainFrame().browsingContext.performActions([
    564      {
    565        type: SourceActionsType.Pointer,
    566        id: InputId.Mouse,
    567        actions,
    568      },
    569    ]);
    570  }
    571 
    572  override async wheel(
    573    options: Readonly<MouseWheelOptions> = {},
    574  ): Promise<void> {
    575    await this.#page.mainFrame().browsingContext.performActions([
    576      {
    577        type: SourceActionsType.Wheel,
    578        id: InputId.Wheel,
    579        actions: [
    580          {
    581            type: ActionType.Scroll,
    582            ...(this.#lastMovePoint ?? {
    583              x: 0,
    584              y: 0,
    585            }),
    586            deltaX: options.deltaX ?? 0,
    587            deltaY: options.deltaY ?? 0,
    588          },
    589        ],
    590      },
    591    ]);
    592  }
    593 
    594  override drag(): never {
    595    throw new UnsupportedOperation();
    596  }
    597 
    598  override dragOver(): never {
    599    throw new UnsupportedOperation();
    600  }
    601 
    602  override dragEnter(): never {
    603    throw new UnsupportedOperation();
    604  }
    605 
    606  override drop(): never {
    607    throw new UnsupportedOperation();
    608  }
    609 
    610  override dragAndDrop(): never {
    611    throw new UnsupportedOperation();
    612  }
    613 }
    614 
    615 /**
    616 * @internal
    617 */
    618 class BidiTouchHandle implements TouchHandle {
    619  #started = false;
    620  #x: number;
    621  #y: number;
    622  #bidiId: string;
    623  #page: BidiPage;
    624  #touchScreen: BidiTouchscreen;
    625  #properties: Bidi.Input.PointerCommonProperties;
    626 
    627  constructor(
    628    page: BidiPage,
    629    touchScreen: BidiTouchscreen,
    630    id: number,
    631    x: number,
    632    y: number,
    633    properties: Bidi.Input.PointerCommonProperties,
    634  ) {
    635    this.#page = page;
    636    this.#touchScreen = touchScreen;
    637    this.#x = Math.round(x);
    638    this.#y = Math.round(y);
    639    this.#properties = properties;
    640    this.#bidiId = `${InputId.Finger}_${id}`;
    641  }
    642 
    643  async start(options: BidiTouchMoveOptions = {}): Promise<void> {
    644    if (this.#started) {
    645      throw new TouchError('Touch has already started');
    646    }
    647    await this.#page.mainFrame().browsingContext.performActions([
    648      {
    649        type: SourceActionsType.Pointer,
    650        id: this.#bidiId,
    651        parameters: {
    652          pointerType: Bidi.Input.PointerType.Touch,
    653        },
    654        actions: [
    655          {
    656            type: ActionType.PointerMove,
    657            x: this.#x,
    658            y: this.#y,
    659            origin: options.origin,
    660          },
    661          {
    662            ...this.#properties,
    663            type: ActionType.PointerDown,
    664            button: 0,
    665          },
    666        ],
    667      },
    668    ]);
    669    this.#started = true;
    670  }
    671 
    672  move(x: number, y: number): Promise<void> {
    673    const newX = Math.round(x);
    674    const newY = Math.round(y);
    675    return this.#page.mainFrame().browsingContext.performActions([
    676      {
    677        type: SourceActionsType.Pointer,
    678        id: this.#bidiId,
    679        parameters: {
    680          pointerType: Bidi.Input.PointerType.Touch,
    681        },
    682        actions: [
    683          {
    684            ...this.#properties,
    685            type: ActionType.PointerMove,
    686            x: newX,
    687            y: newY,
    688          },
    689        ],
    690      },
    691    ]);
    692  }
    693 
    694  async end(): Promise<void> {
    695    await this.#page.mainFrame().browsingContext.performActions([
    696      {
    697        type: SourceActionsType.Pointer,
    698        id: this.#bidiId,
    699        parameters: {
    700          pointerType: Bidi.Input.PointerType.Touch,
    701        },
    702        actions: [
    703          {
    704            type: ActionType.PointerUp,
    705            button: 0,
    706          },
    707        ],
    708      },
    709    ]);
    710    this.#touchScreen.removeHandle(this);
    711  }
    712 }
    713 /**
    714 * @internal
    715 */
    716 export class BidiTouchscreen extends Touchscreen {
    717  #page: BidiPage;
    718  declare touches: BidiTouchHandle[];
    719 
    720  constructor(page: BidiPage) {
    721    super();
    722    this.#page = page;
    723  }
    724 
    725  override async touchStart(
    726    x: number,
    727    y: number,
    728    options: BidiTouchMoveOptions = {},
    729  ): Promise<TouchHandle> {
    730    const id = this.idGenerator();
    731    const properties: Bidi.Input.PointerCommonProperties = {
    732      width: 0.5 * 2, // 2 times default touch radius.
    733      height: 0.5 * 2, // 2 times default touch radius.
    734      pressure: 0.5,
    735      altitudeAngle: Math.PI / 2,
    736    };
    737    const touch = new BidiTouchHandle(this.#page, this, id, x, y, properties);
    738    await touch.start(options);
    739    this.touches.push(touch);
    740    return touch;
    741  }
    742 }