tor-browser

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

MarionetteCommandsChild.sys.mjs (20260B)


      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 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  LayoutUtils: "resource://gre/modules/LayoutUtils.sys.mjs",
      9 
     10  accessibility:
     11    "chrome://remote/content/shared/webdriver/Accessibility.sys.mjs",
     12  AnimationFramePromise: "chrome://remote/content/shared/Sync.sys.mjs",
     13  assertTargetInViewPort:
     14    "chrome://remote/content/shared/webdriver/Actions.sys.mjs",
     15  atom: "chrome://remote/content/marionette/atom.sys.mjs",
     16  dom: "chrome://remote/content/shared/DOM.sys.mjs",
     17  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
     18  evaluate: "chrome://remote/content/marionette/evaluate.sys.mjs",
     19  event: "chrome://remote/content/shared/webdriver/Event.sys.mjs",
     20  executeSoon: "chrome://remote/content/shared/Sync.sys.mjs",
     21  interaction: "chrome://remote/content/marionette/interaction.sys.mjs",
     22  json: "chrome://remote/content/marionette/json.sys.mjs",
     23  Log: "chrome://remote/content/shared/Log.sys.mjs",
     24  sandbox: "chrome://remote/content/marionette/evaluate.sys.mjs",
     25  Sandboxes: "chrome://remote/content/marionette/evaluate.sys.mjs",
     26 });
     27 
     28 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
     29  lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
     30 );
     31 
     32 export class MarionetteCommandsChild extends JSWindowActorChild {
     33  #processActor;
     34 
     35  constructor() {
     36    super();
     37 
     38    this.#processActor = ChromeUtils.domProcessChild.getActor(
     39      "WebDriverProcessData"
     40    );
     41 
     42    // sandbox storage and name of the current sandbox
     43    this.sandboxes = new lazy.Sandboxes(() => this.document.defaultView);
     44  }
     45 
     46  get innerWindowId() {
     47    return this.manager.innerWindowId;
     48  }
     49 
     50  actorCreated() {
     51    lazy.logger.trace(
     52      `[${this.browsingContext.id}] MarionetteCommands actor created ` +
     53        `for window id ${this.innerWindowId}`
     54    );
     55  }
     56 
     57  didDestroy() {
     58    lazy.logger.trace(
     59      `[${this.browsingContext.id}] MarionetteCommands actor destroyed ` +
     60        `for window id ${this.innerWindowId}`
     61    );
     62  }
     63 
     64  #assertInViewPort(options) {
     65    const { target } = options;
     66 
     67    return lazy.assertTargetInViewPort(target, this.contentWindow);
     68  }
     69 
     70  async #dispatchEvent(options) {
     71    const { eventName, details } = options;
     72    const win = this.contentWindow;
     73 
     74    const windowUtils = win.windowUtils;
     75    const microTaskLevel = windowUtils.microTaskLevel;
     76    // Since we're being called as a webidl callback,
     77    // CallbackObjectBase::CallSetup::CallSetup has increased the microtask
     78    // level. Undo that temporarily so that microtask handling works closer
     79    // the way it would work when dispatching events natively.
     80    windowUtils.microTaskLevel = 0;
     81    try {
     82      switch (eventName) {
     83        case "synthesizeKeyDown":
     84          lazy.event.sendKeyDown(details.eventData, win);
     85          break;
     86        case "synthesizeKeyUp":
     87          lazy.event.sendKeyUp(details.eventData, win);
     88          break;
     89        case "synthesizeMouseAtPoint":
     90          await lazy.event.synthesizeMouseAtPoint(
     91            details.x,
     92            details.y,
     93            details.eventData,
     94            win
     95          );
     96          break;
     97        case "synthesizeMultiTouch":
     98          lazy.event.synthesizeMultiTouch(details.eventData, win);
     99          break;
    100        case "synthesizeWheelAtPoint":
    101          await lazy.event.synthesizeWheelAtPoint(
    102            details.x,
    103            details.y,
    104            details.eventData,
    105            win
    106          );
    107          break;
    108        default:
    109          throw new Error(
    110            `${eventName} is not a supported event dispatch method`
    111          );
    112      }
    113    } catch (e) {
    114      if (e.message.includes("NS_ERROR_FAILURE")) {
    115        // Event dispatch failed. Re-throwing as AbortError to allow retrying
    116        // to dispatch the event.
    117        throw new DOMException(
    118          `Failed to dispatch event "${eventName}": ${e.message}`,
    119          "AbortError"
    120        );
    121      }
    122 
    123      throw e;
    124    } finally {
    125      windowUtils.microTaskLevel = microTaskLevel;
    126    }
    127  }
    128 
    129  async #finalizeAction() {
    130    // Terminate the current wheel transaction if there is one. Wheel
    131    // transactions should not live longer than a single action chain.
    132    await ChromeUtils.endWheelTransaction(this.contentWindow);
    133 
    134    // Wait for the next animation frame to make sure the page's content
    135    // was updated.
    136    await lazy.AnimationFramePromise(this.contentWindow);
    137  }
    138 
    139  #getClientRects(options, _context) {
    140    const { elem } = options;
    141 
    142    return elem.getClientRects();
    143  }
    144 
    145  #getInViewCentrePoint(options) {
    146    const { rect } = options;
    147 
    148    return lazy.dom.getInViewCentrePoint(rect, this.contentWindow);
    149  }
    150 
    151  #toBrowserWindowCoordinates(options, _context) {
    152    const { position } = options;
    153 
    154    const [x, y] = position;
    155    const dpr = this.contentWindow.devicePixelRatio;
    156 
    157    const val = lazy.LayoutUtils.rectToTopLevelWidgetRect(this.contentWindow, {
    158      left: x,
    159      top: y,
    160      height: 0,
    161      width: 0,
    162    });
    163 
    164    return [val.x / dpr, val.y / dpr];
    165  }
    166 
    167  // eslint-disable-next-line complexity
    168  async receiveMessage(msg) {
    169    if (!this.contentWindow) {
    170      throw new DOMException("Actor is no longer active", "InactiveActor");
    171    }
    172 
    173    try {
    174      let result;
    175      let waitForNextTick = false;
    176 
    177      const { name, data: serializedData } = msg;
    178 
    179      const data = lazy.json.deserialize(
    180        serializedData,
    181        this.#processActor.getNodeCache(),
    182        this.contentWindow.browsingContext
    183      );
    184 
    185      switch (name) {
    186        case "MarionetteCommandsParent:_assertInViewPort":
    187          result = this.#assertInViewPort(data);
    188          break;
    189        case "MarionetteCommandsParent:_dispatchEvent":
    190          await this.#dispatchEvent(data);
    191          waitForNextTick = true;
    192          break;
    193        case "MarionetteCommandsParent:_getClientRects":
    194          result = this.#getClientRects(data);
    195          break;
    196        case "MarionetteCommandsParent:_getInViewCentrePoint":
    197          result = this.#getInViewCentrePoint(data);
    198          break;
    199        case "MarionetteCommandsParent:_finalizeAction":
    200          this.#finalizeAction();
    201          break;
    202        case "MarionetteCommandsParent:_toBrowserWindowCoordinates":
    203          result = this.#toBrowserWindowCoordinates(data);
    204          break;
    205        case "MarionetteCommandsParent:clearElement":
    206          this.clearElement(data);
    207          waitForNextTick = true;
    208          break;
    209        case "MarionetteCommandsParent:clickElement":
    210          result = await this.clickElement(data);
    211          waitForNextTick = true;
    212          break;
    213        case "MarionetteCommandsParent:executeScript":
    214          result = await this.executeScript(data);
    215          waitForNextTick = true;
    216          break;
    217        case "MarionetteCommandsParent:findElement":
    218          result = await this.findElement(data);
    219          break;
    220        case "MarionetteCommandsParent:findElements":
    221          result = await this.findElements(data);
    222          break;
    223        case "MarionetteCommandsParent:generateTestReport":
    224          result = await this.generateTestReport(data);
    225          break;
    226        case "MarionetteCommandsParent:getActiveElement":
    227          result = await this.getActiveElement();
    228          break;
    229        case "MarionetteCommandsParent:getComputedLabel":
    230          result = await this.getComputedLabel(data);
    231          break;
    232        case "MarionetteCommandsParent:getComputedRole":
    233          result = await this.getComputedRole(data);
    234          break;
    235        case "MarionetteCommandsParent:getElementAttribute":
    236          result = await this.getElementAttribute(data);
    237          break;
    238        case "MarionetteCommandsParent:getElementProperty":
    239          result = await this.getElementProperty(data);
    240          break;
    241        case "MarionetteCommandsParent:getElementRect":
    242          result = await this.getElementRect(data);
    243          break;
    244        case "MarionetteCommandsParent:getElementTagName":
    245          result = await this.getElementTagName(data);
    246          break;
    247        case "MarionetteCommandsParent:getElementText":
    248          result = await this.getElementText(data);
    249          break;
    250        case "MarionetteCommandsParent:getElementValueOfCssProperty":
    251          result = await this.getElementValueOfCssProperty(data);
    252          break;
    253        case "MarionetteCommandsParent:getPageSource":
    254          result = await this.getPageSource();
    255          break;
    256        case "MarionetteCommandsParent:getScreenshotRect":
    257          result = await this.getScreenshotRect(data);
    258          break;
    259        case "MarionetteCommandsParent:getShadowRoot":
    260          result = await this.getShadowRoot(data);
    261          break;
    262        case "MarionetteCommandsParent:isElementDisplayed":
    263          result = await this.isElementDisplayed(data);
    264          break;
    265        case "MarionetteCommandsParent:isElementEnabled":
    266          result = await this.isElementEnabled(data);
    267          break;
    268        case "MarionetteCommandsParent:isElementSelected":
    269          result = await this.isElementSelected(data);
    270          break;
    271        case "MarionetteCommandsParent:sendKeysToElement":
    272          result = await this.sendKeysToElement(data);
    273          waitForNextTick = true;
    274          break;
    275        case "MarionetteCommandsParent:switchToFrame":
    276          result = await this.switchToFrame(data);
    277          waitForNextTick = true;
    278          break;
    279        case "MarionetteCommandsParent:switchToParentFrame":
    280          result = await this.switchToParentFrame();
    281          waitForNextTick = true;
    282          break;
    283      }
    284 
    285      // Inform the content process that the command has completed. It allows
    286      // it to process async follow-up tasks before the reply is sent.
    287      if (waitForNextTick) {
    288        await new Promise(resolve => lazy.executeSoon(resolve));
    289      }
    290 
    291      const { seenNodeIds, serializedValue, hasSerializedWindows } =
    292        lazy.json.clone(result, this.#processActor.getNodeCache());
    293 
    294      // Because in WebDriver classic nodes can only be returned from the same
    295      // browsing context, we only need the seen unique ids as flat array.
    296      return {
    297        seenNodeIds: [...seenNodeIds.values()].flat(),
    298        serializedValue,
    299        hasSerializedWindows,
    300      };
    301    } catch (e) {
    302      if (lazy.error.isWebDriverError(e)) {
    303        // If it's a WebDriver error always serialize it because it could
    304        // contain objects that are not serializable by default.
    305        return { error: e.toJSON(), isWebDriverError: true };
    306      }
    307      return { error: e, isWebDriverError: false };
    308    }
    309  }
    310 
    311  // Implementation of WebDriver commands
    312 
    313  /**
    314   * Clear the text of an element.
    315   *
    316   * @param {object} options
    317   * @param {Element} options.elem
    318   */
    319  clearElement(options = {}) {
    320    const { elem } = options;
    321 
    322    lazy.interaction.clearElement(elem);
    323  }
    324 
    325  /**
    326   * Click an element.
    327   */
    328  async clickElement(options = {}) {
    329    const { capabilities, elem } = options;
    330 
    331    return lazy.interaction.clickElement(
    332      elem,
    333      capabilities["moz:accessibilityChecks"],
    334      capabilities["moz:webdriverClick"]
    335    );
    336  }
    337 
    338  /**
    339   * Executes a JavaScript function.
    340   */
    341  async executeScript(options = {}) {
    342    const { args, opts = {}, script } = options;
    343 
    344    let sb;
    345    if (opts.sandboxName) {
    346      sb = this.sandboxes.get(opts.sandboxName, opts.newSandbox);
    347    } else {
    348      sb = lazy.sandbox.createMutable(this.document.defaultView);
    349    }
    350 
    351    return lazy.evaluate.sandbox(sb, script, args, opts);
    352  }
    353 
    354  /**
    355   * Find an element in the current browsing context's document using the
    356   * given search strategy.
    357   *
    358   * @param {object=} options
    359   * @param {string} options.strategy
    360   * @param {string} options.selector
    361   * @param {object} options.opts
    362   * @param {Element} options.opts.startNode
    363   */
    364  async findElement(options = {}) {
    365    const { strategy, selector, opts } = options;
    366 
    367    opts.all = false;
    368 
    369    const container = { frame: this.document.defaultView };
    370    return lazy.dom.find(container, strategy, selector, opts);
    371  }
    372 
    373  /**
    374   * Find elements in the current browsing context's document using the
    375   * given search strategy.
    376   *
    377   * @param {object=} options
    378   * @param {string} options.strategy
    379   * @param {string} options.selector
    380   * @param {object} options.opts
    381   * @param {Element} options.opts.startNode
    382   */
    383  async findElements(options = {}) {
    384    const { strategy, selector, opts } = options;
    385 
    386    opts.all = true;
    387 
    388    const container = { frame: this.document.defaultView };
    389    return lazy.dom.find(container, strategy, selector, opts);
    390  }
    391 
    392  /**
    393   * Generates and sends a test report to be observed by any registered reporting observers
    394   */
    395  async generateTestReport(options = {}) {
    396    const { message, group } = options;
    397    return this.browsingContext.window.TestReportGenerator.generateReport({
    398      message,
    399      group,
    400    });
    401  }
    402 
    403  /**
    404   * Return the active element in the document.
    405   */
    406  async getActiveElement() {
    407    let elem = this.document.activeElement;
    408    if (!elem) {
    409      throw new lazy.error.NoSuchElementError();
    410    }
    411 
    412    return elem;
    413  }
    414 
    415  /**
    416   * Return the accessible label for a given element.
    417   */
    418  async getComputedLabel(options = {}) {
    419    const { elem } = options;
    420 
    421    return lazy.accessibility.getAccessibleName(elem);
    422  }
    423 
    424  /**
    425   * Return the accessible role for a given element.
    426   */
    427  async getComputedRole(options = {}) {
    428    const { elem } = options;
    429 
    430    return lazy.accessibility.getComputedRole(elem);
    431  }
    432 
    433  /**
    434   * Get the value of an attribute for the given element.
    435   */
    436  async getElementAttribute(options = {}) {
    437    const { name, elem } = options;
    438 
    439    if (lazy.dom.isBooleanAttribute(elem, name)) {
    440      if (elem.hasAttribute(name)) {
    441        return "true";
    442      }
    443      return null;
    444    }
    445    return elem.getAttribute(name);
    446  }
    447 
    448  /**
    449   * Get the value of a property for the given element.
    450   */
    451  async getElementProperty(options = {}) {
    452    const { name, elem } = options;
    453 
    454    // Waive Xrays to get unfiltered access to the untrusted element.
    455    const el = Cu.waiveXrays(elem);
    456    return typeof el[name] != "undefined" ? el[name] : null;
    457  }
    458 
    459  /**
    460   * Get the position and dimensions of the element.
    461   */
    462  async getElementRect(options = {}) {
    463    const { elem } = options;
    464 
    465    const rect = elem.getBoundingClientRect();
    466    return {
    467      x: rect.x + this.document.defaultView.pageXOffset,
    468      y: rect.y + this.document.defaultView.pageYOffset,
    469      width: rect.width,
    470      height: rect.height,
    471    };
    472  }
    473 
    474  /**
    475   * Get the tagName for the given element.
    476   */
    477  async getElementTagName(options = {}) {
    478    const { elem } = options;
    479 
    480    return elem.tagName.toLowerCase();
    481  }
    482 
    483  /**
    484   * Get the text content for the given element.
    485   */
    486  async getElementText(options = {}) {
    487    const { elem } = options;
    488 
    489    try {
    490      return await lazy.atom.getVisibleText(elem, this.document.defaultView);
    491    } catch (e) {
    492      lazy.logger.warn(`Atom getVisibleText failed: "${e.message}"`);
    493 
    494      // Fallback in case the atom implementation is broken.
    495      // As known so far this only happens for XML documents (bug 1794099).
    496      return elem.textContent;
    497    }
    498  }
    499 
    500  /**
    501   * Get the value of a css property for the given element.
    502   */
    503  async getElementValueOfCssProperty(options = {}) {
    504    const { name, elem } = options;
    505 
    506    const style = this.document.defaultView.getComputedStyle(elem);
    507    return style.getPropertyValue(name);
    508  }
    509 
    510  /**
    511   * Get the source of the current browsing context's document.
    512   */
    513  async getPageSource() {
    514    return this.document.documentElement.outerHTML;
    515  }
    516 
    517  /**
    518   * Returns the rect of the element to screenshot.
    519   *
    520   * Because the screen capture takes place in the parent process the dimensions
    521   * for the screenshot have to be determined in the appropriate child process.
    522   *
    523   * Also it takes care of scrolling an element into view if requested.
    524   *
    525   * @param {object} options
    526   * @param {Element} options.elem
    527   *     Optional element to take a screenshot of.
    528   * @param {boolean=} options.full
    529   *     True to take a screenshot of the entire document element.
    530   *     Defaults to true.
    531   * @param {boolean=} options.scroll
    532   *     When <var>elem</var> is given, scroll it into view.
    533   *     Defaults to true.
    534   *
    535   * @returns {DOMRect}
    536   *     The area to take a snapshot from.
    537   */
    538  async getScreenshotRect(options = {}) {
    539    const { elem, full = true, scroll = true } = options;
    540    const win = elem
    541      ? this.document.defaultView
    542      : this.browsingContext.top.window;
    543 
    544    let rect;
    545 
    546    if (elem) {
    547      if (scroll) {
    548        lazy.dom.scrollIntoView(elem);
    549      }
    550      rect = this.getElementRect({ elem });
    551    } else if (full) {
    552      const docEl = win.document.documentElement;
    553      rect = new DOMRect(0, 0, docEl.scrollWidth, docEl.scrollHeight);
    554    } else {
    555      // viewport
    556      rect = new DOMRect(
    557        win.pageXOffset,
    558        win.pageYOffset,
    559        win.innerWidth,
    560        win.innerHeight
    561      );
    562    }
    563 
    564    return rect;
    565  }
    566 
    567  /**
    568   * Return the shadowRoot attached to an element
    569   */
    570  async getShadowRoot(options = {}) {
    571    const { elem } = options;
    572 
    573    return lazy.dom.getShadowRoot(elem);
    574  }
    575 
    576  /**
    577   * Determine the element displayedness of the given web element.
    578   */
    579  async isElementDisplayed(options = {}) {
    580    const { capabilities, elem } = options;
    581 
    582    return lazy.interaction.isElementDisplayed(
    583      elem,
    584      capabilities["moz:accessibilityChecks"]
    585    );
    586  }
    587 
    588  /**
    589   * Check if element is enabled.
    590   */
    591  async isElementEnabled(options = {}) {
    592    const { capabilities, elem } = options;
    593 
    594    return lazy.interaction.isElementEnabled(
    595      elem,
    596      capabilities["moz:accessibilityChecks"]
    597    );
    598  }
    599 
    600  /**
    601   * Determine whether the referenced element is selected or not.
    602   */
    603  async isElementSelected(options = {}) {
    604    const { capabilities, elem } = options;
    605 
    606    return lazy.interaction.isElementSelected(
    607      elem,
    608      capabilities["moz:accessibilityChecks"]
    609    );
    610  }
    611 
    612  /*
    613   * Send key presses to element after focusing on it.
    614   */
    615  async sendKeysToElement(options = {}) {
    616    const { capabilities, elem, text } = options;
    617 
    618    const opts = {
    619      strictFileInteractability: capabilities.strictFileInteractability,
    620      accessibilityChecks: capabilities["moz:accessibilityChecks"],
    621      webdriverClick: capabilities["moz:webdriverClick"],
    622    };
    623 
    624    return lazy.interaction.sendKeysToElement(elem, text, opts);
    625  }
    626 
    627  /**
    628   * Switch to the specified frame.
    629   *
    630   * @param {object=} options
    631   * @param {(number|Element)=} options.id
    632   *     If it's a number treat it as the index for all the existing frames.
    633   *     If it's an Element switch to this specific frame.
    634   *     If not specified or `null` switch to the top-level browsing context.
    635   */
    636  async switchToFrame(options = {}) {
    637    const { id } = options;
    638 
    639    const childContexts = this.browsingContext.children;
    640    let browsingContext;
    641 
    642    if (id == null) {
    643      browsingContext = this.browsingContext.top;
    644    } else if (typeof id == "number") {
    645      if (id < 0 || id >= childContexts.length) {
    646        throw new lazy.error.NoSuchFrameError(
    647          `Unable to locate frame with index: ${id}`
    648        );
    649      }
    650      browsingContext = childContexts[id];
    651    } else {
    652      const context = childContexts.find(childContext => {
    653        return childContext.embedderElement === id;
    654      });
    655      if (!context) {
    656        throw new lazy.error.NoSuchFrameError(
    657          `Unable to locate frame for element: ${id}`
    658        );
    659      }
    660      browsingContext = context;
    661    }
    662 
    663    // For in-process iframes the window global is lazy-loaded for optimization
    664    // reasons. As such force the currentWindowGlobal to be created so we always
    665    // have a window (bug 1691348).
    666    browsingContext.window;
    667 
    668    return { browsingContextId: browsingContext.id };
    669  }
    670 
    671  /**
    672   * Switch to the parent frame.
    673   */
    674  async switchToParentFrame() {
    675    const browsingContext = this.browsingContext.parent || this.browsingContext;
    676 
    677    return { browsingContextId: browsingContext.id };
    678  }
    679 }