tor-browser

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

node.js (17218B)


      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 {
     13  nodeSpec,
     14  nodeListSpec,
     15 } = require("resource://devtools/shared/specs/node.js");
     16 const {
     17  SimpleStringFront,
     18 } = require("resource://devtools/client/fronts/string.js");
     19 
     20 loader.lazyRequireGetter(
     21  this,
     22  "nodeConstants",
     23  "resource://devtools/shared/dom-node-constants.js"
     24 );
     25 
     26 const { XPCOMUtils } = ChromeUtils.importESModule(
     27  "resource://gre/modules/XPCOMUtils.sys.mjs"
     28 );
     29 
     30 XPCOMUtils.defineLazyPreferenceGetter(
     31  this,
     32  "browserToolboxScope",
     33  "devtools.browsertoolbox.scope"
     34 );
     35 
     36 const BROWSER_TOOLBOX_SCOPE_EVERYTHING = "everything";
     37 
     38 const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
     39 
     40 /**
     41 * Client side of a node list as returned by querySelectorAll()
     42 */
     43 class NodeListFront extends FrontClassWithSpec(nodeListSpec) {
     44  marshallPool() {
     45    return this.getParent();
     46  }
     47 
     48  // Update the object given a form representation off the wire.
     49  form(json) {
     50    this.length = json.length;
     51  }
     52 
     53  item(index) {
     54    return super.item(index).then(response => {
     55      return response.node;
     56    });
     57  }
     58 
     59  items(start, end) {
     60    return super.items(start, end).then(response => {
     61      return response.nodes;
     62    });
     63  }
     64 }
     65 
     66 registerFront(NodeListFront);
     67 
     68 /**
     69 * Convenience API for building a list of attribute modifications
     70 * for the `modifyAttributes` request.
     71 */
     72 class AttributeModificationList {
     73  constructor(node) {
     74    this.node = node;
     75    this.modifications = [];
     76  }
     77 
     78  apply() {
     79    const ret = this.node.modifyAttributes(this.modifications);
     80    return ret;
     81  }
     82 
     83  destroy() {
     84    this.node = null;
     85    this.modification = null;
     86  }
     87 
     88  setAttributeNS(ns, name, value) {
     89    this.modifications.push({
     90      attributeNamespace: ns,
     91      attributeName: name,
     92      newValue: value,
     93    });
     94  }
     95 
     96  setAttribute(name, value) {
     97    this.setAttributeNS(undefined, name, value);
     98  }
     99 
    100  removeAttributeNS(ns, name) {
    101    this.setAttributeNS(ns, name, undefined);
    102  }
    103 
    104  removeAttribute(name) {
    105    this.setAttributeNS(undefined, name, undefined);
    106  }
    107 }
    108 
    109 /**
    110 * Client side of the node actor.
    111 *
    112 * Node fronts are strored in a tree that mirrors the DOM tree on the
    113 * server, but with a few key differences:
    114 *  - Not all children will be necessary loaded for each node.
    115 *  - The order of children isn't guaranteed to be the same as the DOM.
    116 * Children are stored in a doubly-linked list, to make addition/removal
    117 * and traversal quick.
    118 *
    119 * Due to the order/incompleteness of the child list, it is safe to use
    120 * the parent node from clients, but the `children` request should be used
    121 * to traverse children.
    122 */
    123 class NodeFront extends FrontClassWithSpec(nodeSpec) {
    124  constructor(conn, targetFront, parentFront) {
    125    super(conn, targetFront, parentFront);
    126    // The parent node
    127    this._parent = null;
    128    // The first child of this node.
    129    this._child = null;
    130    // The next sibling of this node.
    131    this._next = null;
    132    // The previous sibling of this node.
    133    this._prev = null;
    134    // Store the flag to use it after destroy, where targetFront is set to null.
    135    this._hasParentProcessTarget = targetFront.isParentProcess;
    136  }
    137 
    138  /**
    139   * Destroy a node front.  The node must have been removed from the
    140   * ownership tree before this is called, unless the whole walker front
    141   * is being destroyed.
    142   */
    143  destroy() {
    144    super.destroy();
    145  }
    146 
    147  // Update the object given a form representation off the wire.
    148  form(form, ctx) {
    149    // backward-compatibility: shortValue indicates we are connected to old server
    150    if (form.shortValue) {
    151      // If the value is not complete, set nodeValue to null, it will be fetched
    152      // when calling getNodeValue()
    153      form.nodeValue = form.incompleteValue ? null : form.shortValue;
    154    }
    155 
    156    this.traits = form.traits || {};
    157 
    158    // Shallow copy of the form.  We could just store a reference, but
    159    // eventually we'll want to update some of the data.
    160    this._form = Object.assign({}, form);
    161    this._form.attrs = this._form.attrs ? this._form.attrs.slice() : [];
    162 
    163    if (form.parent) {
    164      // Get the owner actor for this actor (the walker), and find the
    165      // parent node of this actor from it, creating a standin node if
    166      // necessary.
    167      const owner = ctx.marshallPool();
    168      if (typeof owner.ensureDOMNodeFront === "function") {
    169        const parentNodeFront = owner.ensureDOMNodeFront(form.parent);
    170        this.reparent(parentNodeFront);
    171      }
    172    }
    173 
    174    if (form.host) {
    175      const owner = ctx.marshallPool();
    176      if (typeof owner.ensureDOMNodeFront === "function") {
    177        this.host = owner.ensureDOMNodeFront(form.host);
    178      }
    179    }
    180 
    181    if (form.inlineTextChild) {
    182      this.inlineTextChild = types
    183        .getType("domnode")
    184        .read(form.inlineTextChild, ctx);
    185    } else {
    186      this.inlineTextChild = undefined;
    187    }
    188  }
    189 
    190  /**
    191   * Returns the parent NodeFront for this NodeFront.
    192   */
    193  parentNode() {
    194    return this._parent;
    195  }
    196 
    197  /**
    198   * Returns the NodeFront corresponding to the parentNode of this NodeFront, or the
    199   * NodeFront corresponding to the host element for shadowRoot elements.
    200   */
    201  parentOrHost() {
    202    return this.isShadowRoot ? this.host : this._parent;
    203  }
    204 
    205  /**
    206   * Returns the owner DocumentElement|ShadowRootElement NodeFront for this NodeFront,
    207   * or null if such element can't be found.
    208   *
    209   * @returns {NodeFront|null}
    210   */
    211  getOwnerRootNodeFront() {
    212    let currentNode = this;
    213    while (currentNode) {
    214      if (
    215        currentNode.isShadowRoot ||
    216        currentNode.nodeType === Node.DOCUMENT_NODE
    217      ) {
    218        return currentNode;
    219      }
    220 
    221      currentNode = currentNode.parentNode();
    222    }
    223 
    224    return null;
    225  }
    226 
    227  /**
    228   * Process a mutation entry as returned from the walker's `getMutations`
    229   * request.  Only tries to handle changes of the node's contents
    230   * themselves (character data and attribute changes), the walker itself
    231   * will keep the ownership tree up to date.
    232   */
    233  updateMutation(change) {
    234    if (change.type === "attributes") {
    235      // We'll need to lazily reparse the attributes after this change.
    236      this._attrMap = undefined;
    237 
    238      // Update any already-existing attributes.
    239      let found = false;
    240      for (let i = 0; i < this.attributes.length; i++) {
    241        const attr = this.attributes[i];
    242        if (
    243          attr.name == change.attributeName &&
    244          attr.namespace == change.attributeNamespace
    245        ) {
    246          if (change.newValue !== null) {
    247            attr.value = change.newValue;
    248          } else {
    249            this.attributes.splice(i, 1);
    250          }
    251          found = true;
    252          break;
    253        }
    254      }
    255      // This is a new attribute. The null check is because of Bug 1192270,
    256      // in the case of a newly added then removed attribute
    257      if (!found && change.newValue !== null) {
    258        this.attributes.push({
    259          name: change.attributeName,
    260          namespace: change.attributeNamespace,
    261          value: change.newValue,
    262        });
    263      }
    264    } else if (change.type === "characterData") {
    265      this._form.nodeValue = change.newValue;
    266    } else if (change.type === "pseudoClassLock") {
    267      this._form.pseudoClassLocks = change.pseudoClassLocks;
    268    } else if (change.type === "events") {
    269      this._form.hasEventListeners = change.hasEventListeners;
    270    } else if (change.type === "mutationBreakpoint") {
    271      this._form.mutationBreakpoints = change.mutationBreakpoints;
    272    }
    273  }
    274 
    275  // Some accessors to make NodeFront feel more like a Node
    276 
    277  get id() {
    278    return this.getAttribute("id");
    279  }
    280 
    281  get nodeType() {
    282    return this._form.nodeType;
    283  }
    284  get namespaceURI() {
    285    return this._form.namespaceURI;
    286  }
    287  get nodeName() {
    288    return this._form.nodeName;
    289  }
    290  get displayName() {
    291    // @backward-compat { version 147 } When 147 reaches release, we can remove this 'if' block.
    292    // The form's displayName will be populated correctly for pseudo elements.
    293    if (
    294      this.isPseudoElement &&
    295      !this.traits.hasPseudoElementNameInDisplayName
    296    ) {
    297      if (this.isMarkerPseudoElement) {
    298        return "::marker";
    299      }
    300      if (this.isBeforePseudoElement) {
    301        return "::before";
    302      }
    303      if (this.isAfterPseudoElement) {
    304        return "::after";
    305      }
    306    }
    307 
    308    return this._form.displayName;
    309  }
    310  get doctypeString() {
    311    return (
    312      "<!DOCTYPE " +
    313      this._form.name +
    314      (this._form.publicId ? ' PUBLIC "' + this._form.publicId + '"' : "") +
    315      (this._form.systemId ? ' "' + this._form.systemId + '"' : "") +
    316      ">"
    317    );
    318  }
    319 
    320  get baseURI() {
    321    return this._form.baseURI;
    322  }
    323 
    324  get browsingContextID() {
    325    return this._form.browsingContextID;
    326  }
    327 
    328  get className() {
    329    return this.getAttribute("class") || "";
    330  }
    331 
    332  // Check if the node has children but the current DevTools session is unable
    333  // to retrieve them.
    334  // Typically: a <frame> or <browser> element which loads a document in another
    335  // process, but the toolbox' configuration prevents to inspect it (eg the
    336  // parent-process only Browser Toolbox).
    337  get childrenUnavailable() {
    338    return (
    339      // If form.useChildTargetToFetchChildren is true, it means the node HAS
    340      // children in another target.
    341      // Note: useChildTargetToFetchChildren might be undefined, force
    342      // conversion to boolean. See Bug 1783613 to try and improve this.
    343      !!this._form.useChildTargetToFetchChildren &&
    344      // But if useChildTargetToFetchChildren is false, it means the client
    345      // configuration prevents from displaying such children.
    346      // This is the only case where children are considered as unavailable:
    347      // they exist, but can't be retrieved by configuration.
    348      !this.useChildTargetToFetchChildren
    349    );
    350  }
    351  get hasChildren() {
    352    return this.numChildren > 0;
    353  }
    354  get numChildren() {
    355    if (this.childrenUnavailable) {
    356      return 0;
    357    }
    358 
    359    return this._form.numChildren;
    360  }
    361  get useChildTargetToFetchChildren() {
    362    if (
    363      this._hasParentProcessTarget &&
    364      browserToolboxScope != BROWSER_TOOLBOX_SCOPE_EVERYTHING
    365    ) {
    366      return false;
    367    }
    368 
    369    return !!this._form.useChildTargetToFetchChildren;
    370  }
    371  get hasEventListeners() {
    372    return this._form.hasEventListeners;
    373  }
    374 
    375  get isMarkerPseudoElement() {
    376    return this._form.isMarkerPseudoElement;
    377  }
    378  get isBeforePseudoElement() {
    379    return this._form.isBeforePseudoElement;
    380  }
    381  get isAfterPseudoElement() {
    382    return this._form.isAfterPseudoElement;
    383  }
    384  get isPseudoElement() {
    385    // @backward-compat { version 147 } When 147 reaches release, we can remove this 'if' block,
    386    // as well as the isXXXPseudoElement getters.
    387    // The form isPseudoElement property will be populated correctly.
    388    if (!this.traits.hasPseudoElementNameInDisplayName) {
    389      return (
    390        this.isBeforePseudoElement ||
    391        this.isAfterPseudoElement ||
    392        this.isMarkerPseudoElement
    393      );
    394    }
    395 
    396    return this._form.isPseudoElement;
    397  }
    398  get isNativeAnonymous() {
    399    return this._form.isNativeAnonymous;
    400  }
    401  get isInHTMLDocument() {
    402    return this._form.isInHTMLDocument;
    403  }
    404  get tagName() {
    405    return this.nodeType === nodeConstants.ELEMENT_NODE ? this.nodeName : null;
    406  }
    407 
    408  get isDocumentElement() {
    409    return !!this._form.isDocumentElement;
    410  }
    411 
    412  get isTopLevelDocument() {
    413    return this._form.isTopLevelDocument;
    414  }
    415 
    416  get isShadowRoot() {
    417    return this._form.isShadowRoot;
    418  }
    419 
    420  get shadowRootMode() {
    421    return this._form.shadowRootMode;
    422  }
    423 
    424  get isShadowHost() {
    425    return this._form.isShadowHost;
    426  }
    427 
    428  get customElementLocation() {
    429    return this._form.customElementLocation;
    430  }
    431 
    432  get isDirectShadowHostChild() {
    433    return this._form.isDirectShadowHostChild;
    434  }
    435 
    436  // doctype properties
    437  get name() {
    438    return this._form.name;
    439  }
    440  get publicId() {
    441    return this._form.publicId;
    442  }
    443  get systemId() {
    444    return this._form.systemId;
    445  }
    446 
    447  getAttribute(name) {
    448    const attr = this._getAttribute(name);
    449    return attr ? attr.value : null;
    450  }
    451  hasAttribute(name) {
    452    this._cacheAttributes();
    453    return name in this._attrMap;
    454  }
    455 
    456  get hidden() {
    457    const cls = this.getAttribute("class");
    458    return cls && cls.indexOf(HIDDEN_CLASS) > -1;
    459  }
    460 
    461  get attributes() {
    462    return this._form.attrs;
    463  }
    464 
    465  get mutationBreakpoints() {
    466    return this._form.mutationBreakpoints;
    467  }
    468 
    469  get pseudoClassLocks() {
    470    return this._form.pseudoClassLocks || [];
    471  }
    472  hasPseudoClassLock(pseudo) {
    473    return this.pseudoClassLocks.some(locked => locked === pseudo);
    474  }
    475 
    476  get displayType() {
    477    return this._form.displayType;
    478  }
    479 
    480  get isDisplayed() {
    481    return this._form.isDisplayed;
    482  }
    483 
    484  get isScrollable() {
    485    return this._form.isScrollable;
    486  }
    487 
    488  get causesOverflow() {
    489    return this._form.causesOverflow;
    490  }
    491 
    492  get containerType() {
    493    return this._form.containerType;
    494  }
    495 
    496  get anchorName() {
    497    return this._form.anchorName;
    498  }
    499 
    500  get isTreeDisplayed() {
    501    let parent = this;
    502    while (parent) {
    503      if (!parent.isDisplayed) {
    504        return false;
    505      }
    506      parent = parent.parentNode();
    507    }
    508    return true;
    509  }
    510 
    511  get inspectorFront() {
    512    return this.parentFront.parentFront;
    513  }
    514 
    515  get walkerFront() {
    516    return this.parentFront;
    517  }
    518 
    519  getNodeValue() {
    520    // backward-compatibility: if nodevalue is null and shortValue is defined, the actual
    521    // value of the node needs to be fetched on the server.
    522    if (this._form.nodeValue === null && this._form.shortValue) {
    523      return super.getNodeValue();
    524    }
    525 
    526    const str = this._form.nodeValue || "";
    527    return Promise.resolve(new SimpleStringFront(str));
    528  }
    529 
    530  /**
    531   * Return a new AttributeModificationList for this node.
    532   */
    533  startModifyingAttributes() {
    534    return new AttributeModificationList(this);
    535  }
    536 
    537  _cacheAttributes() {
    538    if (typeof this._attrMap != "undefined") {
    539      return;
    540    }
    541    this._attrMap = {};
    542    for (const attr of this.attributes) {
    543      this._attrMap[attr.name] = attr;
    544    }
    545  }
    546 
    547  _getAttribute(name) {
    548    this._cacheAttributes();
    549    return this._attrMap[name] || undefined;
    550  }
    551 
    552  /**
    553   * Set this node's parent.  Note that the children saved in
    554   * this tree are unordered and incomplete, so shouldn't be used
    555   * instead of a `children` request.
    556   */
    557  reparent(parent) {
    558    if (this._parent === parent) {
    559      return;
    560    }
    561 
    562    if (this._parent && this._parent._child === this) {
    563      this._parent._child = this._next;
    564    }
    565    if (this._prev) {
    566      this._prev._next = this._next;
    567    }
    568    if (this._next) {
    569      this._next._prev = this._prev;
    570    }
    571    this._next = null;
    572    this._prev = null;
    573    this._parent = parent;
    574    if (!parent) {
    575      // Subtree is disconnected, we're done
    576      return;
    577    }
    578    this._next = parent._child;
    579    if (this._next) {
    580      this._next._prev = this;
    581    }
    582    parent._child = this;
    583  }
    584 
    585  /**
    586   * Return all the known children of this node.
    587   */
    588  treeChildren() {
    589    const ret = [];
    590    for (let child = this._child; child != null; child = child._next) {
    591      ret.push(child);
    592    }
    593    return ret;
    594  }
    595 
    596  /**
    597   * Do we use a local target?
    598   * Useful to know if a rawNode is available or not.
    599   *
    600   * This will, one day, be removed. External code should
    601   * not need to know if the target is remote or not.
    602   */
    603  isLocalToBeDeprecated() {
    604    return !!this.conn._transport._serverConnection;
    605  }
    606 
    607  /**
    608   * Get a Node for the given node front.  This only works locally,
    609   * and is only intended as a stopgap during the transition to the remote
    610   * protocol.  If you depend on this you're likely to break soon.
    611   */
    612  rawNode() {
    613    if (!this.isLocalToBeDeprecated()) {
    614      console.warn("Tried to use rawNode on a remote connection.");
    615      return null;
    616    }
    617    const {
    618      DevToolsServer,
    619    } = require("resource://devtools/server/devtools-server.js");
    620    const actor = DevToolsServer.searchAllConnectionsForActor(this.actorID);
    621    if (!actor) {
    622      // Can happen if we try to get the raw node for an already-expired
    623      // actor.
    624      return null;
    625    }
    626    return actor.rawNode;
    627  }
    628 
    629  async connectToFrame() {
    630    if (!this.useChildTargetToFetchChildren) {
    631      console.warn("Tried to open connection to an invalid frame.");
    632      return null;
    633    }
    634    if (
    635      this._childBrowsingContextTarget &&
    636      !this._childBrowsingContextTarget.isDestroyed()
    637    ) {
    638      return this._childBrowsingContextTarget;
    639    }
    640 
    641    // Get the target for this frame element
    642    this._childBrowsingContextTarget =
    643      await this.targetFront.getWindowGlobalTarget(
    644        this._form.browsingContextID
    645      );
    646 
    647    // Bug 1776250: When the target is destroyed, we need to easily find the
    648    // parent node front so that we can update its frontend container in the
    649    // markup-view.
    650    this._childBrowsingContextTarget.setParentNodeFront(this);
    651 
    652    return this._childBrowsingContextTarget;
    653  }
    654 }
    655 
    656 exports.NodeFront = NodeFront;
    657 registerFront(NodeFront);