tor-browser

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

inspector.js (11923B)


      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 /**
      8 * Here's the server side of the remote inspector.
      9 *
     10 * The WalkerActor is the client's view of the debuggee's DOM.  It's gives
     11 * the client a tree of NodeActor objects.
     12 *
     13 * The walker presents the DOM tree mostly unmodified from the source DOM
     14 * tree, but with a few key differences:
     15 *
     16 *  - Empty text nodes are ignored.  This is pretty typical of developer
     17 *    tools, but maybe we should reconsider that on the server side.
     18 *  - iframes with documents loaded have the loaded document as the child,
     19 *    the walker provides one big tree for the whole document tree.
     20 *
     21 * There are a few ways to get references to NodeActors:
     22 *
     23 *   - When you first get a WalkerActor reference, it comes with a free
     24 *     reference to the root document's node.
     25 *   - Given a node, you can ask for children, siblings, and parents.
     26 *   - You can issue querySelector and querySelectorAll requests to find
     27 *     other elements.
     28 *   - Requests that return arbitrary nodes from the tree (like querySelector
     29 *     and querySelectorAll) will also return any nodes the client hasn't
     30 *     seen in order to have a complete set of parents.
     31 *
     32 * Once you have a NodeFront, you should be able to answer a few questions
     33 * without further round trips, like the node's name, namespace/tagName,
     34 * attributes, etc.  Other questions (like a text node's full nodeValue)
     35 * might require another round trip.
     36 *
     37 * The protocol guarantees that the client will always know the parent of
     38 * any node that is returned by the server.  This means that some requests
     39 * (like querySelector) will include the extra nodes needed to satisfy this
     40 * requirement.  The client keeps track of this parent relationship, so the
     41 * node fronts form a tree that is a subset of the actual DOM tree.
     42 *
     43 *
     44 * We maintain this guarantee to support the ability to release subtrees on
     45 * the client - when a node is disconnected from the DOM tree we want to be
     46 * able to free the client objects for all the children nodes.
     47 *
     48 * So to be able to answer "all the children of a given node that we have
     49 * seen on the client side", we guarantee that every time we've seen a node,
     50 * we connect it up through its parents.
     51 */
     52 
     53 const { Actor } = require("resource://devtools/shared/protocol.js");
     54 const {
     55  inspectorSpec,
     56 } = require("resource://devtools/shared/specs/inspector.js");
     57 
     58 const { setTimeout } = ChromeUtils.importESModule(
     59  "resource://gre/modules/Timer.sys.mjs",
     60  { global: "contextual" }
     61 );
     62 const {
     63  LongStringActor,
     64 } = require("resource://devtools/server/actors/string.js");
     65 
     66 loader.lazyRequireGetter(
     67  this,
     68  "InspectorActorUtils",
     69  "resource://devtools/server/actors/inspector/utils.js"
     70 );
     71 loader.lazyRequireGetter(
     72  this,
     73  "WalkerActor",
     74  "resource://devtools/server/actors/inspector/walker.js",
     75  true
     76 );
     77 loader.lazyRequireGetter(
     78  this,
     79  "EyeDropper",
     80  "resource://devtools/server/actors/highlighters/eye-dropper.js",
     81  true
     82 );
     83 loader.lazyRequireGetter(
     84  this,
     85  "PageStyleActor",
     86  "resource://devtools/server/actors/page-style.js",
     87  true
     88 );
     89 loader.lazyRequireGetter(
     90  this,
     91  ["CustomHighlighterActor", "isTypeRegistered", "HighlighterEnvironment"],
     92  "resource://devtools/server/actors/highlighters.js",
     93  true
     94 );
     95 loader.lazyRequireGetter(
     96  this,
     97  "CompatibilityActor",
     98  "resource://devtools/server/actors/compatibility/compatibility.js",
     99  true
    100 );
    101 
    102 const SVG_NS = "http://www.w3.org/2000/svg";
    103 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    104 
    105 /**
    106 * Server side of the inspector actor, which is used to create
    107 * inspector-related actors, including the walker.
    108 */
    109 class InspectorActor extends Actor {
    110  constructor(conn, targetActor) {
    111    super(conn, inspectorSpec);
    112    this.targetActor = targetActor;
    113 
    114    this._onColorPicked = this._onColorPicked.bind(this);
    115    this._onColorPickCanceled = this._onColorPickCanceled.bind(this);
    116    this.destroyEyeDropper = this.destroyEyeDropper.bind(this);
    117  }
    118 
    119  highlightersState = {
    120    fadingViewportSizeHiglighter: null,
    121  };
    122 
    123  destroy() {
    124    super.destroy();
    125    this.destroyEyeDropper();
    126 
    127    this._compatibility = null;
    128    this._pageStylePromise = null;
    129    this._walkerPromise = null;
    130    this.walker = null;
    131    this.targetActor = null;
    132  }
    133 
    134  get window() {
    135    return this.targetActor.window;
    136  }
    137 
    138  getWalker(options = {}) {
    139    if (this._walkerPromise) {
    140      return this._walkerPromise;
    141    }
    142 
    143    this._walkerPromise = new Promise(resolve => {
    144      const domReady = () => {
    145        const targetActor = this.targetActor;
    146        this.walker = new WalkerActor(this.conn, targetActor, options);
    147        this.manage(this.walker);
    148        this.walker.once("destroyed", () => {
    149          this._walkerPromise = null;
    150          this._pageStylePromise = null;
    151        });
    152        resolve(this.walker);
    153      };
    154 
    155      if (this.window.document.readyState === "loading") {
    156        // Expose an abort controller for DOMContentLoaded to remove the
    157        // listener unconditionally, even if the race hits the timeout.
    158        const abortController = new AbortController();
    159        Promise.race([
    160          new Promise(r => {
    161            this.window.addEventListener("DOMContentLoaded", r, {
    162              capture: true,
    163              once: true,
    164              signal: abortController.signal,
    165            });
    166          }),
    167          // The DOMContentLoaded event will never be emitted on documents stuck
    168          // in the loading state, for instance if document.write was called
    169          // without calling document.close.
    170          // TODO: It is not clear why we are waiting for the event overall, see
    171          // Bug 1766279 to actually stop listening to the event altogether.
    172          new Promise(r => setTimeout(r, 500)),
    173        ])
    174          .then(domReady)
    175          .finally(() => abortController.abort());
    176      } else {
    177        domReady();
    178      }
    179    });
    180 
    181    return this._walkerPromise;
    182  }
    183 
    184  getPageStyle() {
    185    if (this._pageStylePromise) {
    186      return this._pageStylePromise;
    187    }
    188 
    189    this._pageStylePromise = this.getWalker().then(() => {
    190      const pageStyle = new PageStyleActor(this);
    191      this.manage(pageStyle);
    192      return pageStyle;
    193    });
    194    return this._pageStylePromise;
    195  }
    196 
    197  getCompatibility() {
    198    if (this._compatibility) {
    199      return this._compatibility;
    200    }
    201 
    202    this._compatibility = new CompatibilityActor(this);
    203    this.manage(this._compatibility);
    204    return this._compatibility;
    205  }
    206 
    207  /**
    208   * If consumers need to display several highlighters at the same time or
    209   * different types of highlighters, then this method should be used, passing
    210   * the type name of the highlighter needed as argument.
    211   * A new instance will be created everytime the method is called, so it's up
    212   * to the consumer to release it when it is not needed anymore
    213   *
    214   * @param {string} type The type of highlighter to create
    215   * @return {Highlighter} The highlighter actor instance or null if the
    216   * typeName passed doesn't match any available highlighter
    217   */
    218  async getHighlighterByType(typeName) {
    219    if (isTypeRegistered(typeName)) {
    220      const highlighterActor = new CustomHighlighterActor(this, typeName);
    221      if (highlighterActor.instance.isReady) {
    222        await highlighterActor.instance.isReady;
    223      }
    224 
    225      return highlighterActor;
    226    }
    227    return null;
    228  }
    229 
    230  /**
    231   * Get the node's image data if any (for canvas and img nodes).
    232   * Returns an imageData object with the actual data being a LongStringActor
    233   * and a size json object.
    234   * The image data is transmitted as a base64 encoded png data-uri.
    235   * The method rejects if the node isn't an image or if the image is missing
    236   *
    237   * Accepts a maxDim request parameter to resize images that are larger. This
    238   * is important as the resizing occurs server-side so that image-data being
    239   * transfered in the longstring back to the client will be that much smaller
    240   */
    241  getImageDataFromURL(url, maxDim) {
    242    const img = new this.window.Image();
    243    img.src = url;
    244 
    245    // imageToImageData waits for the image to load.
    246    return InspectorActorUtils.imageToImageData(img, maxDim).then(imageData => {
    247      return {
    248        data: new LongStringActor(this.conn, imageData.data),
    249        size: imageData.size,
    250      };
    251    });
    252  }
    253 
    254  /**
    255   * Resolve a URL to its absolute form, in the scope of a given content window.
    256   *
    257   * @param {string} url.
    258   * @param {NodeActor} node If provided, the owner window of this node will be
    259   * used to resolve the URL. Otherwise, the top-level content window will be
    260   * used instead.
    261   * @return {string} url.
    262   */
    263  resolveRelativeURL(url, node) {
    264    const document = InspectorActorUtils.isNodeDead(node)
    265      ? this.window.document
    266      : InspectorActorUtils.nodeDocument(node.rawNode);
    267 
    268    if (!document) {
    269      return url;
    270    }
    271 
    272    const baseURI = Services.io.newURI(document.baseURI);
    273    return Services.io.newURI(url, null, baseURI).spec;
    274  }
    275 
    276  /**
    277   * Create an instance of the eye-dropper highlighter and store it on this._eyeDropper.
    278   * Note that for now, a new instance is created every time to deal with page navigation.
    279   */
    280  createEyeDropper() {
    281    this.destroyEyeDropper();
    282    this._highlighterEnv = new HighlighterEnvironment();
    283    this._highlighterEnv.initFromTargetActor(this.targetActor);
    284    this._eyeDropper = new EyeDropper(this._highlighterEnv);
    285    return this._eyeDropper.isReady;
    286  }
    287 
    288  /**
    289   * Destroy the current eye-dropper highlighter instance.
    290   */
    291  destroyEyeDropper() {
    292    if (this._eyeDropper) {
    293      this.cancelPickColorFromPage();
    294      this._eyeDropper.destroy();
    295      this._eyeDropper = null;
    296      this._highlighterEnv.destroy();
    297      this._highlighterEnv = null;
    298    }
    299  }
    300 
    301  /**
    302   * Pick a color from the page using the eye-dropper. This method doesn't return anything
    303   * but will cause events to be sent to the front when a color is picked or when the user
    304   * cancels the picker.
    305   *
    306   * @param {object} options
    307   */
    308  async pickColorFromPage(options) {
    309    await this.createEyeDropper();
    310    this._eyeDropper.show(this.window.document.documentElement, options);
    311    this._eyeDropper.once("selected", this._onColorPicked);
    312    this._eyeDropper.once("canceled", this._onColorPickCanceled);
    313    this.targetActor.once("will-navigate", this.destroyEyeDropper);
    314  }
    315 
    316  /**
    317   * After the pickColorFromPage method is called, the only way to dismiss the eye-dropper
    318   * highlighter is for the user to click in the page and select a color. If you need to
    319   * dismiss the eye-dropper programatically instead, use this method.
    320   */
    321  cancelPickColorFromPage() {
    322    if (this._eyeDropper) {
    323      this._eyeDropper.hide();
    324      this._eyeDropper.off("selected", this._onColorPicked);
    325      this._eyeDropper.off("canceled", this._onColorPickCanceled);
    326      this.targetActor.off("will-navigate", this.destroyEyeDropper);
    327    }
    328  }
    329 
    330  /**
    331   * Check if the current document supports highlighters using a canvasFrame anonymous
    332   * content container.
    333   * It is impossible to detect the feature programmatically as some document types simply
    334   * don't render the canvasFrame without throwing any error.
    335   */
    336  supportsHighlighters() {
    337    const doc = this.targetActor.window.document;
    338    const ns = doc.documentElement.namespaceURI;
    339 
    340    // XUL documents do not support insertAnonymousContent().
    341    if (ns === XUL_NS) {
    342      return false;
    343    }
    344 
    345    // SVG documents do not render the canvasFrame (see Bug 1157592).
    346    if (ns === SVG_NS) {
    347      return false;
    348    }
    349 
    350    return true;
    351  }
    352 
    353  _onColorPicked(color) {
    354    this.emit("color-picked", color);
    355  }
    356 
    357  _onColorPickCanceled() {
    358    this.emit("color-pick-canceled");
    359  }
    360 }
    361 
    362 exports.InspectorActor = InspectorActor;