tor-browser

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

HTMLSourcesCache.sys.mjs (6430B)


      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 /**
      6 * Helper class spawn independently from target to watcher for HTML source content
      7 * being emitted by the C++ HTML Parser.
      8 *
      9 * As HTML sources are parsed before the HTML document's WindowGlobal is created,
     10 * they will be parsed before the target gets created.
     11 * This class is instantiated as soon as we start watching for WindowGlobal's
     12 * and will hold HTML sources and deliver them on-demand to SourcesManager.
     13 *
     14 * This cache clears itself automatically when the WindowGlobals are destroyed.
     15 */
     16 class HtmlCache {
     17  // Map<string ->
     18  //   Map<string -> Object{
     19  //     content: string
     20  //     complete: boolean,
     21  //     parserID: string,
     22  //   }>
     23  // >
     24  // Nested Maps of source objects keyed by source url, stored into
     25  // this map keyed by Browsing Context ID.
     26  #sourcesByBrowsingContext = new Map();
     27 
     28  // Map<string: number>
     29  // Number of active listeners (WindowGlobal target watchers) for all currently observed browser element IDs.
     30  #noOfActiveTargetWatchers = new Map();
     31 
     32  // Map<number -> number>
     33  // Map the browsing context ID for each currently active Window global InnerWindowID
     34  //
     35  // We have to keep track of the relationship between innerWindowID and browsingContextID
     36  // as inner-window-destroyed only emits the innerWindowID number, from which
     37  // we can't derivate the browsingContextID required to clear `#sourcesByBrowsingContext` Map.
     38  #browsingContextIDByInnerWindowId = new Map();
     39 
     40  constructor() {
     41    Services.obs.addObserver(this, "devtools-html-content");
     42    Services.obs.addObserver(this, "document-element-inserted");
     43    Services.obs.addObserver(this, "inner-window-destroyed");
     44  }
     45 
     46  destroy() {
     47    Services.obs.removeObserver(this, "devtools-html-content");
     48    Services.obs.removeObserver(this, "document-element-inserted");
     49    Services.obs.removeObserver(this, "inner-window-destroyed");
     50  }
     51 
     52  watch(browserId) {
     53    const noOfTargetWatchers =
     54      this.#noOfActiveTargetWatchers.get(browserId) || 0;
     55    this.#noOfActiveTargetWatchers.set(browserId, noOfTargetWatchers + 1);
     56  }
     57 
     58  unwatch(browserId) {
     59    const noOfTargetWatchers = this.#noOfActiveTargetWatchers.get(browserId);
     60    if (noOfTargetWatchers > 1) {
     61      this.#noOfActiveTargetWatchers.set(browserId, noOfTargetWatchers - 1);
     62    } else {
     63      this.#noOfActiveTargetWatchers.delete(browserId);
     64    }
     65  }
     66 
     67  get(browsingContextID, url, partial) {
     68    const sourcesByUrl = this.#sourcesByBrowsingContext.get(browsingContextID);
     69    if (sourcesByUrl) {
     70      const source = sourcesByUrl.get(url);
     71      if (source) {
     72        if (!partial && !source.complete) {
     73          return source.onComplete.then(() => {
     74            return {
     75              content: source.content,
     76              contentType: "text/html",
     77            };
     78          });
     79        }
     80        return {
     81          content: source.content,
     82          contentType: "text/html",
     83        };
     84      }
     85    }
     86    if (partial) {
     87      return {
     88        content: "",
     89        contentType: "",
     90      };
     91    }
     92    return null;
     93  }
     94 
     95  /**
     96   * Listener for new HTML content.
     97   */
     98  observe(subject, topic, data) {
     99    if (topic === "inner-window-destroyed") {
    100      const innerWindowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
    101      const browsingContextID =
    102        this.#browsingContextIDByInnerWindowId.get(innerWindowId);
    103      if (browsingContextID) {
    104        this.#sourcesByBrowsingContext.delete(browsingContextID);
    105        this.#browsingContextIDByInnerWindowId.delete(innerWindowId);
    106      }
    107    } else if (topic === "document-element-inserted") {
    108      const window = subject.defaultView;
    109      // Ignore any non-HTML document
    110      if (!window) {
    111        return;
    112      }
    113      const innerWindowId = window.windowGlobalChild.innerWindowId;
    114      const browsingContextID = window.browsingContext.id;
    115      this.#browsingContextIDByInnerWindowId.set(
    116        innerWindowId,
    117        browsingContextID
    118      );
    119    } else if (topic === "devtools-html-content") {
    120      const {
    121        parserID,
    122        browserId,
    123        browsingContextID,
    124        uri,
    125        contents,
    126        complete,
    127      } = JSON.parse(data);
    128 
    129      // Only save data if the related browser element is being observed
    130      if (!this.#noOfActiveTargetWatchers.has(browserId)) {
    131        return;
    132      }
    133 
    134      let sourcesByUrl = this.#sourcesByBrowsingContext.get(browsingContextID);
    135      if (!sourcesByUrl) {
    136        sourcesByUrl = new Map();
    137        this.#sourcesByBrowsingContext.set(browsingContextID, sourcesByUrl);
    138      }
    139      const source = sourcesByUrl.get(uri);
    140      if (source) {
    141        // We received many devtools-html-content events, if we already received one,
    142        // aggregate the data with the one we already received.
    143        if (source.parserID == parserID) {
    144          source.content += contents;
    145          source.complete = complete;
    146 
    147          // After the HTML has finished loading, resolve any promises
    148          // waiting for the complete file contents. Waits will only
    149          // occur when the URL was ever partially loaded.
    150          if (complete) {
    151            source.resolveComplete();
    152          }
    153        }
    154      } else if (contents) {
    155        const { promise, resolve } = Promise.withResolvers();
    156        // Ensure that `contents` is non-empty. We may miss all the devtools-html-content events except the last
    157        // one which has an empty `contents` and `complete` set to true.
    158        // This reproduces when opening a same-process iframe. In this particular scenario, we instantiate the target and thread actor
    159        // on `DOMDocElementInserted` and the HTML document is already parsed, but we still receive this one very last notification.
    160        sourcesByUrl.set(uri, {
    161          // String: HTML source text content
    162          content: contents,
    163          // Boolean: is the source complete
    164          complete,
    165          // String: uuid generated by the html parser to uniquely identify a document
    166          parserID,
    167          // Promise: resolved when the source is complete
    168          onComplete: promise,
    169          // Function: to be called when the source is complete
    170          resolveComplete: resolve,
    171        });
    172      }
    173    }
    174  }
    175 }
    176 
    177 export const HTMLSourcesCache = new HtmlCache();