/* 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 Services = require("Services"); // const DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); // const { dbg_assert, fetch } = DevToolsUtils; // const EventEmitter = require("devtools/toolkit/event-emitter"); // const { OriginalLocation, GeneratedLocation, getOffsetColumn } = require("devtools/server/actors/common"); // const { resolve } = require("promise"); // loader.lazyRequireGetter(this, "SourceActor", "devtools/server/actors/script", true); // loader.lazyRequireGetter(this, "isEvalSource", "devtools/server/actors/script", 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(); // 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 */ reconfigure: function(options) { if ('useSourceMaps' in options) { this._useSourceMaps = options.useSourceMaps; } if ('autoBlackBox' in options) { this._autoBlackBox = options.autoBlackBox; } 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 }) { dbg_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._isMinifiedURL(actor.url)) { this.blackBox(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('getSourceByURL: could not find source for ' + url); }, /** * 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) { try { let url = Services.io.newURI(aURL, null, null) .QueryInterface(Ci.nsIURL); return MINIFIED_SOURCE_REGEXP.test(url.fileName); } 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