/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { Ci, Cu } = require("chrome"); const DevToolsUtils = require("devtools/shared/DevToolsUtils"); const { assert, fetch } = DevToolsUtils; const EventEmitter = require("devtools/shared/event-emitter"); const { OriginalLocation, GeneratedLocation } = require("devtools/server/actors/common"); const { resolve } = require("promise"); const { joinURI } = require("devtools/shared/path"); loader.lazyRequireGetter(this, "SourceActor", "devtools/server/actors/source", true); loader.lazyRequireGetter(this, "isEvalSource", "devtools/server/actors/source", true); loader.lazyRequireGetter(this, "SourceMapConsumer", "source-map", true); loader.lazyRequireGetter(this, "SourceMapGenerator", "source-map", true); /** * Manages the sources for a thread. Handles source maps, locations in the * sources, etc for ThreadActors. */ function TabSources(threadActor, allowSourceFn = () => true) { EventEmitter.decorate(this); this._thread = threadActor; this._useSourceMaps = true; this._autoBlackBox = true; this._anonSourceMapId = 1; this.allowSource = source => { return !isHiddenSource(source) && allowSourceFn(source); }; this.blackBoxedSources = new Set(); this.prettyPrintedSources = new Map(); this.neverAutoBlackBoxSources = new Set(); // generated Debugger.Source -> promise of SourceMapConsumer this._sourceMaps = new Map(); // sourceMapURL -> promise of SourceMapConsumer this._sourceMapCache = Object.create(null); // Debugger.Source -> SourceActor this._sourceActors = new Map(); // url -> SourceActor this._sourceMappedSourceActors = Object.create(null); } /** * Matches strings of the form "foo.min.js" or "foo-min.js", etc. If the regular * expression matches, we can be fairly sure that the source is minified, and * treat it as such. */ const MINIFIED_SOURCE_REGEXP = /\bmin\.js$/; TabSources.prototype = { /** * Update preferences and clear out existing sources */ setOptions: function (options) { let shouldReset = false; if ("useSourceMaps" in options) { shouldReset = true; this._useSourceMaps = options.useSourceMaps; } if ("autoBlackBox" in options) { shouldReset = true; this._autoBlackBox = options.autoBlackBox; } if (shouldReset) { this.reset(); } }, /** * Clear existing sources so they are recreated on the next access. * * @param Object opts * Specify { sourceMaps: true } if you also want to clear * the source map cache (usually done on reload). */ reset: function (opts = {}) { this._sourceActors = new Map(); this._sourceMaps = new Map(); this._sourceMappedSourceActors = Object.create(null); if (opts.sourceMaps) { this._sourceMapCache = Object.create(null); } }, /** * Return the source actor representing the `source` (or * `originalUrl`), creating one if none exists already. May return * null if the source is disallowed. * * @param Debugger.Source source * The source to make an actor for * @param String originalUrl * The original source URL of a sourcemapped source * @param optional Debguger.Source generatedSource * The generated source that introduced this source via source map, * if any. * @param optional String contentType * The content type of the source, if immediately available. * @returns a SourceActor representing the source or null. */ source: function ({ source, originalUrl, generatedSource, isInlineSource, contentType }) { assert(source || (originalUrl && generatedSource), "TabSources.prototype.source needs an originalUrl or a source"); if (source) { // If a source is passed, we are creating an actor for a real // source, which may or may not be sourcemapped. if (!this.allowSource(source)) { return null; } // It's a hack, but inline HTML scripts each have real sources, // but we want to represent all of them as one source as the // HTML page. The actor representing this fake HTML source is // stored in this array, which always has a URL, so check it // first. if (source.url in this._sourceMappedSourceActors) { return this._sourceMappedSourceActors[source.url]; } if (isInlineSource) { // If it's an inline source, the fake HTML source hasn't been // created yet (would have returned above), so flip this source // into a sourcemapped state by giving it an `originalUrl` which // is the HTML url. originalUrl = source.url; source = null; } else if (this._sourceActors.has(source)) { return this._sourceActors.get(source); } } else if (originalUrl) { // Not all "original" scripts are distinctly separate from the // generated script. Pretty-printed sources have a sourcemap for // themselves, so we need to make sure there a real source // doesn't already exist with this URL. for (let [source, actor] of this._sourceActors) { if (source.url === originalUrl) { return actor; } } if (originalUrl in this._sourceMappedSourceActors) { return this._sourceMappedSourceActors[originalUrl]; } } let actor = new SourceActor({ thread: this._thread, source: source, originalUrl: originalUrl, generatedSource: generatedSource, isInlineSource: isInlineSource, contentType: contentType }); let sourceActorStore = this._thread.sourceActorStore; var id = sourceActorStore.getReusableActorId(source, originalUrl); if (id) { actor.actorID = id; } this._thread.threadLifetimePool.addActor(actor); sourceActorStore.setReusableActorId(source, originalUrl, actor.actorID); if (this._autoBlackBox && !this.neverAutoBlackBoxSources.has(actor.url) && this._isMinifiedURL(actor.url)) { this.blackBox(actor.url); this.neverAutoBlackBoxSources.add(actor.url); } if (source) { this._sourceActors.set(source, actor); } else { this._sourceMappedSourceActors[originalUrl] = actor; } this._emitNewSource(actor); return actor; }, _emitNewSource: function (actor) { if (!actor.source) { // Always notify if we don't have a source because that means // it's something that has been sourcemapped, or it represents // the HTML file that contains inline sources. this.emit("newSource", actor); } else { // If sourcemapping is enabled and a source has sourcemaps, we // create `SourceActor` instances for both the original and // generated sources. The source actors for the generated // sources are only for internal use, however; breakpoints are // managed by these internal actors. We only want to notify the // user of the original sources though, so if the actor has a // `Debugger.Source` instance and a valid source map (meaning // it's a generated source), don't send the notification. this.fetchSourceMap(actor.source).then(map => { if (!map) { this.emit("newSource", actor); } }); } }, getSourceActor: function (source) { if (source.url in this._sourceMappedSourceActors) { return this._sourceMappedSourceActors[source.url]; } if (this._sourceActors.has(source)) { return this._sourceActors.get(source); } throw new Error("getSource: could not find source actor for " + (source.url || "source")); }, getSourceActorByURL: function (url) { if (url) { for (let [source, actor] of this._sourceActors) { if (source.url === url) { return actor; } } if (url in this._sourceMappedSourceActors) { return this._sourceMappedSourceActors[url]; } } throw new Error("getSourceActorByURL: could not find source for " + url); return null; }, /** * Returns true if the URL likely points to a minified resource, false * otherwise. * * @param String aURL * The URL to test. * @returns Boolean */ _isMinifiedURL: function (aURL) { if (!aURL) { return false; } try { let url = new URL(aURL); let pathname = url.pathname; return MINIFIED_SOURCE_REGEXP.test(pathname.slice(pathname.lastIndexOf("/") + 1)); } catch (e) { // Not a valid URL so don't try to parse out the filename, just test the // whole thing with the minified source regexp. return MINIFIED_SOURCE_REGEXP.test(aURL); } }, /** * Create a source actor representing this source. This ignores * source mapping and always returns an actor representing this real * source. Use `createSourceActors` if you want to respect source maps. * * @param Debugger.Source aSource * The source instance to create an actor for. * @returns SourceActor */ createNonSourceMappedActor: function (aSource) { // Don't use getSourceURL because we don't want to consider the // displayURL property if it's an eval source. We only want to // consider real URLs, otherwise if there is a URL but it's // invalid the code below will not set the content type, and we // will later try to fetch the contents of the URL to figure out // the content type, but it's a made up URL for eval sources. let url = isEvalSource(aSource) ? null : aSource.url; let spec = { source: aSource }; // XXX bug 915433: We can't rely on Debugger.Source.prototype.text // if the source is an HTML-embedded