tor-browser

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

MarionetteCommandsParent.sys.mjs (13203B)


      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  capture: "chrome://remote/content/shared/Capture.sys.mjs",
      9  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
     10  getSeenNodesForBrowsingContext:
     11    "chrome://remote/content/shared/webdriver/Session.sys.mjs",
     12  json: "chrome://remote/content/marionette/json.sys.mjs",
     13  Log: "chrome://remote/content/shared/Log.sys.mjs",
     14 });
     15 
     16 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
     17  lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
     18 );
     19 
     20 // Because Marionette supports a single session only we store its id
     21 // globally so that the parent actor can access it.
     22 let webDriverSessionId = null;
     23 
     24 export class MarionetteCommandsParent extends JSWindowActorParent {
     25  #deferredDialogOpened;
     26 
     27  actorCreated() {
     28    this.#deferredDialogOpened = null;
     29  }
     30 
     31  assertInViewPort(target, _context) {
     32    return this.sendQuery("MarionetteCommandsParent:_assertInViewPort", {
     33      target,
     34    });
     35  }
     36 
     37  dispatchEvent(eventName, details) {
     38    return this.sendQuery("MarionetteCommandsParent:_dispatchEvent", {
     39      eventName,
     40      details,
     41    });
     42  }
     43 
     44  finalizeAction() {
     45    return this.sendQuery("MarionetteCommandsParent:_finalizeAction");
     46  }
     47 
     48  getClientRects(webEl, _context) {
     49    return this.sendQuery("MarionetteCommandsParent:_getClientRects", {
     50      elem: webEl,
     51    });
     52  }
     53 
     54  getInViewCentrePoint(rect, _context) {
     55    return this.sendQuery("MarionetteCommandsParent:_getInViewCentrePoint", {
     56      rect,
     57    });
     58  }
     59 
     60  toBrowserWindowCoordinates(position, _context) {
     61    return this.sendQuery(
     62      "MarionetteCommandsParent:_toBrowserWindowCoordinates",
     63      {
     64        position,
     65      }
     66    );
     67  }
     68 
     69  async sendQuery(name, serializedValue) {
     70    const seenNodes = lazy.getSeenNodesForBrowsingContext(
     71      webDriverSessionId,
     72      this.manager.browsingContext
     73    );
     74 
     75    // return early if a dialog is opened
     76    this.#deferredDialogOpened = Promise.withResolvers();
     77    let {
     78      error,
     79      isWebDriverError,
     80      seenNodeIds,
     81      serializedValue: serializedResult,
     82      hasSerializedWindows,
     83    } = await Promise.race([
     84      super.sendQuery(name, serializedValue),
     85      this.#deferredDialogOpened.promise,
     86    ]).finally(() => {
     87      this.#deferredDialogOpened = null;
     88    });
     89 
     90    if (error) {
     91      if (isWebDriverError) {
     92        // If it's a WebDriver error we need to deserialize it.
     93        error = lazy.error.WebDriverError.fromJSON(error);
     94      }
     95 
     96      this.#handleError(error, seenNodes);
     97    }
     98 
     99    // Update seen nodes for serialized element and shadow root nodes.
    100    seenNodeIds?.forEach(nodeId => seenNodes.add(nodeId));
    101 
    102    if (hasSerializedWindows) {
    103      // The serialized data contains WebWindow references that need to be
    104      // converted to unique identifiers.
    105      serializedResult = lazy.json.mapToNavigableIds(serializedResult);
    106    }
    107 
    108    return serializedResult;
    109  }
    110 
    111  /**
    112   * Handle an error and replace error type if necessary.
    113   *
    114   * @param {Error} error
    115   *     The error to handle.
    116   * @param {Set<string>} seenNodes
    117   *     List of node ids already seen in this navigable.
    118   *
    119   * @throws {Error}
    120   *     The original or replaced error.
    121   */
    122  #handleError(error, seenNodes) {
    123    // If an element hasn't been found during deserialization check if it
    124    // may be a stale reference.
    125    if (
    126      error instanceof lazy.error.NoSuchElementError &&
    127      error.data.elementId !== undefined &&
    128      seenNodes.has(error.data.elementId)
    129    ) {
    130      throw new lazy.error.StaleElementReferenceError(error);
    131    }
    132 
    133    // If a shadow root hasn't been found during deserialization check if it
    134    // may be a detached reference.
    135    if (
    136      error instanceof lazy.error.NoSuchShadowRootError &&
    137      error.data.shadowId !== undefined &&
    138      seenNodes.has(error.data.shadowId)
    139    ) {
    140      throw new lazy.error.DetachedShadowRootError(error);
    141    }
    142 
    143    throw error;
    144  }
    145 
    146  notifyDialogOpened() {
    147    if (this.#deferredDialogOpened) {
    148      this.#deferredDialogOpened.resolve({ data: null });
    149    }
    150  }
    151 
    152  // Proxying methods for WebDriver commands
    153 
    154  clearElement(webEl) {
    155    return this.sendQuery("MarionetteCommandsParent:clearElement", {
    156      elem: webEl,
    157    });
    158  }
    159 
    160  clickElement(webEl, capabilities) {
    161    return this.sendQuery("MarionetteCommandsParent:clickElement", {
    162      elem: webEl,
    163      capabilities: capabilities.toJSON(),
    164    });
    165  }
    166 
    167  async executeScript(script, args, opts) {
    168    return this.sendQuery("MarionetteCommandsParent:executeScript", {
    169      script,
    170      args: lazy.json.mapFromNavigableIds(args),
    171      opts,
    172    });
    173  }
    174 
    175  findElement(strategy, selector, opts) {
    176    return this.sendQuery("MarionetteCommandsParent:findElement", {
    177      strategy,
    178      selector,
    179      opts,
    180    });
    181  }
    182 
    183  findElements(strategy, selector, opts) {
    184    return this.sendQuery("MarionetteCommandsParent:findElements", {
    185      strategy,
    186      selector,
    187      opts,
    188    });
    189  }
    190 
    191  generateTestReport(messageBody, messageGroup) {
    192    return this.sendQuery("MarionetteCommandsParent:generateTestReport", {
    193      message: messageBody,
    194      group: messageGroup,
    195    });
    196  }
    197 
    198  async getShadowRoot(webEl) {
    199    return this.sendQuery("MarionetteCommandsParent:getShadowRoot", {
    200      elem: webEl,
    201    });
    202  }
    203 
    204  async getActiveElement() {
    205    return this.sendQuery("MarionetteCommandsParent:getActiveElement");
    206  }
    207 
    208  async getComputedLabel(webEl) {
    209    return this.sendQuery("MarionetteCommandsParent:getComputedLabel", {
    210      elem: webEl,
    211    });
    212  }
    213 
    214  async getComputedRole(webEl) {
    215    return this.sendQuery("MarionetteCommandsParent:getComputedRole", {
    216      elem: webEl,
    217    });
    218  }
    219 
    220  async getElementAttribute(webEl, name) {
    221    return this.sendQuery("MarionetteCommandsParent:getElementAttribute", {
    222      elem: webEl,
    223      name,
    224    });
    225  }
    226 
    227  async getElementProperty(webEl, name) {
    228    return this.sendQuery("MarionetteCommandsParent:getElementProperty", {
    229      elem: webEl,
    230      name,
    231    });
    232  }
    233 
    234  async getElementRect(webEl) {
    235    return this.sendQuery("MarionetteCommandsParent:getElementRect", {
    236      elem: webEl,
    237    });
    238  }
    239 
    240  async getElementTagName(webEl) {
    241    return this.sendQuery("MarionetteCommandsParent:getElementTagName", {
    242      elem: webEl,
    243    });
    244  }
    245 
    246  async getElementText(webEl) {
    247    return this.sendQuery("MarionetteCommandsParent:getElementText", {
    248      elem: webEl,
    249    });
    250  }
    251 
    252  async getElementValueOfCssProperty(webEl, name) {
    253    return this.sendQuery(
    254      "MarionetteCommandsParent:getElementValueOfCssProperty",
    255      {
    256        elem: webEl,
    257        name,
    258      }
    259    );
    260  }
    261 
    262  async getPageSource() {
    263    return this.sendQuery("MarionetteCommandsParent:getPageSource");
    264  }
    265 
    266  async isElementDisplayed(webEl, capabilities) {
    267    return this.sendQuery("MarionetteCommandsParent:isElementDisplayed", {
    268      capabilities: capabilities.toJSON(),
    269      elem: webEl,
    270    });
    271  }
    272 
    273  async isElementEnabled(webEl, capabilities) {
    274    return this.sendQuery("MarionetteCommandsParent:isElementEnabled", {
    275      capabilities: capabilities.toJSON(),
    276      elem: webEl,
    277    });
    278  }
    279 
    280  async isElementSelected(webEl, capabilities) {
    281    return this.sendQuery("MarionetteCommandsParent:isElementSelected", {
    282      capabilities: capabilities.toJSON(),
    283      elem: webEl,
    284    });
    285  }
    286 
    287  async sendKeysToElement(webEl, text, capabilities) {
    288    return this.sendQuery("MarionetteCommandsParent:sendKeysToElement", {
    289      capabilities: capabilities.toJSON(),
    290      elem: webEl,
    291      text,
    292    });
    293  }
    294 
    295  async switchToFrame(id) {
    296    const { browsingContextId } = await this.sendQuery(
    297      "MarionetteCommandsParent:switchToFrame",
    298      { id }
    299    );
    300 
    301    return {
    302      browsingContext: BrowsingContext.get(browsingContextId),
    303    };
    304  }
    305 
    306  async switchToParentFrame() {
    307    const { browsingContextId } = await this.sendQuery(
    308      "MarionetteCommandsParent:switchToParentFrame"
    309    );
    310 
    311    return {
    312      browsingContext: BrowsingContext.get(browsingContextId),
    313    };
    314  }
    315 
    316  async takeScreenshot(webEl, format, full, scroll) {
    317    const rect = await this.sendQuery(
    318      "MarionetteCommandsParent:getScreenshotRect",
    319      {
    320        elem: webEl,
    321        full,
    322        scroll,
    323      }
    324    );
    325 
    326    // If no element has been specified use the top-level browsing context.
    327    // Otherwise use the browsing context from the currently selected frame.
    328    const browsingContext = webEl
    329      ? this.browsingContext
    330      : this.browsingContext.top;
    331 
    332    let canvas = await lazy.capture.canvas(
    333      browsingContext.topChromeWindow,
    334      browsingContext,
    335      rect.x,
    336      rect.y,
    337      rect.width,
    338      rect.height
    339    );
    340 
    341    switch (format) {
    342      case lazy.capture.Format.Hash:
    343        return lazy.capture.toHash(canvas);
    344 
    345      case lazy.capture.Format.Base64:
    346        return lazy.capture.toBase64(canvas, "image/png");
    347 
    348      default:
    349        throw new TypeError(`Invalid capture format: ${format}`);
    350    }
    351  }
    352 }
    353 
    354 /**
    355 * Proxy that will dynamically create MarionetteCommands actors for a dynamically
    356 * provided browsing context until the method can be fully executed by the
    357 * JSWindowActor pair.
    358 *
    359 * @param {function(): BrowsingContext} browsingContextFn
    360 *     A function that returns the reference to the browsing context for which
    361 *     the query should run.
    362 */
    363 export function getMarionetteCommandsActorProxy(browsingContextFn) {
    364  const MAX_ATTEMPTS = 10;
    365 
    366  /**
    367   * Methods which modify the content page cannot be retried safely.
    368   * See Bug 1673345.
    369   */
    370  const NO_RETRY_METHODS = [
    371    "clickElement",
    372    "executeScript",
    373    "sendKeysToElement",
    374  ];
    375 
    376  return new Proxy(
    377    {},
    378    {
    379      get(target, methodName) {
    380        return async (...args) => {
    381          let attempts = 0;
    382          // eslint-disable-next-line no-constant-condition
    383          while (true) {
    384            let browsingContext = browsingContextFn();
    385 
    386            // If a top-level browsing context was replaced and retrying is allowed,
    387            // retrieve the new one for the current browser.
    388            if (
    389              browsingContext?.isReplaced &&
    390              browsingContext.top === browsingContext &&
    391              !NO_RETRY_METHODS.includes(methodName)
    392            ) {
    393              browsingContext = BrowsingContext.getCurrentTopByBrowserId(
    394                browsingContext.browserId
    395              );
    396            }
    397 
    398            if (!browsingContext || browsingContext.isDiscarded) {
    399              throw new lazy.error.NoSuchWindowError(
    400                `BrowsingContext does no longer exist`
    401              );
    402            }
    403 
    404            try {
    405              const actor =
    406                browsingContext.currentWindowGlobal.getActor(
    407                  "MarionetteCommands"
    408                );
    409 
    410              const result = await actor[methodName](...args);
    411              return result;
    412            } catch (e) {
    413              if (!["AbortError", "InactiveActor"].includes(e.name)) {
    414                // Only retry when the JSWindowActor pair gets destroyed, or
    415                // gets inactive eg. when the page is moved into bfcache.
    416                throw e;
    417              }
    418 
    419              if (NO_RETRY_METHODS.includes(methodName)) {
    420                lazy.logger.trace(
    421                  `[${browsingContext.id}] Querying "${methodName}"` +
    422                    ` failed with ${e.name}, returning "null" as fallback`
    423                );
    424                return null;
    425              }
    426 
    427              if (++attempts > MAX_ATTEMPTS) {
    428                lazy.logger.trace(
    429                  `[${browsingContext.id}] Querying "${methodName}"` +
    430                    ` reached the limit of retry attempts (${MAX_ATTEMPTS})`
    431                );
    432                throw e;
    433              }
    434 
    435              lazy.logger.trace(
    436                `[${browsingContext.id}] Retrying "${methodName}"` +
    437                  `, attempt: ${attempts}`
    438              );
    439              await new Promise(resolve =>
    440                Services.tm.dispatchToMainThread(resolve)
    441              );
    442            }
    443          }
    444        };
    445      },
    446    }
    447  );
    448 }
    449 
    450 /**
    451 * Register the MarionetteCommands actor that holds all the commands.
    452 *
    453 * @param {string} sessionId
    454 *     The id of the current WebDriver session.
    455 */
    456 export function registerCommandsActor(sessionId) {
    457  try {
    458    ChromeUtils.registerWindowActor("MarionetteCommands", {
    459      kind: "JSWindowActor",
    460      parent: {
    461        esModuleURI:
    462          "chrome://remote/content/marionette/actors/MarionetteCommandsParent.sys.mjs",
    463      },
    464      child: {
    465        esModuleURI:
    466          "chrome://remote/content/marionette/actors/MarionetteCommandsChild.sys.mjs",
    467      },
    468 
    469      allFrames: true,
    470      includeChrome: true,
    471    });
    472  } catch (e) {
    473    if (e.name === "NotSupportedError") {
    474      lazy.logger.warn(`MarionetteCommands actor is already registered!`);
    475    } else {
    476      throw e;
    477    }
    478  }
    479 
    480  webDriverSessionId = sessionId;
    481 }
    482 
    483 export function unregisterCommandsActor() {
    484  webDriverSessionId = null;
    485 
    486  ChromeUtils.unregisterWindowActor("MarionetteCommands");
    487 }