tor-browser

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

walker.js (15367B)


      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 {
      8  FrontClassWithSpec,
      9  types,
     10  registerFront,
     11 } = require("resource://devtools/shared/protocol.js");
     12 const { walkerSpec } = require("resource://devtools/shared/specs/walker.js");
     13 const {
     14  safeAsyncMethod,
     15 } = require("resource://devtools/shared/async-utils.js");
     16 
     17 /**
     18 * Client side of the DOM walker.
     19 */
     20 class WalkerFront extends FrontClassWithSpec(walkerSpec) {
     21  constructor(client, targetFront, parentFront) {
     22    super(client, targetFront, parentFront);
     23    this._isPicking = false;
     24    this._orphaned = new Set();
     25    this._retainedOrphans = new Set();
     26 
     27    // Set to true if cleanup should be requested after every mutation list.
     28    this.autoCleanup = true;
     29 
     30    this._rootNodePromise = new Promise(
     31      r => (this._rootNodePromiseResolve = r)
     32    );
     33 
     34    this._onRootNodeAvailable = this._onRootNodeAvailable.bind(this);
     35    this._onRootNodeDestroyed = this._onRootNodeDestroyed.bind(this);
     36 
     37    // pick/cancelPick requests can be triggered while the Walker is being destroyed.
     38    this.pick = safeAsyncMethod(this.pick.bind(this), () => this.isDestroyed());
     39    this.cancelPick = safeAsyncMethod(this.cancelPick.bind(this), () =>
     40      this.isDestroyed()
     41    );
     42 
     43    this.before("new-mutations", this.onMutations.bind(this));
     44 
     45    // Those events will be emitted if watchRootNode was called on the
     46    // corresponding WalkerActor, which should be handled by the ResourceCommand
     47    // as long as a consumer is watching for root-node resources.
     48    // This should be fixed by using watchResources directly from the walker
     49    // front, either with the ROOT_NODE resource, or with the DOCUMENT_EVENT
     50    // resource. See Bug 1663973.
     51    this.on("root-available", this._onRootNodeAvailable);
     52    this.on("root-destroyed", this._onRootNodeDestroyed);
     53  }
     54 
     55  // Update the object given a form representation off the wire.
     56  form(json) {
     57    this.actorID = json.actor;
     58 
     59    // The rootNode property should usually be provided via watchRootNode.
     60    // However tests are currently using the walker front without explicitly
     61    // calling watchRootNode, so we keep this assignment as a fallback.
     62    this.rootNode = types.getType("domnode").read(json.root, this);
     63 
     64    // Bug 1861328: boolean set to true when color scheme can't be changed (happens when `privacy.resistFingerprinting` is set to true)
     65    this.rfpCSSColorScheme = json.rfpCSSColorScheme;
     66 
     67    this.traits = json.traits;
     68  }
     69 
     70  /**
     71   * Clients can use walker.rootNode to get the current root node of the
     72   * walker, but during a reload the root node might be null.  This
     73   * method returns a promise that will resolve to the root node when it is
     74   * set.
     75   */
     76  async getRootNode() {
     77    let rootNode = this.rootNode;
     78    if (!rootNode) {
     79      rootNode = await this._rootNodePromise;
     80    }
     81 
     82    return rootNode;
     83  }
     84 
     85  /**
     86   * When reading an actor form off the wire, we want to hook it up to its
     87   * parent or host front.  The protocol guarantees that the parent will
     88   * be seen by the client in either a previous or the current request.
     89   * So if we've already seen this parent return it, otherwise create
     90   * a bare-bones stand-in node.  The stand-in node will be updated
     91   * with a real form by the end of the deserialization.
     92   */
     93  ensureDOMNodeFront(id) {
     94    const front = this.getActorByID(id);
     95    if (front) {
     96      return front;
     97    }
     98 
     99    return types.getType("domnode").read({ actor: id }, this, "standin");
    100  }
    101 
    102  /**
    103   * See the documentation for WalkerActor.prototype.retainNode for
    104   * information on retained nodes.
    105   *
    106   * From the client's perspective, `retainNode` can fail if the node in
    107   * question is removed from the ownership tree before the `retainNode`
    108   * request reaches the server.  This can only happen if the client has
    109   * asked the server to release nodes but hasn't gotten a response
    110   * yet: Either a `releaseNode` request or a `getMutations` with `cleanup`
    111   * set is outstanding.
    112   *
    113   * If either of those requests is outstanding AND releases the retained
    114   * node, this request will fail with noSuchActor, but the ownership tree
    115   * will stay in a consistent state.
    116   *
    117   * Because the protocol guarantees that requests will be processed and
    118   * responses received in the order they were sent, we get the right
    119   * semantics by setting our local retained flag on the node only AFTER
    120   * a SUCCESSFUL retainNode call.
    121   */
    122  async retainNode(node) {
    123    await super.retainNode(node);
    124    node.retained = true;
    125  }
    126 
    127  async unretainNode(node) {
    128    await super.unretainNode(node);
    129    node.retained = false;
    130    if (this._retainedOrphans.has(node)) {
    131      this._retainedOrphans.delete(node);
    132      this._releaseFront(node);
    133    }
    134  }
    135 
    136  releaseNode(node, options = {}) {
    137    // NodeFront.destroy will destroy children in the ownership tree too,
    138    // mimicking what the server will do here.
    139    const actorID = node.actorID;
    140    this._releaseFront(node, !!options.force);
    141    return super.releaseNode({ actorID });
    142  }
    143 
    144  async findInspectingNode() {
    145    const response = await super.findInspectingNode();
    146    return response.node;
    147  }
    148 
    149  async querySelector(queryNode, selector) {
    150    const response = await super.querySelector(queryNode, selector);
    151    return response.node;
    152  }
    153 
    154  async getIdrefNode(queryNode, id) {
    155    const response = await super.getIdrefNode(queryNode, id);
    156    return response.node;
    157  }
    158 
    159  async getNodeActorFromWindowID(windowID) {
    160    const response = await super.getNodeActorFromWindowID(windowID);
    161    return response ? response.node : null;
    162  }
    163 
    164  async getNodeActorFromContentDomReference(contentDomReference) {
    165    const response = await super.getNodeActorFromContentDomReference(
    166      contentDomReference
    167    );
    168    return response ? response.node : null;
    169  }
    170 
    171  async getStyleSheetOwnerNode(styleSheetActorID) {
    172    const response = await super.getStyleSheetOwnerNode(styleSheetActorID);
    173    return response ? response.node : null;
    174  }
    175 
    176  async getNodeFromActor(actorID, path) {
    177    const response = await super.getNodeFromActor(actorID, path);
    178    return response ? response.node : null;
    179  }
    180 
    181  _releaseFront(node, force) {
    182    if (node.retained && !force) {
    183      node.reparent(null);
    184      this._retainedOrphans.add(node);
    185      return;
    186    }
    187 
    188    if (node.retained) {
    189      // Forcing a removal.
    190      this._retainedOrphans.delete(node);
    191    }
    192 
    193    // Release any children
    194    for (const child of node.treeChildren()) {
    195      this._releaseFront(child, force);
    196    }
    197 
    198    // All children will have been removed from the node by this point.
    199    node.reparent(null);
    200    node.destroy();
    201  }
    202 
    203  /**
    204   * Get any unprocessed mutation records and process them.
    205   */
    206  // eslint-disable-next-line complexity
    207  async getMutations(options = {}) {
    208    const mutations = await super.getMutations(options);
    209    const emitMutations = [];
    210    for (const change of mutations) {
    211      // The target is only an actorID, get the associated front.
    212      const targetID = change.target;
    213      const targetFront = this.getActorByID(targetID);
    214 
    215      if (!targetFront) {
    216        console.warn(
    217          "Got a mutation for an unexpected actor: " +
    218            targetID +
    219            ", please file a bug on bugzilla.mozilla.org!"
    220        );
    221        console.trace();
    222        continue;
    223      }
    224 
    225      const emittedMutation = Object.assign(change, { target: targetFront });
    226 
    227      if (change.type === "childList") {
    228        // Update the ownership tree according to the mutation record.
    229        const addedFronts = [];
    230        const removedFronts = [];
    231        for (const removed of change.removed) {
    232          const removedFront = this.getActorByID(removed);
    233          if (!removedFront) {
    234            console.error(
    235              "Got a removal of an actor we didn't know about: " + removed
    236            );
    237            continue;
    238          }
    239          // Remove from the ownership tree
    240          removedFront.reparent(null);
    241 
    242          // This node is orphaned unless we get it in the 'added' list
    243          // eventually.
    244          this._orphaned.add(removedFront);
    245          removedFronts.push(removedFront);
    246        }
    247        for (const added of change.added) {
    248          const addedFront = this.getActorByID(added);
    249          if (!addedFront) {
    250            console.error(
    251              "Got an addition of an actor we didn't know " + "about: " + added
    252            );
    253            continue;
    254          }
    255          addedFront.reparent(targetFront);
    256 
    257          // The actor is reconnected to the ownership tree, unorphan
    258          // it.
    259          this._orphaned.delete(addedFront);
    260          addedFronts.push(addedFront);
    261        }
    262 
    263        // Before passing to users, replace the added and removed actor
    264        // ids with front in the mutation record.
    265        emittedMutation.added = addedFronts;
    266        emittedMutation.removed = removedFronts;
    267 
    268        // If this is coming from a DOM mutation, the actor's numChildren
    269        // was passed in. Otherwise, it is simulated from a frame load or
    270        // unload, so don't change the front's form.
    271        if ("numChildren" in change) {
    272          targetFront._form.numChildren = change.numChildren;
    273        }
    274      } else if (change.type === "shadowRootAttached") {
    275        targetFront._form.isShadowHost = true;
    276      } else if (change.type === "customElementDefined") {
    277        targetFront._form.customElementLocation = change.customElementLocation;
    278      } else if (change.type === "unretained") {
    279        // Retained orphans were force-released without the intervention of
    280        // client (probably a navigated frame).
    281        for (const released of change.nodes) {
    282          const releasedFront = this.getActorByID(released);
    283          this._retainedOrphans.delete(released);
    284          this._releaseFront(releasedFront, true);
    285        }
    286      } else {
    287        targetFront.updateMutation(change);
    288      }
    289 
    290      // Update the inlineTextChild property of the target for a selected list of
    291      // mutation types.
    292      if (
    293        change.type === "inlineTextChild" ||
    294        change.type === "childList" ||
    295        change.type === "shadowRootAttached"
    296      ) {
    297        if (change.inlineTextChild) {
    298          targetFront.inlineTextChild = types
    299            .getType("domnode")
    300            .read(change.inlineTextChild, this);
    301        } else {
    302          targetFront.inlineTextChild = undefined;
    303        }
    304      }
    305 
    306      emitMutations.push(emittedMutation);
    307    }
    308 
    309    if (options.cleanup) {
    310      for (const node of this._orphaned) {
    311        // This will move retained nodes to this._retainedOrphans.
    312        this._releaseFront(node);
    313      }
    314      this._orphaned = new Set();
    315    }
    316 
    317    this.emit("mutations", emitMutations);
    318  }
    319 
    320  /**
    321   * Handle the `new-mutations` notification by fetching the
    322   * available mutation records.
    323   */
    324  onMutations() {
    325    // Fetch and process the mutations.
    326    this.getMutations({ cleanup: this.autoCleanup }).catch(() => {});
    327  }
    328 
    329  isLocal() {
    330    return !!this.conn._transport._serverConnection;
    331  }
    332 
    333  async removeNode(node) {
    334    const previousSibling = await this.previousSibling(node);
    335    const nextSibling = await super.removeNode(node);
    336    return {
    337      previousSibling,
    338      nextSibling,
    339    };
    340  }
    341 
    342  async children(node, options) {
    343    if (!node.useChildTargetToFetchChildren) {
    344      return super.children(node, options);
    345    }
    346    const target = await node.connectToFrame();
    347 
    348    // We had several issues in the past where `connectToFrame` was returning the same
    349    // target as the owner document one, which led to the inspector being broken.
    350    // Ultimately, we shouldn't get to this point (fix should happen in connectToFrame or
    351    // on the server, e.g. for Bug 1752342), but at least this will serve as a safe guard
    352    // so we don't freeze/crash the inspector.
    353    if (
    354      target == this.targetFront &&
    355      Services.prefs.getBoolPref(
    356        "devtools.testing.bypass-walker-children-iframe-guard",
    357        false
    358      ) !== true
    359    ) {
    360      console.warn("connectToFrame returned an unexpected target");
    361      return {
    362        nodes: [],
    363        hasFirst: true,
    364        hasLast: true,
    365      };
    366    }
    367 
    368    const walker = (await target.getFront("inspector")).walker;
    369 
    370    // Finally retrieve the NodeFront of the remote frame's document
    371    const documentNode = await walker.getRootNode();
    372 
    373    // Force reparenting through the remote frame boundary.
    374    documentNode.reparent(node);
    375 
    376    // And return the same kind of response `walker.children` returns
    377    return {
    378      nodes: [documentNode],
    379      hasFirst: true,
    380      hasLast: true,
    381    };
    382  }
    383 
    384  /**
    385   * Ensure that the RootNode of this Walker has the right parent NodeFront.
    386   *
    387   * This method does nothing if we are on the top level target's WalkerFront,
    388   * as the RootNode won't have any parent.
    389   *
    390   * Otherwise, if we are in an iframe's WalkerFront, we would expect the parent
    391   * of the RootNode (i.e. the NodeFront for the document loaded within the iframe)
    392   * to be the <iframe>'s NodeFront. Because of fission, the two NodeFront may refer
    393   * to DOM Element running in distinct processes and so the NodeFront comes from
    394   * two distinct Targets and two distinct WalkerFront.
    395   * This is why we need this manual "reparent" code to do the glue between the
    396   * two documents.
    397   */
    398  async reparentRemoteFrame() {
    399    const parentTarget = await this.targetFront.getParentTarget();
    400    if (!parentTarget) {
    401      return;
    402    }
    403    // Don't reparent if we are on the top target
    404    if (parentTarget == this.targetFront) {
    405      return;
    406    }
    407    // Get the NodeFront for the embedder element
    408    // i.e. the <iframe> element which is hosting the document that
    409    const parentWalker = (await parentTarget.getFront("inspector")).walker;
    410    // As this <iframe> most likely runs in another process, we have to get it through the parent
    411    // target's WalkerFront.
    412    const parentNode = (
    413      await parentWalker.getEmbedderElement(this.targetFront.browsingContextID)
    414    ).node;
    415 
    416    // Finally, set this embedder element's node front as the
    417    const documentNode = await this.getRootNode();
    418    documentNode.reparent(parentNode);
    419  }
    420 
    421  _onRootNodeAvailable(rootNode) {
    422    if (rootNode.isTopLevelDocument) {
    423      this.rootNode = rootNode;
    424      this._rootNodePromiseResolve(this.rootNode);
    425    }
    426  }
    427 
    428  _onRootNodeDestroyed(rootNode) {
    429    if (rootNode.isTopLevelDocument) {
    430      this._rootNodePromise = new Promise(
    431        r => (this._rootNodePromiseResolve = r)
    432      );
    433      this.rootNode = null;
    434    }
    435  }
    436 
    437  /**
    438   * Start the element picker on the debuggee target.
    439   *
    440   * @param {boolean} doFocus - Optionally focus the content area once the picker is
    441   *                            activated.
    442   */
    443  pick(doFocus) {
    444    if (this._isPicking) {
    445      return Promise.resolve();
    446    }
    447 
    448    this._isPicking = true;
    449 
    450    return super.pick(
    451      doFocus,
    452      this.targetFront.commands.descriptorFront.isLocalTab
    453    );
    454  }
    455 
    456  /**
    457   * Stop the element picker.
    458   */
    459  cancelPick() {
    460    if (!this._isPicking) {
    461      return Promise.resolve();
    462    }
    463 
    464    this._isPicking = false;
    465    return super.cancelPick();
    466  }
    467 }
    468 
    469 registerFront(WalkerFront);