tor-browser

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

sources-manager.js (13164B)


      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 DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
      8 const { assert, fetch } = DevToolsUtils;
      9 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
     10 const {
     11  SourceLocation,
     12 } = require("resource://devtools/server/actors/common.js");
     13 
     14 loader.lazyRequireGetter(
     15  this,
     16  "SourceActor",
     17  "resource://devtools/server/actors/source.js",
     18  true
     19 );
     20 
     21 const lazy = {};
     22 ChromeUtils.defineESModuleGetters(
     23  lazy,
     24  {
     25    HTMLSourcesCache:
     26      "resource://devtools/server/actors/utils/HTMLSourcesCache.sys.mjs",
     27  },
     28  { global: "contextual" }
     29 );
     30 
     31 /**
     32 * Matches strings of the form "foo.min.js" or "foo-min.js", etc. If the regular
     33 * expression matches, we can be fairly sure that the source is minified, and
     34 * treat it as such.
     35 */
     36 const MINIFIED_SOURCE_REGEXP = /\bmin\.js$/;
     37 
     38 /**
     39 * Manages the sources for a thread. Handles URL contents, locations in
     40 * the sources, etc for ThreadActors.
     41 */
     42 class SourcesManager extends EventEmitter {
     43  constructor(threadActor) {
     44    super();
     45    this._thread = threadActor;
     46 
     47    this.blackBoxedSources = new Map();
     48 
     49    // Debugger.Source -> SourceActor
     50    this._sourceActors = new Map();
     51 
     52    // Debugger.Source.id -> Debugger.Source
     53    //
     54    // The IDs associated with ScriptSources and available via DebuggerSource.id
     55    // are internal to this process and should not be exposed to the client. This
     56    // map associates these IDs with the corresponding source, provided the source
     57    // has not been GC'ed and the actor has been created. This is lazily populated
     58    // the first time it is needed.
     59    this._sourcesByInternalSourceId = null;
     60  }
     61 
     62  destroy() {}
     63 
     64  /**
     65   * Clear existing sources so they are recreated on the next access.
     66   */
     67  reset() {
     68    this._sourceActors = new Map();
     69    this._sourcesByInternalSourceId = null;
     70  }
     71 
     72  /**
     73   * Create a source actor representing this source.
     74   *
     75   * @param Debugger.Source source
     76   *        The source to make an actor for.
     77   * @returns a SourceActor representing the source.
     78   */
     79  createSourceActor(source) {
     80    assert(source, "SourcesManager.prototype.source needs a source");
     81 
     82    if (this._sourceActors.has(source)) {
     83      return this._sourceActors.get(source);
     84    }
     85 
     86    const actor = new SourceActor({
     87      thread: this._thread,
     88      source,
     89    });
     90 
     91    this._thread.threadLifetimePool.manage(actor);
     92 
     93    this._sourceActors.set(source, actor);
     94    // source.id can be 0 for WASM sources
     95    if (this._sourcesByInternalSourceId && Number.isInteger(source.id)) {
     96      this._sourcesByInternalSourceId.set(source.id, source);
     97    }
     98 
     99    this.emit("newSource", actor);
    100    return actor;
    101  }
    102 
    103  _getSourceActor(source) {
    104    if (this._sourceActors.has(source)) {
    105      return this._sourceActors.get(source);
    106    }
    107 
    108    return null;
    109  }
    110 
    111  hasSourceActor(source) {
    112    return !!this._getSourceActor(source);
    113  }
    114 
    115  getSourceActor(source) {
    116    const sourceActor = this._getSourceActor(source);
    117 
    118    if (!sourceActor) {
    119      throw new Error(
    120        "getSource: could not find source actor for " + (source.url || "source")
    121      );
    122    }
    123 
    124    return sourceActor;
    125  }
    126 
    127  getOrCreateSourceActor(source) {
    128    // Tolerate the source coming from a different Debugger than the one
    129    // associated with the thread.
    130    try {
    131      source = this._thread.dbg.adoptSource(source);
    132    } catch (e) {
    133      // We can't create actors for sources in the same compartment as the
    134      // thread's Debugger.
    135      if (/is in the same compartment as this debugger/.test(e)) {
    136        return null;
    137      }
    138      throw e;
    139    }
    140 
    141    if (this.hasSourceActor(source)) {
    142      return this.getSourceActor(source);
    143    }
    144    return this.createSourceActor(source);
    145  }
    146 
    147  getSourceActorByInternalSourceId(id) {
    148    if (!this._sourcesByInternalSourceId) {
    149      this._sourcesByInternalSourceId = new Map();
    150      for (const source of this._thread.dbg.findSources()) {
    151        // source.id can be 0 for WASM sources
    152        if (Number.isInteger(source.id)) {
    153          this._sourcesByInternalSourceId.set(source.id, source);
    154        }
    155      }
    156    }
    157    const source = this._sourcesByInternalSourceId.get(id);
    158    if (source) {
    159      return this.getOrCreateSourceActor(source);
    160    }
    161    return null;
    162  }
    163 
    164  getSourceActorsByURL(url) {
    165    const rv = [];
    166    if (url) {
    167      for (const [, actor] of this._sourceActors) {
    168        if (actor.url === url) {
    169          rv.push(actor);
    170        }
    171      }
    172    }
    173    return rv;
    174  }
    175 
    176  getSourceActorById(actorId) {
    177    for (const [, actor] of this._sourceActors) {
    178      if (actor.actorID == actorId) {
    179        return actor;
    180      }
    181    }
    182    return null;
    183  }
    184 
    185  /**
    186   * Returns true if the URL likely points to a minified resource, false
    187   * otherwise.
    188   *
    189   * @param String uri
    190   *        The url to test.
    191   * @returns Boolean
    192   */
    193  _isMinifiedURL(uri) {
    194    if (!uri) {
    195      return false;
    196    }
    197 
    198    const url = URL.parse(uri);
    199    if (url) {
    200      const pathname = url.pathname;
    201      return MINIFIED_SOURCE_REGEXP.test(
    202        pathname.slice(pathname.lastIndexOf("/") + 1)
    203      );
    204    }
    205    // Not a valid URL so don't try to parse out the filename, just test the
    206    // whole thing with the minified source regexp.
    207    return MINIFIED_SOURCE_REGEXP.test(uri);
    208  }
    209 
    210  /**
    211   * Return the non-source-mapped location of an offset in a script.
    212   *
    213   * @param Debugger.Script script
    214   *        The script associated with the offset.
    215   * @param Number offset
    216   *        Offset within the script of the location.
    217   * @returns Object
    218   *          Returns an object of the form { source, line, column }
    219   */
    220  getScriptOffsetLocation(script, offset) {
    221    const { lineNumber, columnNumber } = script.getOffsetMetadata(offset);
    222    // NOTE: Debugger.Source.prototype.startColumn is 1-based.
    223    //       Convert to 0-based, while keeping the wasm's column (1) as is.
    224    //       (bug 1863878)
    225    const columnBase = script.format === "wasm" ? 0 : 1;
    226    return new SourceLocation(
    227      this.createSourceActor(script.source),
    228      lineNumber,
    229      columnNumber - columnBase
    230    );
    231  }
    232 
    233  /**
    234   * Return the non-source-mapped location of the given Debugger.Frame. If the
    235   * frame does not have a script, the location's properties are all null.
    236   *
    237   * @param Debugger.Frame frame
    238   *        The frame whose location we are getting.
    239   * @returns Object
    240   *          Returns an object of the form { source, line, column }
    241   */
    242  getFrameLocation(frame) {
    243    if (!frame || !frame.script) {
    244      return new SourceLocation();
    245    }
    246    return this.getScriptOffsetLocation(frame.script, frame.offset);
    247  }
    248 
    249  /**
    250   * Returns true if URL for the given source is black boxed.
    251   *
    252   *   * @param url String
    253   *        The URL of the source which we are checking whether it is black
    254   *        boxed or not.
    255   */
    256  isBlackBoxed(url, line, column) {
    257    if (this.blackBoxedSources.size == 0) {
    258      return false;
    259    }
    260    if (!this.blackBoxedSources.has(url)) {
    261      return false;
    262    }
    263 
    264    const ranges = this.blackBoxedSources.get(url);
    265 
    266    // If we have an entry in the map, but it is falsy, the source is fully blackboxed.
    267    if (!ranges) {
    268      return true;
    269    }
    270 
    271    const range = ranges.find(r => isLocationInRange({ line, column }, r));
    272    return !!range;
    273  }
    274 
    275  isFrameBlackBoxed(frame) {
    276    if (this.blackBoxedSources.size == 0) {
    277      return false;
    278    }
    279    const { url, line, column } = this.getFrameLocation(frame);
    280    return this.isBlackBoxed(url, line, column);
    281  }
    282 
    283  clearAllBlackBoxing() {
    284    this.blackBoxedSources.clear();
    285  }
    286 
    287  /**
    288   * Add the given source URL to the set of sources that are black boxed.
    289   *
    290   * @param url String
    291   *        The URL of the source which we are black boxing.
    292   */
    293  blackBox(url, range) {
    294    if (!range) {
    295      // blackbox the whole source
    296      return this.blackBoxedSources.set(url, null);
    297    }
    298 
    299    const ranges = this.blackBoxedSources.get(url) || [];
    300    // ranges are sorted in ascening order
    301    const index = ranges.findIndex(
    302      r => r.end.line <= range.start.line && r.end.column <= range.start.column
    303    );
    304 
    305    ranges.splice(index + 1, 0, range);
    306    this.blackBoxedSources.set(url, ranges);
    307    return true;
    308  }
    309 
    310  /**
    311   * Remove the given source URL to the set of sources that are black boxed.
    312   *
    313   * @param url String
    314   *        The URL of the source which we are no longer black boxing.
    315   */
    316  unblackBox(url, range) {
    317    if (!range) {
    318      return this.blackBoxedSources.delete(url);
    319    }
    320 
    321    const ranges = this.blackBoxedSources.get(url);
    322    const index = ranges.findIndex(
    323      r =>
    324        r.start.line === range.start.line &&
    325        r.start.column === range.start.column &&
    326        r.end.line === range.end.line &&
    327        r.end.column === range.end.column
    328    );
    329 
    330    if (index !== -1) {
    331      ranges.splice(index, 1);
    332    }
    333 
    334    if (ranges.length === 0) {
    335      return this.blackBoxedSources.delete(url);
    336    }
    337 
    338    return this.blackBoxedSources.set(url, ranges);
    339  }
    340 
    341  /**
    342   * List all currently registered source actors.
    343   *
    344   * @return Iterator<SourceActor>
    345   */
    346  iter() {
    347    return this._sourceActors.values();
    348  }
    349 
    350  /**
    351   * Get the contents of a URL, fetching it if necessary. If partial is set and
    352   * any content for the URL has been received, that partial content is returned
    353   * synchronously.
    354   */
    355  urlContents(url, partial, canUseCache) {
    356    const { browsingContextID } = this._thread.targetActor;
    357    const content = !isWorker
    358      ? lazy.HTMLSourcesCache.get(browsingContextID, url, partial)
    359      : null;
    360    if (content) {
    361      return content;
    362    }
    363    return this._fetchURLContents(url, partial, canUseCache);
    364  }
    365 
    366  async _fetchURLContents(url, partial, canUseCache) {
    367    // Only try the cache if it is currently enabled for the document.
    368    // Without this check, the cache may return stale data that doesn't match
    369    // the document shown in the browser.
    370    let loadFromCache = canUseCache;
    371    if (canUseCache && this._thread.targetActor.browsingContext) {
    372      loadFromCache = !(
    373        this._thread.targetActor.browsingContext.defaultLoadFlags ===
    374        Ci.nsIRequest.LOAD_BYPASS_CACHE
    375      );
    376    }
    377 
    378    // Fetch the sources with the same principal as the original document
    379    const win = this._thread.targetActor.window;
    380    let principal, cacheKey;
    381    // On xpcshell, we don't have a window but a Sandbox
    382    if (!isWorker && win instanceof Ci.nsIDOMWindow && win.docShell) {
    383      const docShell = win.docShell;
    384      const channel = docShell.currentDocumentChannel;
    385      principal = channel.loadInfo.loadingPrincipal;
    386 
    387      // Retrieve the cacheKey in order to load POST requests from cache
    388      // Note that chrome:// URLs don't support this interface.
    389      if (
    390        loadFromCache &&
    391        docShell.currentDocumentChannel instanceof Ci.nsICacheInfoChannel
    392      ) {
    393        cacheKey = docShell.currentDocumentChannel.cacheKey;
    394      }
    395    }
    396 
    397    let result;
    398    try {
    399      result = await fetch(url, {
    400        principal,
    401        cacheKey,
    402        loadFromCache,
    403      });
    404    } catch (error) {
    405      this._reportLoadSourceError(error);
    406      throw error;
    407    }
    408 
    409    // When we fetch the contents, there is a risk that the contents we get
    410    // do not match up with the actual text of the sources these contents will
    411    // be associated with. We want to always show contents that include that
    412    // actual text (otherwise it will be very confusing or unusable for users),
    413    // so replace the contents with the actual text if there is a mismatch.
    414    const actors = [...this._sourceActors.values()].filter(
    415      // Bug 1907977: some source may not have a valid source text content exposed by spidermonkey
    416      // and have their text be "[no source]", so avoid falling back to them and consider
    417      // the request fallback.
    418      actor => actor.url == url && actor.actualText() != "[no source]"
    419    );
    420    if (!actors.every(actor => actor.contentMatches(result))) {
    421      if (actors.length > 1) {
    422        // When there are multiple actors we won't be able to show the source
    423        // for all of them. Ask the user to reload so that we don't have to do
    424        // any fetching.
    425        result.content = "Error: Incorrect contents fetched, please reload.";
    426      } else {
    427        result.content = actors[0].actualText();
    428      }
    429    }
    430 
    431    return result;
    432  }
    433 
    434  _reportLoadSourceError(error) {
    435    try {
    436      DevToolsUtils.reportException("SourceActor", error);
    437 
    438      const lines = JSON.stringify(this.form(), null, 4).split(/\n/g);
    439      lines.forEach(line => console.error("\t", line));
    440    } catch (e) {
    441      // ignore
    442    }
    443  }
    444 }
    445 
    446 function isLocationInRange({ line, column }, range) {
    447  return (
    448    (range.start.line <= line ||
    449      (range.start.line == line && range.start.column <= column)) &&
    450    (range.end.line >= line ||
    451      (range.end.line == line && range.end.column >= column))
    452  );
    453 }
    454 
    455 exports.SourcesManager = SourcesManager;