/* 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 { Cu } = require("chrome"); const { setBreakpointAtEntryPoints, } = require("devtools/server/actors/breakpoint"); const { ActorClassWithSpec, Actor } = require("devtools/shared/protocol"); const DevToolsUtils = require("devtools/shared/DevToolsUtils"); const { assert } = DevToolsUtils; const { sourceSpec } = require("devtools/shared/specs/source"); const { resolveSourceURL, getSourcemapBaseURL, } = require("devtools/server/actors/utils/source-map-utils"); const { getDebuggerSourceURL, } = require("devtools/server/actors/utils/source-url"); loader.lazyRequireGetter( this, "ArrayBufferActor", "devtools/server/actors/array-buffer", true ); loader.lazyRequireGetter( this, "LongStringActor", "devtools/server/actors/string", true ); loader.lazyRequireGetter(this, "Services"); loader.lazyGetter( this, "WebExtensionPolicy", () => Cu.getGlobalForObject(Cu).WebExtensionPolicy ); const windowsDrive = /^([a-zA-Z]:)/; function getSourceURL(source, window) { // Some eval sources have URLs, but we want to explcitly ignore those because // they are generally useless strings like "eval" or "debugger eval code". const resourceURL = (getDebuggerSourceURL(source) || "").split(" -> ").pop() || null; // A "//# sourceURL=" pragma should basically be treated as a source file's // full URL, so that is what we want to use as the base if it is present. // If this is not an absolute URL, this will mean the maps in the file // will not have a valid base URL, but that is up to tooling that let result = resolveSourceURL(source.displayURL, window); if (!result) { result = resolveSourceURL(resourceURL, window) || resourceURL; // In XPCShell tests, the source URL isn't actually a URL, it's a file path. // That causes issues because "C:/folder/file.js" is parsed as a URL with // "c:" as the URL scheme, which causes the drive letter to be unexpectedly // lower-cased when the parsed URL is re-serialized. To avoid that, we // detect that case and re-uppercase it again. This is a bit gross and // ideally it seems like XPCShell tests should use file:// URLs for files, // but alas they do not. if ( resourceURL && resourceURL.match(windowsDrive) && result.slice(0, 2) == resourceURL.slice(0, 2).toLowerCase() ) { result = resourceURL.slice(0, 2) + result.slice(2); } } return result; } /** * A SourceActor provides information about the source of a script. Source * actors are 1:1 with Debugger.Source objects. * * @param Debugger.Source source * The source object we are representing. * @param ThreadActor thread * The current thread actor. */ const SourceActor = ActorClassWithSpec(sourceSpec, { initialize: function({ source, thread }) { Actor.prototype.initialize.call(this, thread.conn); this._threadActor = thread; this._url = undefined; this._source = source; this.__isInlineSource = undefined; this._startLineColumnDisplacement = null; }, get _isInlineSource() { const source = this._source; if (this.__isInlineSource === undefined) { // If the source has a usable displayURL, the source is treated as not // inlined because it has its own URL. this.__isInlineSource = source.introductionType === "inlineScript" && !resolveSourceURL(source.displayURL, this.threadActor._parent.window); } return this.__isInlineSource; }, get threadActor() { return this._threadActor; }, get sourcesManager() { return this._threadActor.sourcesManager; }, get dbg() { return this.threadActor.dbg; }, get breakpointActorMap() { return this.threadActor.breakpointActorMap; }, get url() { if (this._url === undefined) { this._url = getSourceURL(this._source, this.threadActor._parent.window); } return this._url; }, get extensionName() { if (this._extensionName === undefined) { this._extensionName = null; // Cu is not available for workers and so we are not able to get a // WebExtensionPolicy object if (!isWorker && this.url) { try { const extURI = Services.io.newURI(this.url); if (extURI) { const policy = WebExtensionPolicy.getByURI(extURI); if (policy) { this._extensionName = policy.name; } } } catch (e) { // Ignore } } } return this._extensionName; }, get internalSourceId() { return this._source.id; }, form: function() { const source = this._source; let introductionType = source.introductionType; if ( introductionType === "srcScript" || introductionType === "inlineScript" || introductionType === "injectedScript" ) { // These three used to be one single type, so here we combine them all // so that clients don't see any change in behavior. introductionType = "scriptElement"; } return { actor: this.actorID, extensionName: this.extensionName, url: this.url, isBlackBoxed: this.sourcesManager.isBlackBoxed(this.url), sourceMapBaseURL: getSourcemapBaseURL( this.url, this.threadActor._parent.window ), sourceMapURL: source.sourceMapURL, introductionType, }; }, destroy: function() { const parent = this.getParent(); if (parent && parent.sourceActors) { delete parent.sourceActors[this.actorID]; } Actor.prototype.destroy.call(this); }, get _isWasm() { return this._source.introductionType === "wasm"; }, _getSourceText: async function() { if (this._isWasm) { const wasm = this._source.binary; const buffer = wasm.buffer; assert( wasm.byteOffset === 0 && wasm.byteLength === buffer.byteLength, "Typed array from wasm source binary must cover entire buffer" ); return { content: buffer, contentType: "text/wasm", }; } // Use `source.text` if it exists, is not the "no source" string, and // the source isn't one that is inlined into some larger file. // It will be "no source" if the Debugger API wasn't able to load // the source because sources were discarded // (javascript.options.discardSystemSource == true). if (this._source.text !== "[no source]" && !this._isInlineSource) { return { content: this.actualText(), contentType: "text/javascript", }; } return this.sourcesManager.urlContents( this.url, /* partial */ false, /* canUseCache */ this._isInlineSource ); }, // Get the actual text of this source, padded so that line numbers will match // up with the source itself. actualText() { // If the source doesn't start at line 1, line numbers in the client will // not match up with those in the source. Pad the text with blank lines to // fix this. This can show up for sources associated with inline scripts // in HTML created via document.write() calls: the script's source line // number is relative to the start of the written HTML, but we show the // source's content by itself. const padding = this._source.startLine ? "\n".repeat(this._source.startLine - 1) : ""; return padding + this._source.text; }, // Return whether the specified fetched contents includes the actual text of // this source in the expected position. contentMatches(fileContents) { const lineBreak = /\r\n?|\n|\u2028|\u2029/; const contentLines = fileContents.content.split(lineBreak); const sourceLines = this._source.text.split(lineBreak); let line = this._source.startLine - 1; for (const sourceLine of sourceLines) { const contentLine = contentLines[line++] || ""; if (!contentLine.includes(sourceLine)) { return false; } } return true; }, getBreakableLines: async function() { const positions = await this.getBreakpointPositions(); const lines = new Set(); for (const position of positions) { if (!lines.has(position.line)) { lines.add(position.line); } } return Array.from(lines); }, // For inline