tor-browser

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

node.js (27095B)


      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 "use strict";
      6 
      7 const { Actor } = require("resource://devtools/shared/protocol.js");
      8 const {
      9  nodeSpec,
     10  nodeListSpec,
     11 } = require("resource://devtools/shared/specs/node.js");
     12 
     13 const {
     14  PSEUDO_CLASSES,
     15 } = require("resource://devtools/shared/css/constants.js");
     16 
     17 loader.lazyRequireGetter(
     18  this,
     19  ["getCssPath", "getXPath", "findCssSelector"],
     20  "resource://devtools/shared/inspector/css-logic.js",
     21  true
     22 );
     23 
     24 loader.lazyRequireGetter(
     25  this,
     26  [
     27    "getShadowRootMode",
     28    "isDirectShadowHostChild",
     29    "isFrameBlockedByCSP",
     30    "isFrameWithChildTarget",
     31    "isShadowHost",
     32    "isShadowRoot",
     33  ],
     34  "resource://devtools/shared/layout/utils.js",
     35  true
     36 );
     37 
     38 loader.lazyRequireGetter(
     39  this,
     40  [
     41    "getBackgroundColor",
     42    "getClosestBackgroundColor",
     43    "getNodeDisplayName",
     44    "imageToImageData",
     45    "isNodeDead",
     46  ],
     47  "resource://devtools/server/actors/inspector/utils.js",
     48  true
     49 );
     50 loader.lazyRequireGetter(
     51  this,
     52  "LongStringActor",
     53  "resource://devtools/server/actors/string.js",
     54  true
     55 );
     56 loader.lazyRequireGetter(
     57  this,
     58  "getFontPreviewData",
     59  "resource://devtools/server/actors/utils/style-utils.js",
     60  true
     61 );
     62 loader.lazyRequireGetter(
     63  this,
     64  "CssLogic",
     65  "resource://devtools/server/actors/inspector/css-logic.js",
     66  true
     67 );
     68 loader.lazyRequireGetter(
     69  this,
     70  "EventCollector",
     71  "resource://devtools/server/actors/inspector/event-collector.js",
     72  true
     73 );
     74 loader.lazyRequireGetter(
     75  this,
     76  "DOMHelpers",
     77  "resource://devtools/shared/dom-helpers.js",
     78  true
     79 );
     80 
     81 const FONT_FAMILY_PREVIEW_TEXT = "The quick brown fox jumps over the lazy dog";
     82 const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20;
     83 
     84 /**
     85 * Server side of the node actor.
     86 */
     87 class NodeActor extends Actor {
     88  constructor(walker, node) {
     89    super(walker.conn, nodeSpec);
     90    this.walker = walker;
     91    this.rawNode = node;
     92    this._eventCollector = new EventCollector(this.walker.targetActor);
     93    // Map<id -> nsIEventListenerInfo> that we maintain to be able to disable/re-enable event listeners
     94    // The id is generated from getEventListenerInfo
     95    this._nsIEventListenersInfo = new Map();
     96 
     97    // Store the original display type and scrollable state and whether or not the node is
     98    // displayed to track changes when reflows occur.
     99    const wasScrollable = this.isScrollable;
    100 
    101    this.currentDisplayType = this.displayType;
    102    this.wasDisplayed = this.isDisplayed;
    103    this.wasScrollable = wasScrollable;
    104    this.currentContainerType = this.containerType;
    105    this.currentAnchorName = this.anchorName;
    106 
    107    if (wasScrollable) {
    108      this.walker.updateOverflowCausingElements(
    109        this,
    110        this.walker.overflowCausingElementsMap
    111      );
    112    }
    113  }
    114 
    115  toString() {
    116    return (
    117      "[NodeActor " + this.actorID + " for " + this.rawNode.toString() + "]"
    118    );
    119  }
    120 
    121  isDocumentElement() {
    122    return (
    123      this.rawNode.ownerDocument &&
    124      this.rawNode.ownerDocument.documentElement === this.rawNode
    125    );
    126  }
    127 
    128  destroy() {
    129    super.destroy();
    130 
    131    if (this.mutationObserver) {
    132      if (!Cu.isDeadWrapper(this.mutationObserver)) {
    133        this.mutationObserver.disconnect();
    134      }
    135      this.mutationObserver = null;
    136    }
    137 
    138    if (this.slotchangeListener) {
    139      if (!isNodeDead(this)) {
    140        this.rawNode.removeEventListener("slotchange", this.slotchangeListener);
    141      }
    142      this.slotchangeListener = null;
    143    }
    144 
    145    if (this._waitForFrameLoadAbortController) {
    146      this._waitForFrameLoadAbortController.abort();
    147      this._waitForFrameLoadAbortController = null;
    148    }
    149    if (this._waitForFrameLoadIntervalId) {
    150      clearInterval(this._waitForFrameLoadIntervalId);
    151      this._waitForFrameLoadIntervalId = null;
    152    }
    153 
    154    if (this._nsIEventListenersInfo) {
    155      // Re-enable all event listeners that we might have disabled
    156      for (const nsIEventListenerInfo of this._nsIEventListenersInfo.values()) {
    157        // If event listeners/node don't exist anymore, accessing nsIEventListenerInfo.enabled
    158        // will throw.
    159        try {
    160          if (!nsIEventListenerInfo.enabled) {
    161            nsIEventListenerInfo.enabled = true;
    162          }
    163        } catch (e) {
    164          // ignore
    165        }
    166      }
    167      this._nsIEventListenersInfo = null;
    168    }
    169 
    170    this._eventCollector.destroy();
    171    this._eventCollector = null;
    172    this.rawNode = null;
    173    this.walker = null;
    174  }
    175 
    176  // Returns the JSON representation of this object over the wire.
    177  form() {
    178    const parentNode = this.walker.parentNode(this);
    179    const inlineTextChild = this.walker.inlineTextChild(this.rawNode);
    180    const shadowRoot = isShadowRoot(this.rawNode);
    181    const hostActor = shadowRoot
    182      ? this.walker.getNode(this.rawNode.host)
    183      : null;
    184    const nodeType = this.rawNode.nodeType;
    185 
    186    const form = {
    187      actor: this.actorID,
    188      host: hostActor ? hostActor.actorID : undefined,
    189      baseURI: this.rawNode.baseURI,
    190      parent: parentNode ? parentNode.actorID : undefined,
    191      nodeType,
    192      namespaceURI: this.rawNode.namespaceURI,
    193      nodeName: this.rawNode.nodeName,
    194      nodeValue: this.rawNode.nodeValue,
    195      displayName: getNodeDisplayName(this.rawNode),
    196      numChildren: this.numChildren,
    197      inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined,
    198      displayType: this.displayType,
    199      isScrollable: this.isScrollable,
    200      isTopLevelDocument: this.isTopLevelDocument,
    201      causesOverflow: this.walker.overflowCausingElementsMap.has(this.rawNode),
    202      containerType: this.containerType,
    203      anchorName: this.anchorName,
    204 
    205      // doctype attributes
    206      name: this.rawNode.name,
    207      publicId: this.rawNode.publicId,
    208      systemId: this.rawNode.systemId,
    209 
    210      attrs: this.writeAttrs(),
    211      customElementLocation: this.getCustomElementLocation(),
    212      isPseudoElement: !!this.rawNode.implementedPseudoElement,
    213      isNativeAnonymous: this.rawNode.isNativeAnonymous,
    214      isShadowRoot: shadowRoot,
    215      shadowRootMode: getShadowRootMode(this.rawNode),
    216      isShadowHost: isShadowHost(this.rawNode),
    217      isDirectShadowHostChild: isDirectShadowHostChild(this.rawNode),
    218      pseudoClassLocks: this.writePseudoClassLocks(),
    219      mutationBreakpoints: this.walker.getMutationBreakpoints(this),
    220 
    221      isDisplayed: this.isDisplayed,
    222      isInHTMLDocument:
    223        this.rawNode.ownerDocument &&
    224        this.rawNode.ownerDocument.contentType === "text/html",
    225      traits: {
    226        // @backward-compat { version 147 } Can be removed once 147 reaches release
    227        hasPseudoElementNameInDisplayName: true,
    228      },
    229    };
    230 
    231    // The event collector can be expensive, so only check for events on nodes that
    232    // can display the `event` badge.
    233    if (
    234      nodeType !== Node.COMMENT_NODE &&
    235      nodeType !== Node.TEXT_NODE &&
    236      nodeType !== Node.CDATA_SECTION_NODE &&
    237      nodeType !== Node.DOCUMENT_NODE &&
    238      nodeType !== Node.DOCUMENT_TYPE_NODE &&
    239      !form.isPseudoElement
    240    ) {
    241      form.hasEventListeners = this.hasEventListeners();
    242    }
    243 
    244    if (this.isDocumentElement()) {
    245      form.isDocumentElement = true;
    246    }
    247 
    248    if (isFrameBlockedByCSP(this.rawNode)) {
    249      form.numChildren = 0;
    250    }
    251 
    252    // Flag the node if a different walker is needed to retrieve its children (i.e. if
    253    // this is a remote frame, or if it's an iframe and we're creating targets for every iframes)
    254    if (this.useChildTargetToFetchChildren) {
    255      form.useChildTargetToFetchChildren = true;
    256      // Declare at least one child (the #document element) so
    257      // that they can be expanded.
    258      form.numChildren = 1;
    259    }
    260    form.browsingContextID = this.rawNode.browsingContext?.id;
    261 
    262    return form;
    263  }
    264 
    265  /**
    266   * Watch the given document node for mutations using the DOM observer
    267   * API.
    268   */
    269  watchDocument(doc, callback) {
    270    if (!doc.defaultView) {
    271      return;
    272    }
    273 
    274    const node = this.rawNode;
    275    // Create the observer on the node's actor.  The node will make sure
    276    // the observer is cleaned up when the actor is released.
    277    const observer = new doc.defaultView.MutationObserver(callback);
    278    observer.mergeAttributeRecords = true;
    279    observer.observe(node, {
    280      attributes: true,
    281      characterData: true,
    282      characterDataOldValue: true,
    283      childList: true,
    284      subtree: true,
    285      // Track addition/removal of pseudo-elements too
    286      chromeOnlyNodes: true,
    287    });
    288    this.mutationObserver = observer;
    289  }
    290 
    291  /**
    292   * Watch for all "slotchange" events on the node.
    293   */
    294  watchSlotchange(callback) {
    295    this.slotchangeListener = callback;
    296    this.rawNode.addEventListener("slotchange", this.slotchangeListener);
    297  }
    298 
    299  /**
    300   * Check if the current node represents an element (e.g. an iframe) which has a dedicated
    301   * target for its underlying document that we would need to use to fetch the child nodes.
    302   * This will be the case for iframes if EFT is enabled, or if this is a remote iframe and
    303   * fission is enabled.
    304   */
    305  get useChildTargetToFetchChildren() {
    306    return isFrameWithChildTarget(this.walker.targetActor, this.rawNode);
    307  }
    308 
    309  get isTopLevelDocument() {
    310    return this.rawNode === this.walker.rootDoc;
    311  }
    312 
    313  // Estimate the number of children that the walker will return without making
    314  // a call to children() if possible.
    315  get numChildren() {
    316    const rawNode = this.rawNode;
    317    let numChildren = rawNode.childNodes.length;
    318    const hasContentDocument = rawNode.contentDocument;
    319    const hasSVGDocument = rawNode.getSVGDocument && rawNode.getSVGDocument();
    320    if (numChildren === 0 && (hasContentDocument || hasSVGDocument)) {
    321      // This might be an iframe with virtual children.
    322      numChildren = 1;
    323    }
    324 
    325    // Normal counting misses ::before/::after.  Also, some anonymous children
    326    // may ultimately be skipped, so we have to consult with the walker.
    327    if (
    328      numChildren === 0 ||
    329      isShadowHost(this.rawNode) ||
    330      // FIXME: We should be able to just check <slot> rather than
    331      // containingShadowRoot.
    332      this.rawNode.containingShadowRoot ||
    333      !!this.rawNode.implementedPseudoElement
    334    ) {
    335      numChildren = this.walker.countChildren(this);
    336    }
    337 
    338    return numChildren;
    339  }
    340 
    341  get computedStyle() {
    342    if (!this._computedStyle) {
    343      this._computedStyle = CssLogic.getComputedStyle(this.rawNode);
    344    }
    345    return this._computedStyle;
    346  }
    347 
    348  /**
    349   * Returns the computed display style property value of the node.
    350   */
    351  get displayType() {
    352    // Consider all non-element nodes as displayed.
    353    if (isNodeDead(this) || this.rawNode.nodeType !== Node.ELEMENT_NODE) {
    354      return null;
    355    }
    356 
    357    const style = this.computedStyle;
    358    if (!style) {
    359      return null;
    360    }
    361 
    362    let display = null;
    363    try {
    364      display = style.display;
    365    } catch (e) {
    366      // Fails for <scrollbar> elements.
    367    }
    368 
    369    const gridContainerType = InspectorUtils.getGridContainerType(this.rawNode);
    370    if (
    371      gridContainerType &
    372      (InspectorUtils.GRID_SUBGRID_COL | InspectorUtils.GRID_SUBGRID_ROW)
    373    ) {
    374      display = "subgrid";
    375    }
    376 
    377    return display;
    378  }
    379 
    380  /**
    381   * Returns the computed containerType style property value of the node.
    382   */
    383  get containerType() {
    384    // non-element nodes can't be containers
    385    if (
    386      isNodeDead(this) ||
    387      this.rawNode.nodeType !== Node.ELEMENT_NODE ||
    388      !this.computedStyle
    389    ) {
    390      return null;
    391    }
    392 
    393    return this.computedStyle.containerType;
    394  }
    395 
    396  /**
    397   * Returns the computed anchorName style property value of the node.
    398   */
    399  get anchorName() {
    400    // non-element nodes can't be anchors
    401    if (
    402      isNodeDead(this) ||
    403      this.rawNode.nodeType !== Node.ELEMENT_NODE ||
    404      !this.computedStyle
    405    ) {
    406      return null;
    407    }
    408 
    409    return this.computedStyle.anchorName;
    410  }
    411 
    412  /**
    413   * Check whether the node currently has scrollbars and is scrollable.
    414   */
    415  get isScrollable() {
    416    return (
    417      this.rawNode.nodeType === Node.ELEMENT_NODE &&
    418      this.rawNode.hasVisibleScrollbars
    419    );
    420  }
    421 
    422  /**
    423   * Is the node currently displayed?
    424   */
    425  get isDisplayed() {
    426    const type = this.displayType;
    427 
    428    // Consider all non-elements or elements with no display-types to be displayed.
    429    if (!type) {
    430      return true;
    431    }
    432 
    433    // Otherwise consider elements to be displayed only if their display-types is other
    434    // than "none"".
    435    return type !== "none";
    436  }
    437 
    438  /**
    439   * Are there event listeners that are listening on this node? This method
    440   * uses all parsers registered via event-parsers.js.registerEventParser() to
    441   * check if there are any event listeners.
    442   *
    443   * @returns {boolean}
    444   */
    445  hasEventListeners(refreshCache = false) {
    446    if (this._hasEventListenersCached === undefined || refreshCache) {
    447      const result = this._eventCollector.hasEventListeners(this.rawNode);
    448      this._hasEventListenersCached = result;
    449    }
    450    return this._hasEventListenersCached;
    451  }
    452 
    453  writeAttrs() {
    454    // If the node has no attributes or this.rawNode is the document node and a
    455    // node with `name="attributes"` exists in the DOM we need to bail.
    456    if (
    457      !this.rawNode.attributes ||
    458      !NamedNodeMap.isInstance(this.rawNode.attributes)
    459    ) {
    460      return undefined;
    461    }
    462 
    463    return [...this.rawNode.attributes].map(attr => {
    464      return { namespace: attr.namespace, name: attr.name, value: attr.value };
    465    });
    466  }
    467 
    468  writePseudoClassLocks() {
    469    if (this.rawNode.nodeType !== Node.ELEMENT_NODE) {
    470      return undefined;
    471    }
    472    let ret = undefined;
    473    for (const pseudo of PSEUDO_CLASSES) {
    474      if (InspectorUtils.hasPseudoClassLock(this.rawNode, pseudo)) {
    475        ret = ret || [];
    476        ret.push(pseudo);
    477      }
    478    }
    479    return ret;
    480  }
    481 
    482  /**
    483   * Retrieve the script location of the custom element definition for this node, when
    484   * relevant. To be linked to a custom element definition
    485   */
    486  getCustomElementLocation() {
    487    // Get a reference to the custom element definition function.
    488    const name = this.rawNode.localName;
    489 
    490    if (!this.rawNode.ownerGlobal) {
    491      return undefined;
    492    }
    493 
    494    const customElementsRegistry = this.rawNode.ownerGlobal.customElements;
    495    const customElement =
    496      customElementsRegistry && customElementsRegistry.get(name);
    497    if (!customElement) {
    498      return undefined;
    499    }
    500    // Create debugger object for the customElement function.
    501    const global = Cu.getGlobalForObject(customElement);
    502 
    503    const dbg = this.getParent().targetActor.makeDebugger();
    504 
    505    // If we hit a <browser> element of Firefox, its global will be the chrome window
    506    // which is system principal and will be in the same compartment as the debuggee.
    507    // For some reason, this happens when we run the content toolbox. As for the content
    508    // toolboxes, the modules are loaded in the same compartment as the <browser> element,
    509    // this throws as the debugger can _not_ be in the same compartment as the debugger.
    510    // This happens when we toggle fission for content toolbox because we try to reparent
    511    // the Walker of the tab. This happens because we do not detect in Walker.reparentRemoteFrame
    512    // that the target of the tab is the top level. That's because the target is a WindowGlobalTargetActor
    513    // which is retrieved via Node.getEmbedderElement and doesn't return the LocalTabTargetActor.
    514    // We should probably work on TabDescriptor so that the LocalTabTargetActor has a descriptor,
    515    // and see if we can possibly move the local tab specific out of the TargetActor and have
    516    // the TabDescriptor expose a pure WindowGlobalTargetActor?? (See bug 1579042)
    517    if (Cu.getObjectPrincipal(global) == Cu.getObjectPrincipal(dbg)) {
    518      return undefined;
    519    }
    520 
    521    const globalDO = dbg.addDebuggee(global);
    522    const customElementDO = globalDO.makeDebuggeeValue(customElement);
    523 
    524    // Return undefined if we can't find a script for the custom element definition.
    525    if (!customElementDO.script) {
    526      return undefined;
    527    }
    528 
    529    // NOTE: Debugger.Script.prototype.startColumn is 1-based.
    530    //       Convert to 0-based, while keeping the wasm's column (1) as is.
    531    //       (bug 1863878)
    532    const columnBase = customElementDO.script.format === "wasm" ? 0 : 1;
    533 
    534    return {
    535      url: customElementDO.script.url,
    536      line: customElementDO.script.startLine,
    537      column: customElementDO.script.startColumn - columnBase,
    538    };
    539  }
    540 
    541  /**
    542   * Returns a LongStringActor with the node's value.
    543   */
    544  getNodeValue() {
    545    return new LongStringActor(this.conn, this.rawNode.nodeValue || "");
    546  }
    547 
    548  /**
    549   * Set the node's value to a given string.
    550   */
    551  setNodeValue(value) {
    552    this.rawNode.nodeValue = value;
    553  }
    554 
    555  /**
    556   * Get a unique selector string for this node.
    557   */
    558  getUniqueSelector() {
    559    if (Cu.isDeadWrapper(this.rawNode)) {
    560      return "";
    561    }
    562    return findCssSelector(this.rawNode);
    563  }
    564 
    565  /**
    566   * Get the full CSS path for this node.
    567   *
    568   * @return {string} A CSS selector with a part for the node and each of its ancestors.
    569   */
    570  getCssPath() {
    571    if (Cu.isDeadWrapper(this.rawNode)) {
    572      return "";
    573    }
    574    return getCssPath(this.rawNode);
    575  }
    576 
    577  /**
    578   * Get the XPath for this node.
    579   *
    580   * @return {string} The XPath for finding this node on the page.
    581   */
    582  getXPath() {
    583    if (Cu.isDeadWrapper(this.rawNode)) {
    584      return "";
    585    }
    586    return getXPath(this.rawNode);
    587  }
    588 
    589  /**
    590   * Scroll the selected node into view.
    591   */
    592  scrollIntoView() {
    593    // this.rawNode can be an element without `scrollIntoView` (e.g. a `Text` or a `Comment`)
    594    // In such case, bail out.
    595    if (typeof this.rawNode.scrollIntoView !== "function") {
    596      return;
    597    }
    598    this.rawNode.scrollIntoView(true);
    599  }
    600 
    601  /**
    602   * Get the node's image data if any (for canvas and img nodes).
    603   * Returns an imageData object with the actual data being a LongStringActor
    604   * and a size json object.
    605   * The image data is transmitted as a base64 encoded png data-uri.
    606   * The method rejects if the node isn't an image or if the image is missing
    607   *
    608   * Accepts a maxDim request parameter to resize images that are larger. This
    609   * is important as the resizing occurs server-side so that image-data being
    610   * transfered in the longstring back to the client will be that much smaller
    611   */
    612  getImageData(maxDim) {
    613    return imageToImageData(this.rawNode, maxDim).then(imageData => {
    614      return {
    615        data: new LongStringActor(this.conn, imageData.data),
    616        size: imageData.size,
    617      };
    618    });
    619  }
    620 
    621  /**
    622   * Get all event listeners that are listening on this node.
    623   */
    624  getEventListenerInfo() {
    625    this._nsIEventListenersInfo.clear();
    626 
    627    const eventListenersData = this._eventCollector.getEventListeners(
    628      this.rawNode
    629    );
    630    let counter = 0;
    631    for (const eventListenerData of eventListenersData) {
    632      if (eventListenerData.nsIEventListenerInfo) {
    633        const id = `event-listener-info-${++counter}`;
    634        this._nsIEventListenersInfo.set(
    635          id,
    636          eventListenerData.nsIEventListenerInfo
    637        );
    638 
    639        eventListenerData.eventListenerInfoId = id;
    640        // remove the nsIEventListenerInfo since we don't want to send it to the client.
    641        delete eventListenerData.nsIEventListenerInfo;
    642      }
    643    }
    644    return eventListenersData;
    645  }
    646 
    647  /**
    648   * Disable a specific event listener given its associated id
    649   *
    650   * @param {string} eventListenerInfoId
    651   */
    652  disableEventListener(eventListenerInfoId) {
    653    const nsEventListenerInfo =
    654      this._nsIEventListenersInfo.get(eventListenerInfoId);
    655    if (!nsEventListenerInfo) {
    656      throw new Error("Unkown nsEventListenerInfo");
    657    }
    658    nsEventListenerInfo.enabled = false;
    659  }
    660 
    661  /**
    662   * (Re-)enable a specific event listener given its associated id
    663   *
    664   * @param {string} eventListenerInfoId
    665   */
    666  enableEventListener(eventListenerInfoId) {
    667    const nsEventListenerInfo =
    668      this._nsIEventListenersInfo.get(eventListenerInfoId);
    669    if (!nsEventListenerInfo) {
    670      throw new Error("Unkown nsEventListenerInfo");
    671    }
    672    nsEventListenerInfo.enabled = true;
    673  }
    674 
    675  /**
    676   * Modify a node's attributes.  Passed an array of modifications
    677   * similar in format to "attributes" mutations.
    678   * {
    679   *   attributeName: <string>
    680   *   attributeNamespace: <optional string>
    681   *   newValue: <optional string> - If null or undefined, the attribute
    682   *     will be removed.
    683   * }
    684   *
    685   * Returns when the modifications have been made.  Mutations will
    686   * be queued for any changes made.
    687   */
    688  modifyAttributes(modifications) {
    689    const rawNode = this.rawNode;
    690    for (const change of modifications) {
    691      if (change.newValue == null) {
    692        if (change.attributeNamespace) {
    693          rawNode.removeAttributeNS(
    694            change.attributeNamespace,
    695            change.attributeName
    696          );
    697        } else {
    698          rawNode.removeAttribute(change.attributeName);
    699        }
    700      } else if (change.attributeNamespace) {
    701        rawNode.setAttributeDevtoolsNS(
    702          change.attributeNamespace,
    703          change.attributeName,
    704          change.newValue
    705        );
    706      } else {
    707        rawNode.setAttributeDevtools(change.attributeName, change.newValue);
    708      }
    709    }
    710  }
    711 
    712  /**
    713   * Given the font and fill style, get the image data of a canvas with the
    714   * preview text and font.
    715   * Returns an imageData object with the actual data being a LongStringActor
    716   * and the width of the text as a string.
    717   * The image data is transmitted as a base64 encoded png data-uri.
    718   */
    719  getFontFamilyDataURL(font, fillStyle = "black") {
    720    const doc = this.rawNode.ownerDocument;
    721    const options = {
    722      previewText: FONT_FAMILY_PREVIEW_TEXT,
    723      previewFontSize: FONT_FAMILY_PREVIEW_TEXT_SIZE,
    724      fillStyle,
    725    };
    726    const { dataURL, size } = getFontPreviewData(font, doc, options);
    727 
    728    return { data: new LongStringActor(this.conn, dataURL), size };
    729  }
    730 
    731  /**
    732   * Finds the computed background color of the closest parent with a set background
    733   * color.
    734   *
    735   * @return {string}
    736   *         String with the background color of the form rgba(r, g, b, a). Defaults to
    737   *         rgba(255, 255, 255, 1) if no background color is found.
    738   */
    739  getClosestBackgroundColor() {
    740    return getClosestBackgroundColor(this.rawNode);
    741  }
    742 
    743  /**
    744   * Finds the background color range for the parent of a single text node
    745   * (i.e. for multi-colored backgrounds with gradients, images) or a single
    746   * background color for single-colored backgrounds. Defaults to the closest
    747   * background color if an error is encountered.
    748   *
    749   * @return {object}
    750   *         Object with one or more of the following properties: value, min, max
    751   */
    752  getBackgroundColor() {
    753    return getBackgroundColor(this);
    754  }
    755 
    756  /**
    757   * Returns an object with the width and height of the node's owner window.
    758   *
    759   * @return {object}
    760   */
    761  getOwnerGlobalDimensions() {
    762    const win = this.rawNode.ownerGlobal;
    763    return {
    764      innerWidth: win.innerWidth,
    765      innerHeight: win.innerHeight,
    766    };
    767  }
    768 
    769  /**
    770   * If the current node is an iframe, wait for the content window to be loaded.
    771   */
    772  async waitForFrameLoad() {
    773    if (this.useChildTargetToFetchChildren) {
    774      // If the document is handled by a dedicated target, we'll wait for a DOCUMENT_EVENT
    775      // on the created target.
    776      throw new Error(
    777        "iframe content document has its own target, use that one instead"
    778      );
    779    }
    780 
    781    if (Cu.isDeadWrapper(this.rawNode)) {
    782      throw new Error("Node is dead");
    783    }
    784 
    785    const { contentDocument } = this.rawNode;
    786    if (!contentDocument) {
    787      throw new Error("Can't access contentDocument");
    788    }
    789 
    790    if (contentDocument.readyState === "uninitialized") {
    791      // If the readyState is "uninitialized", the document is probably an about:blank
    792      // transient document. In such case, we want to wait until the "final" document
    793      // is inserted.
    794 
    795      const { chromeEventHandler } = this.rawNode.ownerGlobal.docShell;
    796      const browsingContextID = this.rawNode.browsingContext.id;
    797      await new Promise((resolve, reject) => {
    798        this._waitForFrameLoadAbortController = new AbortController();
    799 
    800        chromeEventHandler.addEventListener(
    801          "DOMDocElementInserted",
    802          e => {
    803            const { browsingContext } = e.target.defaultView;
    804            // Check that the document we're notified about is the iframe one.
    805            if (browsingContext.id == browsingContextID) {
    806              resolve();
    807              this._waitForFrameLoadAbortController.abort();
    808            }
    809          },
    810          { signal: this._waitForFrameLoadAbortController.signal }
    811        );
    812 
    813        // It might happen that the "final" document will be a remote one, living in a
    814        // different process, which means we won't get the DOMDocElementInserted event
    815        // here, and will wait forever. To prevent this Promise to hang forever, we use
    816        // a setInterval to check if the final document can be reached, so we can reject
    817        // if it's not.
    818        // This is definitely not a perfect solution, but I wasn't able to find something
    819        // better for this feature. I think it's _fine_ as this method will be removed
    820        // when EFT is  enabled everywhere in release.
    821        this._waitForFrameLoadIntervalId = setInterval(() => {
    822          if (Cu.isDeadWrapper(this.rawNode) || !this.rawNode.contentDocument) {
    823            reject("Can't access the iframe content document");
    824            clearInterval(this._waitForFrameLoadIntervalId);
    825            this._waitForFrameLoadIntervalId = null;
    826            this._waitForFrameLoadAbortController.abort();
    827          }
    828        }, 50);
    829      });
    830    }
    831 
    832    if (this.rawNode.contentDocument.readyState === "loading") {
    833      await new Promise(resolve => {
    834        DOMHelpers.onceDOMReady(this.rawNode.contentWindow, resolve);
    835      });
    836    }
    837  }
    838 }
    839 
    840 /**
    841 * Server side of a node list as returned by querySelectorAll()
    842 */
    843 class NodeListActor extends Actor {
    844  constructor(walker, nodeList) {
    845    super(walker.conn, nodeListSpec);
    846    this.walker = walker;
    847    this.nodeList = nodeList || [];
    848  }
    849 
    850  /**
    851   * Items returned by this actor should belong to the parent walker.
    852   */
    853  marshallPool() {
    854    return this.walker;
    855  }
    856 
    857  // Returns the JSON representation of this object over the wire.
    858  form() {
    859    return {
    860      actor: this.actorID,
    861      length: this.nodeList ? this.nodeList.length : 0,
    862    };
    863  }
    864 
    865  /**
    866   * Get a single node from the node list.
    867   */
    868  item(index) {
    869    return this.walker.attachElement(this.nodeList[index]);
    870  }
    871 
    872  /**
    873   * Get a range of the items from the node list.
    874   */
    875  items(start = 0, end = this.nodeList.length) {
    876    const items = Array.prototype.slice
    877      .call(this.nodeList, start, end)
    878      .map(item => this.walker._getOrCreateNodeActor(item));
    879    return this.walker.attachElements(items);
    880  }
    881 
    882  release() {}
    883 }
    884 
    885 exports.NodeActor = NodeActor;
    886 exports.NodeListActor = NodeListActor;