tor-browser

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

highlighters.js (11423B)


      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("devtools/shared/protocol");
      8 const { customHighlighterSpec } = require("devtools/shared/specs/highlighters");
      9 const { TYPES } = ChromeUtils.importESModule(
     10  "resource://devtools/shared/highlighters.mjs",
     11  { global: "contextual" }
     12 );
     13 
     14 const EventEmitter = require("devtools/shared/event-emitter");
     15 
     16 loader.lazyRequireGetter(
     17  this,
     18  "isXUL",
     19  "resource://devtools/server/actors/highlighters/utils/markup.js",
     20  true
     21 );
     22 
     23 /**
     24 * The registration mechanism for highlighters provides a quick way to
     25 * have modular highlighters instead of a hard coded list.
     26 */
     27 const highlighterTypes = new Map();
     28 
     29 /**
     30 * Returns `true` if a highlighter for the given `typeName` is registered,
     31 * `false` otherwise.
     32 */
     33 const isTypeRegistered = typeName => highlighterTypes.has(typeName);
     34 exports.isTypeRegistered = isTypeRegistered;
     35 
     36 /**
     37 * Registers a given constructor as highlighter, for the `typeName` given.
     38 */
     39 const registerHighlighter = (typeName, modulePath) => {
     40  if (highlighterTypes.has(typeName)) {
     41    throw Error(`${typeName} is already registered.`);
     42  }
     43 
     44  highlighterTypes.set(typeName, modulePath);
     45 };
     46 
     47 /**
     48 * CustomHighlighterActor is a generic Actor that instantiates a custom implementation of
     49 * a highlighter class given its type name which must be registered in `highlighterTypes`.
     50 * CustomHighlighterActor proxies calls to methods of the highlighter class instance:
     51 * constructor(nargetActor), show(node, options), hide(), destroy()
     52 */
     53 exports.CustomHighlighterActor = class CustomHighligherActor extends Actor {
     54  /**
     55   * Create a highlighter instance given its typeName.
     56   */
     57  constructor(parent, typeName) {
     58    super(parent.conn, customHighlighterSpec);
     59 
     60    this._parent = parent;
     61 
     62    const modulePath = highlighterTypes.get(typeName);
     63    if (!modulePath) {
     64      const list = [...highlighterTypes.keys()];
     65 
     66      throw new Error(`${typeName} isn't a valid highlighter class (${list})`);
     67    }
     68 
     69    const constructor = require(modulePath)[typeName];
     70    // The assumption is that custom highlighters either need the canvasframe
     71    // container to append their elements and thus a non-XUL window or they have
     72    // to define a static XULSupported flag that indicates that the highlighter
     73    // supports XUL windows. Otherwise, bail out.
     74    if (!isXUL(this._parent.targetActor.window) || constructor.XULSupported) {
     75      this._highlighterEnv = new HighlighterEnvironment();
     76      this._highlighterEnv.initFromTargetActor(parent.targetActor);
     77      this._highlighter = new constructor(this._highlighterEnv, parent);
     78      if (this._highlighter.on) {
     79        this._highlighter.on(
     80          "highlighter-event",
     81          this._onHighlighterEvent.bind(this)
     82        );
     83      }
     84    } else {
     85      throw new Error(
     86        "Custom " + typeName + "highlighter cannot be created in a XUL window"
     87      );
     88    }
     89  }
     90 
     91  destroy() {
     92    super.destroy();
     93    this.finalize();
     94    this._parent = null;
     95  }
     96 
     97  release() {}
     98 
     99  /**
    100   * Get current instance of the highlighter object.
    101   */
    102  get instance() {
    103    return this._highlighter;
    104  }
    105 
    106  /**
    107   * Show the highlighter.
    108   * This calls through to the highlighter instance's |show(node, options)|
    109   * method.
    110   *
    111   * Most custom highlighters are made to highlight DOM nodes, hence the first
    112   * NodeActor argument (NodeActor as in devtools/server/actor/inspector).
    113   * Note however that some highlighters use this argument merely as a context
    114   * node: The SelectorHighlighter for instance uses it as a base node to run the
    115   * provided CSS selector on.
    116   *
    117   * @param {NodeActor} The node to be highlighted
    118   * @param {object} Options for the custom highlighter
    119   * @return {boolean} True, if the highlighter has been successfully shown
    120   */
    121  show(node, options) {
    122    if (!this._highlighter) {
    123      return null;
    124    }
    125 
    126    const rawNode = node?.rawNode;
    127 
    128    return this._highlighter.show(rawNode, options);
    129  }
    130 
    131  /**
    132   * Hide the highlighter if it was shown before
    133   */
    134  hide() {
    135    if (this._highlighter) {
    136      this._highlighter.hide();
    137    }
    138  }
    139 
    140  /**
    141   * Upon receiving an event from the highlighter, forward it to the client.
    142   */
    143  _onHighlighterEvent(data) {
    144    this.emit("highlighter-event", data);
    145  }
    146 
    147  /**
    148   * Destroy the custom highlighter implementation.
    149   * This method is called automatically just before the actor is destroyed.
    150   */
    151  finalize() {
    152    if (this._highlighter) {
    153      if (this._highlighter.off) {
    154        this._highlighter.off(
    155          "highlighter-event",
    156          this._onHighlighterEvent.bind(this)
    157        );
    158      }
    159      this._highlighter.destroy();
    160      this._highlighter = null;
    161    }
    162 
    163    if (this._highlighterEnv) {
    164      this._highlighterEnv.destroy();
    165      this._highlighterEnv = null;
    166    }
    167  }
    168 };
    169 
    170 /**
    171 * The HighlighterEnvironment is an object that holds all the required data for
    172 * highlighters to work: the window, docShell, event listener target, ...
    173 * It also emits "will-navigate", "navigate" and "window-ready" events,
    174 * similarly to the WindowGlobalTargetActor.
    175 *
    176 * It can be initialized either from a WindowGlobalTargetActor (which is the
    177 * most frequent way of using it, since highlighters are initialized by
    178 * CustomHighlighterActor, which has a targetActor reference).
    179 * It can also be initialized just with a window object (which is
    180 * useful for when a highlighter is used outside of the devtools server context.
    181 */
    182 
    183 class HighlighterEnvironment extends EventEmitter {
    184  initFromTargetActor(targetActor) {
    185    this._targetActor = targetActor;
    186 
    187    const relayedEvents = [
    188      "window-ready",
    189      "navigate",
    190      "will-navigate",
    191      "use-simple-highlighters-updated",
    192    ];
    193 
    194    this._abortController = new AbortController();
    195    const signal = this._abortController.signal;
    196    for (const event of relayedEvents) {
    197      this._targetActor.on(event, this.relayTargetEvent.bind(this, event), {
    198        signal,
    199      });
    200    }
    201  }
    202 
    203  initFromWindow(win) {
    204    this._win = win;
    205 
    206    // We need a progress listener to know when the window will navigate/has
    207    // navigated.
    208    const self = this;
    209    this.listener = {
    210      QueryInterface: ChromeUtils.generateQI([
    211        "nsIWebProgressListener",
    212        "nsISupportsWeakReference",
    213      ]),
    214 
    215      onStateChange(progress, request, flag) {
    216        const isStart = flag & Ci.nsIWebProgressListener.STATE_START;
    217        const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP;
    218        const isWindow = flag & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
    219        const isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
    220 
    221        if (progress.DOMWindow !== win) {
    222          return;
    223        }
    224 
    225        if (isDocument && isStart) {
    226          // One of the earliest events that tells us a new URI is being loaded
    227          // in this window.
    228          self.emit("will-navigate", {
    229            window: win,
    230            isTopLevel: true,
    231          });
    232        }
    233        if (isWindow && isStop) {
    234          self.emit("navigate", {
    235            window: win,
    236            isTopLevel: true,
    237          });
    238        }
    239      },
    240    };
    241 
    242    this.webProgress.addProgressListener(
    243      this.listener,
    244      Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
    245        Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
    246    );
    247  }
    248 
    249  get isInitialized() {
    250    return this._win || this._targetActor;
    251  }
    252 
    253  get isXUL() {
    254    return isXUL(this.window);
    255  }
    256 
    257  get useSimpleHighlightersForReducedMotion() {
    258    return this._targetActor?._useSimpleHighlightersForReducedMotion;
    259  }
    260 
    261  get window() {
    262    if (!this.isInitialized) {
    263      throw new Error(
    264        "Initialize HighlighterEnvironment with a targetActor " +
    265          "or window first"
    266      );
    267    }
    268    const win = this._targetActor ? this._targetActor.window : this._win;
    269 
    270    try {
    271      return Cu.isDeadWrapper(win) ? null : win;
    272    } catch (e) {
    273      // win is null
    274      return null;
    275    }
    276  }
    277 
    278  get document() {
    279    return this.window && this.window.document;
    280  }
    281 
    282  get docShell() {
    283    return this.window && this.window.docShell;
    284  }
    285 
    286  get webProgress() {
    287    return (
    288      this.docShell &&
    289      this.docShell
    290        .QueryInterface(Ci.nsIInterfaceRequestor)
    291        .getInterface(Ci.nsIWebProgress)
    292    );
    293  }
    294 
    295  /**
    296   * Get the right target for listening to events on the page.
    297   * - If the environment was initialized from a WindowGlobalTargetActor
    298   *   *and* if we're in the Browser Toolbox (to inspect Firefox Desktop): the
    299   *   targetActor is the RootActor, in which case, the window property can be
    300   *   used to listen to events.
    301   * - With Firefox Desktop, the targetActor is a WindowGlobalTargetActor, and we use
    302   *   the chromeEventHandler which gives us a target we can use to listen to
    303   *   events, even from nested iframes.
    304   * - If the environment was initialized from a window, we also use the
    305   *   chromeEventHandler.
    306   */
    307  get pageListenerTarget() {
    308    if (this._targetActor && this._targetActor.isRootActor) {
    309      return this.window;
    310    }
    311    return (
    312      this._targetActor?.chromeEventHandler || this.docShell.chromeEventHandler
    313    );
    314  }
    315 
    316  relayTargetEvent(name, data) {
    317    this.emit(name, data);
    318  }
    319 
    320  destroy() {
    321    if (this._abortController) {
    322      this._abortController.abort();
    323      this._abortController = null;
    324    }
    325 
    326    // In case the environment was initialized from a window, we need to remove
    327    // the progress listener.
    328    if (this._win) {
    329      try {
    330        this.webProgress.removeProgressListener(this.listener);
    331      } catch (e) {
    332        // Which may fail in case the window was already destroyed.
    333      }
    334    }
    335 
    336    this._targetActor = null;
    337    this._win = null;
    338  }
    339 }
    340 exports.HighlighterEnvironment = HighlighterEnvironment;
    341 
    342 // This constant object is created to make the calls array more
    343 // readable. Otherwise, linting rules force some array defs to span 4
    344 // lines instead, which is much harder to parse.
    345 const HIGHLIGHTERS = {
    346  [TYPES.ACCESSIBLE]: "devtools/server/actors/highlighters/accessible",
    347  [TYPES.BOXMODEL]: "devtools/server/actors/highlighters/box-model",
    348  [TYPES.GRID]: "devtools/server/actors/highlighters/css-grid",
    349  [TYPES.TRANSFORM]: "devtools/server/actors/highlighters/css-transform",
    350  [TYPES.EYEDROPPER]: "devtools/server/actors/highlighters/eye-dropper",
    351  [TYPES.FLEXBOX]: "devtools/server/actors/highlighters/flexbox",
    352  [TYPES.FONTS]: "devtools/server/actors/highlighters/fonts",
    353  [TYPES.GEOMETRY]: "devtools/server/actors/highlighters/geometry-editor",
    354  [TYPES.MEASURING]: "devtools/server/actors/highlighters/measuring-tool",
    355  [TYPES.PAUSED_DEBUGGER]:
    356    "devtools/server/actors/highlighters/paused-debugger",
    357  [TYPES.RULERS]: "devtools/server/actors/highlighters/rulers",
    358  [TYPES.SELECTOR]: "devtools/server/actors/highlighters/selector",
    359  [TYPES.SHAPES]: "devtools/server/actors/highlighters/shapes",
    360  [TYPES.TABBING_ORDER]: "devtools/server/actors/highlighters/tabbing-order",
    361  [TYPES.VIEWPORT_SIZE]: "devtools/server/actors/highlighters/viewport-size",
    362  [TYPES.VIEWPORT_SIZE_ON_RESIZE]:
    363    "devtools/server/actors/highlighters/viewport-size-on-resize",
    364 };
    365 for (const [typeName, modulePath] of Object.entries(HIGHLIGHTERS)) {
    366  registerHighlighter(typeName, modulePath);
    367 }