summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/utils
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/utils')
-rw-r--r--devtools/server/actors/utils/TabSources.js833
-rw-r--r--devtools/server/actors/utils/actor-registry-utils.js78
-rw-r--r--devtools/server/actors/utils/audionodes.json113
-rw-r--r--devtools/server/actors/utils/automation-timeline.js373
-rw-r--r--devtools/server/actors/utils/css-grid-utils.js61
-rw-r--r--devtools/server/actors/utils/make-debugger.js101
-rw-r--r--devtools/server/actors/utils/map-uri-to-addon-id.js44
-rw-r--r--devtools/server/actors/utils/moz.build19
-rw-r--r--devtools/server/actors/utils/stack.js185
-rw-r--r--devtools/server/actors/utils/walker-search.js278
-rw-r--r--devtools/server/actors/utils/webconsole-utils.js1063
-rw-r--r--devtools/server/actors/utils/webconsole-worker-utils.js20
12 files changed, 3168 insertions, 0 deletions
diff --git a/devtools/server/actors/utils/TabSources.js b/devtools/server/actors/utils/TabSources.js
new file mode 100644
index 000000000..56e862939
--- /dev/null
+++ b/devtools/server/actors/utils/TabSources.js
@@ -0,0 +1,833 @@
+/* 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 <script> tag. Since we don't
+ // have an API implemented to detect whether this is the case, we
+ // need to be conservative and only treat valid js files as real
+ // sources. Otherwise, use the `originalUrl` property to treat it
+ // as an HTML source that manages multiple inline sources.
+
+ // Assume the source is inline if the element that introduced it is not a
+ // script element, or does not have a src attribute.
+ let element = aSource.element ? aSource.element.unsafeDereference() : null;
+ if (element && (element.tagName !== "SCRIPT" || !element.hasAttribute("src"))) {
+ spec.isInlineSource = true;
+ } else if (aSource.introductionType === "wasm") {
+ // Wasm sources are not JavaScript. Give them their own content-type.
+ spec.contentType = "text/wasm";
+ } else {
+ if (url) {
+ // There are a few special URLs that we know are JavaScript:
+ // inline `javascript:` and code coming from the console
+ if (url.indexOf("Scratchpad/") === 0 ||
+ url.indexOf("javascript:") === 0 ||
+ url === "debugger eval code") {
+ spec.contentType = "text/javascript";
+ } else {
+ try {
+ let pathname = new URL(url).pathname;
+ let filename = pathname.slice(pathname.lastIndexOf("/") + 1);
+ let index = filename.lastIndexOf(".");
+ let extension = index >= 0 ? filename.slice(index + 1) : "";
+ if (extension === "xml") {
+ // XUL inline scripts may not correctly have the
+ // `source.element` property, so do a blunt check here if
+ // it's an xml page.
+ spec.isInlineSource = true;
+ }
+ else if (extension === "js") {
+ spec.contentType = "text/javascript";
+ }
+ } catch (e) {
+ // This only needs to be here because URL is not yet exposed to
+ // workers. (BUG 1258892)
+ const filename = url;
+ const index = filename.lastIndexOf(".");
+ const extension = index >= 0 ? filename.slice(index + 1) : "";
+ if (extension === "js") {
+ spec.contentType = "text/javascript";
+ }
+ }
+ }
+ }
+ else {
+ // Assume the content is javascript if there's no URL
+ spec.contentType = "text/javascript";
+ }
+ }
+
+ return this.source(spec);
+ },
+
+ /**
+ * This is an internal function that returns a promise of an array
+ * of source actors representing all the source mapped sources of
+ * `aSource`, or `null` if the source is not sourcemapped or
+ * sourcemapping is disabled. Users should call `createSourceActors`
+ * instead of this.
+ *
+ * @param Debugger.Source aSource
+ * The source instance to create actors for.
+ * @return Promise of an array of source actors
+ */
+ _createSourceMappedActors: function (aSource) {
+ if (!this._useSourceMaps || !aSource.sourceMapURL) {
+ return resolve(null);
+ }
+
+ return this.fetchSourceMap(aSource)
+ .then(map => {
+ if (map) {
+ return map.sources.map(s => {
+ return this.source({ originalUrl: s, generatedSource: aSource });
+ }).filter(isNotNull);
+ }
+ return null;
+ });
+ },
+
+ /**
+ * Creates the source actors representing the appropriate sources
+ * of `aSource`. If sourcemapped, returns actors for all of the original
+ * sources, otherwise returns a 1-element array with the actor for
+ * `aSource`.
+ *
+ * @param Debugger.Source aSource
+ * The source instance to create actors for.
+ * @param Promise of an array of source actors
+ */
+ createSourceActors: function (aSource) {
+ return this._createSourceMappedActors(aSource).then(actors => {
+ let actor = this.createNonSourceMappedActor(aSource);
+ return (actors || [actor]).filter(isNotNull);
+ });
+ },
+
+ /**
+ * Return a promise of a SourceMapConsumer for the source map for
+ * `aSource`; if we already have such a promise extant, return that.
+ * This will fetch the source map if we don't have a cached object
+ * and source maps are enabled (see `_fetchSourceMap`).
+ *
+ * @param Debugger.Source aSource
+ * The source instance to get sourcemaps for.
+ * @return Promise of a SourceMapConsumer
+ */
+ fetchSourceMap: function (aSource) {
+ if (!this._useSourceMaps) {
+ return resolve(null);
+ }
+ else if (this._sourceMaps.has(aSource)) {
+ return this._sourceMaps.get(aSource);
+ }
+ else if (!aSource || !aSource.sourceMapURL) {
+ return resolve(null);
+ }
+
+ let sourceMapURL = aSource.sourceMapURL;
+ if (aSource.url) {
+ sourceMapURL = joinURI(aSource.url, sourceMapURL);
+ }
+ let result = this._fetchSourceMap(sourceMapURL, aSource.url);
+
+ // The promises in `_sourceMaps` must be the exact same instances
+ // as returned by `_fetchSourceMap` for `clearSourceMapCache` to
+ // work.
+ this._sourceMaps.set(aSource, result);
+ return result;
+ },
+
+ /**
+ * Return a promise of a SourceMapConsumer for the source map for
+ * `aSource`. The resolved result may be null if the source does not
+ * have a source map or source maps are disabled.
+ */
+ getSourceMap: function (aSource) {
+ return resolve(this._sourceMaps.get(aSource));
+ },
+
+ /**
+ * Set a SourceMapConsumer for the source map for
+ * |aSource|.
+ */
+ setSourceMap: function (aSource, aMap) {
+ this._sourceMaps.set(aSource, resolve(aMap));
+ },
+
+ /**
+ * Return a promise of a SourceMapConsumer for the source map located at
+ * |aAbsSourceMapURL|, which must be absolute. If there is already such a
+ * promise extant, return it. This will not fetch if source maps are
+ * disabled.
+ *
+ * @param string aAbsSourceMapURL
+ * The source map URL, in absolute form, not relative.
+ * @param string aScriptURL
+ * When the source map URL is a data URI, there is no sourceRoot on the
+ * source map, and the source map's sources are relative, we resolve
+ * them from aScriptURL.
+ */
+ _fetchSourceMap: function (aAbsSourceMapURL, aSourceURL) {
+ assert(this._useSourceMaps,
+ "Cannot fetch sourcemaps if they are disabled");
+
+ if (this._sourceMapCache[aAbsSourceMapURL]) {
+ return this._sourceMapCache[aAbsSourceMapURL];
+ }
+
+ let fetching = fetch(aAbsSourceMapURL, { loadFromCache: false })
+ .then(({ content }) => {
+ let map = new SourceMapConsumer(content);
+ this._setSourceMapRoot(map, aAbsSourceMapURL, aSourceURL);
+ return map;
+ })
+ .then(null, error => {
+ if (!DevToolsUtils.reportingDisabled) {
+ DevToolsUtils.reportException("TabSources.prototype._fetchSourceMap", error);
+ }
+ return null;
+ });
+ this._sourceMapCache[aAbsSourceMapURL] = fetching;
+ return fetching;
+ },
+
+ /**
+ * Sets the source map's sourceRoot to be relative to the source map url.
+ */
+ _setSourceMapRoot: function (aSourceMap, aAbsSourceMapURL, aScriptURL) {
+ // No need to do this fiddling if we won't be fetching any sources over the
+ // wire.
+ if (aSourceMap.hasContentsOfAllSources()) {
+ return;
+ }
+
+ const base = this._dirname(
+ aAbsSourceMapURL.indexOf("data:") === 0
+ ? aScriptURL
+ : aAbsSourceMapURL);
+ aSourceMap.sourceRoot = aSourceMap.sourceRoot
+ ? joinURI(base, aSourceMap.sourceRoot)
+ : base;
+ },
+
+ _dirname: function (aPath) {
+ let url = new URL(aPath);
+ let href = url.href;
+ return href.slice(0, href.lastIndexOf("/"));
+ },
+
+ /**
+ * Clears the source map cache. Source maps are cached by URL so
+ * they can be reused across separate Debugger instances (once in
+ * this cache, they will never be reparsed again). They are
+ * also cached by Debugger.Source objects for usefulness. By default
+ * this just removes the Debugger.Source cache, but you can remove
+ * the lower-level URL cache with the `hard` option.
+ *
+ * @param aSourceMapURL string
+ * The source map URL to uncache
+ * @param opts object
+ * An object with the following properties:
+ * - hard: Also remove the lower-level URL cache, which will
+ * make us completely forget about the source map.
+ */
+ clearSourceMapCache: function (aSourceMapURL, opts = { hard: false }) {
+ let oldSm = this._sourceMapCache[aSourceMapURL];
+
+ if (opts.hard) {
+ delete this._sourceMapCache[aSourceMapURL];
+ }
+
+ if (oldSm) {
+ // Clear out the current cache so all sources will get the new one
+ for (let [source, sm] of this._sourceMaps.entries()) {
+ if (sm === oldSm) {
+ this._sourceMaps.delete(source);
+ }
+ }
+ }
+ },
+
+ /*
+ * Forcefully change the source map of a source, changing the
+ * sourceMapURL and installing the source map in the cache. This is
+ * necessary to expose changes across Debugger instances
+ * (pretty-printing is the use case). Generate a random url if one
+ * isn't specified, allowing you to set "anonymous" source maps.
+ *
+ * @param aSource Debugger.Source
+ * The source to change the sourceMapURL property
+ * @param aUrl string
+ * The source map URL (optional)
+ * @param aMap SourceMapConsumer
+ * The source map instance
+ */
+ setSourceMapHard: function (aSource, aUrl, aMap) {
+ let url = aUrl;
+ if (!url) {
+ // This is a littly hacky, but we want to forcefully set a
+ // sourcemap regardless of sourcemap settings. We want to
+ // literally change the sourceMapURL so that all debuggers will
+ // get this and pretty-printing will Just Work (Debugger.Source
+ // instances are per-debugger, so we can't key off that). To
+ // avoid tons of work serializing the sourcemap into a data url,
+ // just make a fake URL and stick the sourcemap there.
+ url = "internal://sourcemap" + (this._anonSourceMapId++) + "/";
+ }
+ aSource.sourceMapURL = url;
+
+ // Forcefully set the sourcemap cache. This will be used even if
+ // sourcemaps are disabled.
+ this._sourceMapCache[url] = resolve(aMap);
+ this.emit("updatedSource", this.getSourceActor(aSource));
+ },
+
+ /**
+ * Return the non-source-mapped location of the given Debugger.Frame. If the
+ * frame does not have a script, the location's properties are all null.
+ *
+ * @param Debugger.Frame aFrame
+ * The frame whose location we are getting.
+ * @returns Object
+ * Returns an object of the form { source, line, column }
+ */
+ getFrameLocation: function (aFrame) {
+ if (!aFrame || !aFrame.script) {
+ return new GeneratedLocation();
+ }
+ let {lineNumber, columnNumber} =
+ aFrame.script.getOffsetLocation(aFrame.offset);
+ return new GeneratedLocation(
+ this.createNonSourceMappedActor(aFrame.script.source),
+ lineNumber,
+ columnNumber
+ );
+ },
+
+ /**
+ * Returns a promise of the location in the original source if the source is
+ * source mapped, otherwise a promise of the same location. This can
+ * be called with a source from *any* Debugger instance and we make
+ * sure to that it works properly, reusing source maps if already
+ * fetched. Use this from any actor that needs sourcemapping.
+ */
+ getOriginalLocation: function (generatedLocation) {
+ let {
+ generatedSourceActor,
+ generatedLine,
+ generatedColumn
+ } = generatedLocation;
+ let source = generatedSourceActor.source;
+ let url = source ? source.url : generatedSourceActor._originalUrl;
+
+ // In certain scenarios the source map may have not been fetched
+ // yet (or at least tied to this Debugger.Source instance), so use
+ // `fetchSourceMap` instead of `getSourceMap`. This allows this
+ // function to be called from anywere (across debuggers) and it
+ // should just automatically work.
+ return this.fetchSourceMap(source).then(map => {
+ if (map) {
+ let {
+ source: originalUrl,
+ line: originalLine,
+ column: originalColumn,
+ name: originalName
+ } = map.originalPositionFor({
+ line: generatedLine,
+ column: generatedColumn == null ? Infinity : generatedColumn
+ });
+
+ // Since the `Debugger.Source` instance may come from a
+ // different `Debugger` instance (any actor can call this
+ // method), we can't rely on any of the source discovery
+ // setup (`_discoverSources`, etc) to have been run yet. So
+ // we have to assume that the actor may not already exist,
+ // and we might need to create it, so use `source` and give
+ // it the required parameters for a sourcemapped source.
+ return new OriginalLocation(
+ originalUrl ? this.source({
+ originalUrl: originalUrl,
+ generatedSource: source
+ }) : null,
+ originalLine,
+ originalColumn,
+ originalName
+ );
+ }
+
+ // No source map
+ return OriginalLocation.fromGeneratedLocation(generatedLocation);
+ });
+ },
+
+ getAllGeneratedLocations: function (originalLocation) {
+ let {
+ originalSourceActor,
+ originalLine,
+ originalColumn
+ } = originalLocation;
+
+ let source = (originalSourceActor.source ||
+ originalSourceActor.generatedSource);
+
+ return this.fetchSourceMap(source).then((map) => {
+ if (map) {
+ map.computeColumnSpans();
+
+ return map.allGeneratedPositionsFor({
+ source: originalSourceActor.url,
+ line: originalLine,
+ column: originalColumn
+ }).map(({ line, column, lastColumn }) => {
+ return new GeneratedLocation(
+ this.createNonSourceMappedActor(source),
+ line,
+ column,
+ lastColumn
+ );
+ });
+ }
+
+ return [GeneratedLocation.fromOriginalLocation(originalLocation)];
+ });
+ },
+
+
+ /**
+ * Returns a promise of the location in the generated source corresponding to
+ * the original source and line given.
+ *
+ * When we pass a script S representing generated code to `sourceMap`,
+ * above, that returns a promise P. The process of resolving P populates
+ * the tables this function uses; thus, it won't know that S's original
+ * source URLs map to S until P is resolved.
+ */
+ getGeneratedLocation: function (originalLocation) {
+ let { originalSourceActor } = originalLocation;
+
+ // Both original sources and normal sources could have sourcemaps,
+ // because normal sources can be pretty-printed which generates a
+ // sourcemap for itself. Check both of the source properties to make it work
+ // for both kinds of sources.
+ let source = originalSourceActor.source || originalSourceActor.generatedSource;
+
+ // See comment about `fetchSourceMap` in `getOriginalLocation`.
+ return this.fetchSourceMap(source).then((map) => {
+ if (map) {
+ let {
+ originalLine,
+ originalColumn
+ } = originalLocation;
+
+ let {
+ line: generatedLine,
+ column: generatedColumn
+ } = map.generatedPositionFor({
+ source: originalSourceActor.url,
+ line: originalLine,
+ column: originalColumn == null ? 0 : originalColumn,
+ bias: SourceMapConsumer.LEAST_UPPER_BOUND
+ });
+
+ return new GeneratedLocation(
+ this.createNonSourceMappedActor(source),
+ generatedLine,
+ generatedColumn
+ );
+ }
+
+ return GeneratedLocation.fromOriginalLocation(originalLocation);
+ });
+ },
+
+ /**
+ * Returns true if URL for the given source is black boxed.
+ *
+ * @param aURL String
+ * The URL of the source which we are checking whether it is black
+ * boxed or not.
+ */
+ isBlackBoxed: function (aURL) {
+ return this.blackBoxedSources.has(aURL);
+ },
+
+ /**
+ * Add the given source URL to the set of sources that are black boxed.
+ *
+ * @param aURL String
+ * The URL of the source which we are black boxing.
+ */
+ blackBox: function (aURL) {
+ this.blackBoxedSources.add(aURL);
+ },
+
+ /**
+ * Remove the given source URL to the set of sources that are black boxed.
+ *
+ * @param aURL String
+ * The URL of the source which we are no longer black boxing.
+ */
+ unblackBox: function (aURL) {
+ this.blackBoxedSources.delete(aURL);
+ },
+
+ /**
+ * Returns true if the given URL is pretty printed.
+ *
+ * @param aURL String
+ * The URL of the source that might be pretty printed.
+ */
+ isPrettyPrinted: function (aURL) {
+ return this.prettyPrintedSources.has(aURL);
+ },
+
+ /**
+ * Add the given URL to the set of sources that are pretty printed.
+ *
+ * @param aURL String
+ * The URL of the source to be pretty printed.
+ */
+ prettyPrint: function (aURL, aIndent) {
+ this.prettyPrintedSources.set(aURL, aIndent);
+ },
+
+ /**
+ * Return the indent the given URL was pretty printed by.
+ */
+ prettyPrintIndent: function (aURL) {
+ return this.prettyPrintedSources.get(aURL);
+ },
+
+ /**
+ * Remove the given URL from the set of sources that are pretty printed.
+ *
+ * @param aURL String
+ * The URL of the source that is no longer pretty printed.
+ */
+ disablePrettyPrint: function (aURL) {
+ this.prettyPrintedSources.delete(aURL);
+ },
+
+ iter: function () {
+ let actors = Object.keys(this._sourceMappedSourceActors).map(k => {
+ return this._sourceMappedSourceActors[k];
+ });
+ for (let actor of this._sourceActors.values()) {
+ if (!this._sourceMaps.has(actor.source)) {
+ actors.push(actor);
+ }
+ }
+ return actors;
+ }
+};
+
+/*
+ * Checks if a source should never be displayed to the user because
+ * it's either internal or we don't support in the UI yet.
+ */
+function isHiddenSource(aSource) {
+ // Ignore the internal Function.prototype script
+ return aSource.text === "() {\n}";
+}
+
+/**
+ * Returns true if its argument is not null.
+ */
+function isNotNull(aThing) {
+ return aThing !== null;
+}
+
+exports.TabSources = TabSources;
+exports.isHiddenSource = isHiddenSource;
diff --git a/devtools/server/actors/utils/actor-registry-utils.js b/devtools/server/actors/utils/actor-registry-utils.js
new file mode 100644
index 000000000..5866827e1
--- /dev/null
+++ b/devtools/server/actors/utils/actor-registry-utils.js
@@ -0,0 +1,78 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+var { Cu, CC, Ci, Cc } = require("chrome");
+
+const { DebuggerServer } = require("devtools/server/main");
+const promise = require("promise");
+
+/**
+ * Support for actor registration. Main used by ActorRegistryActor
+ * for dynamic registration of new actors.
+ *
+ * @param sourceText {String} Source of the actor implementation
+ * @param fileName {String} URL of the actor module (for proper stack traces)
+ * @param options {Object} Configuration object
+ */
+exports.registerActor = function (sourceText, fileName, options) {
+ // Register in the current process
+ exports.registerActorInCurrentProcess(sourceText, fileName, options);
+ // Register in any child processes
+ return DebuggerServer.setupInChild({
+ module: "devtools/server/actors/utils/actor-registry-utils",
+ setupChild: "registerActorInCurrentProcess",
+ args: [sourceText, fileName, options],
+ waitForEval: true
+ });
+};
+
+exports.registerActorInCurrentProcess = function (sourceText, fileName, options) {
+ const principal = CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")();
+ const sandbox = Cu.Sandbox(principal);
+ sandbox.exports = {};
+ sandbox.require = require;
+
+ Cu.evalInSandbox(sourceText, sandbox, "1.8", fileName, 1);
+
+ let { prefix, constructor, type } = options;
+
+ if (type.global && !DebuggerServer.globalActorFactories.hasOwnProperty(prefix)) {
+ DebuggerServer.addGlobalActor({
+ constructorName: constructor,
+ constructorFun: sandbox[constructor]
+ }, prefix);
+ }
+
+ if (type.tab && !DebuggerServer.tabActorFactories.hasOwnProperty(prefix)) {
+ DebuggerServer.addTabActor({
+ constructorName: constructor,
+ constructorFun: sandbox[constructor]
+ }, prefix);
+ }
+};
+
+exports.unregisterActor = function (options) {
+ // Unregister in the current process
+ exports.unregisterActorInCurrentProcess(options);
+ // Unregister in any child processes
+ DebuggerServer.setupInChild({
+ module: "devtools/server/actors/utils/actor-registry-utils",
+ setupChild: "unregisterActorInCurrentProcess",
+ args: [options]
+ });
+};
+
+exports.unregisterActorInCurrentProcess = function (options) {
+ if (options.tab) {
+ DebuggerServer.removeTabActor(options);
+ }
+
+ if (options.global) {
+ DebuggerServer.removeGlobalActor(options);
+ }
+};
diff --git a/devtools/server/actors/utils/audionodes.json b/devtools/server/actors/utils/audionodes.json
new file mode 100644
index 000000000..12cc6c34b
--- /dev/null
+++ b/devtools/server/actors/utils/audionodes.json
@@ -0,0 +1,113 @@
+{
+ "OscillatorNode": {
+ "source": true,
+ "properties": {
+ "type": {},
+ "frequency": {
+ "param": true
+ },
+ "detune": {
+ "param": true
+ }
+ }
+ },
+ "GainNode": {
+ "properties": { "gain": { "param": true }}
+ },
+ "DelayNode": {
+ "properties": { "delayTime": { "param": true }}
+ },
+ "AudioBufferSourceNode": {
+ "source": true,
+ "properties": {
+ "buffer": { "Buffer": true },
+ "playbackRate": {
+ "param": true
+ },
+ "loop": {},
+ "loopStart": {},
+ "loopEnd": {}
+ }
+ },
+ "ScriptProcessorNode": {
+ "properties": { "bufferSize": { "readonly": true }}
+ },
+ "PannerNode": {
+ "properties": {
+ "panningModel": {},
+ "distanceModel": {},
+ "refDistance": {},
+ "maxDistance": {},
+ "rolloffFactor": {},
+ "coneInnerAngle": {},
+ "coneOuterAngle": {},
+ "coneOuterGain": {}
+ }
+ },
+ "ConvolverNode": {
+ "properties": {
+ "buffer": { "Buffer": true },
+ "normalize": {}
+ }
+ },
+ "DynamicsCompressorNode": {
+ "properties": {
+ "threshold": { "param": true },
+ "knee": { "param": true },
+ "ratio": { "param": true },
+ "reduction": {},
+ "attack": { "param": true },
+ "release": { "param": true }
+ }
+ },
+ "BiquadFilterNode": {
+ "properties": {
+ "type": {},
+ "frequency": { "param": true },
+ "Q": { "param": true },
+ "detune": { "param": true },
+ "gain": { "param": true }
+ }
+ },
+ "WaveShaperNode": {
+ "properties": {
+ "curve": { "Float32Array": true },
+ "oversample": {}
+ }
+ },
+ "AnalyserNode": {
+ "properties": {
+ "fftSize": {},
+ "minDecibels": {},
+ "maxDecibels": {},
+ "smoothingTimeConstant": {},
+ "frequencyBinCount": { "readonly": true }
+ }
+ },
+ "AudioDestinationNode": {
+ "unbypassable": true
+ },
+ "ChannelSplitterNode": {
+ "unbypassable": true
+ },
+ "ChannelMergerNode": {
+ "unbypassable": true
+ },
+ "MediaElementAudioSourceNode": {
+ "source": true
+ },
+ "MediaStreamAudioSourceNode": {
+ "source": true
+ },
+ "MediaStreamAudioDestinationNode": {
+ "unbypassable": true,
+ "properties": {
+ "stream": { "MediaStream": true }
+ }
+ },
+ "StereoPannerNode": {
+ "properties": {
+ "pan": { "param": true }
+ }
+ }
+}
diff --git a/devtools/server/actors/utils/automation-timeline.js b/devtools/server/actors/utils/automation-timeline.js
new file mode 100644
index 000000000..a086d90be
--- /dev/null
+++ b/devtools/server/actors/utils/automation-timeline.js
@@ -0,0 +1,373 @@
+/**
+ * web-audio-automation-timeline - 1.0.3
+ * https://github.com/jsantell/web-audio-automation-timeline
+ * MIT License, copyright (c) 2014 Jordan Santell
+ */
+!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.Timeline=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+module.exports = require("./lib/timeline").Timeline;
+
+},{"./lib/timeline":4}],2:[function(require,module,exports){
+var F = require("./formulas");
+
+function TimelineEvent (eventName, value, time, timeConstant, duration) {
+ this.type = eventName;
+ this.value = value;
+ this.time = time;
+ this.constant = timeConstant || 0;
+ this.duration = duration || 0;
+}
+exports.TimelineEvent = TimelineEvent;
+
+
+TimelineEvent.prototype.exponentialApproach = function (lastValue, time) {
+ return F.exponentialApproach(this.time, lastValue, this.value, this.constant, time);
+}
+
+TimelineEvent.prototype.extractValueFromCurve = function (time) {
+ return F.extractValueFromCurve(this.time, this.value, this.value.length, this.duration, time);
+}
+
+TimelineEvent.prototype.linearInterpolate = function (next, time) {
+ return F.linearInterpolate(this.time, this.value, next.time, next.value, time);
+}
+
+TimelineEvent.prototype.exponentialInterpolate = function (next, time) {
+ return F.exponentialInterpolate(this.time, this.value, next.time, next.value, time);
+}
+
+},{"./formulas":3}],3:[function(require,module,exports){
+var EPSILON = 0.0000000001;
+
+exports.linearInterpolate = function (t0, v0, t1, v1, t) {
+ return v0 + (v1 - v0) * ((t - t0) / (t1 - t0));
+};
+
+exports.exponentialInterpolate = function (t0, v0, t1, v1, t) {
+ return v0 * Math.pow(v1 / v0, (t - t0) / (t1 - t0));
+};
+
+exports.extractValueFromCurve = function (start, curve, curveLength, duration, t) {
+ var ratio;
+
+ // If time is after duration, return the last curve value,
+ // or if ratio is >= 1
+ if (t >= start + duration || (ratio = Math.max((t - start) / duration, 0)) >= 1) {
+ return curve[curveLength - 1];
+ }
+
+ return curve[~~(curveLength * ratio)];
+};
+
+exports.exponentialApproach = function (t0, v0, v1, timeConstant, t) {
+ return v1 + (v0 - v1) * Math.exp(-(t - t0) / timeConstant);
+};
+
+// Since we are going to accumulate error by adding 0.01 multiple times
+// in a loop, we want to fuzz the equality check in `getValueAtTime`
+exports.fuzzyEqual = function (lhs, rhs) {
+ return Math.abs(lhs - rhs) < EPSILON;
+};
+
+exports.EPSILON = EPSILON;
+
+},{}],4:[function(require,module,exports){
+var TimelineEvent = require("./event").TimelineEvent;
+var F = require("./formulas");
+
+exports.Timeline = Timeline;
+
+function Timeline (defaultValue) {
+ this.events = [];
+
+ this._value = defaultValue || 0;
+}
+
+Timeline.prototype.getEventCount = function () {
+ return this.events.length;
+};
+
+Timeline.prototype.value = function () {
+ return this._value;
+};
+
+Timeline.prototype.setValue = function (value) {
+ if (this.events.length === 0) {
+ this._value = value;
+ }
+};
+
+Timeline.prototype.getValue = function () {
+ if (this.events.length) {
+ throw new Error("Can only call `getValue` when there are 0 events.");
+ }
+
+ return this._value;
+};
+
+Timeline.prototype.getValueAtTime = function (time) {
+ return this._getValueAtTimeHelper(time);
+};
+
+Timeline.prototype._getValueAtTimeHelper = function (time) {
+ var bailOut = false;
+ var previous = null;
+ var next = null;
+ var lastComputedValue = null; // Used for `setTargetAtTime` nodes
+ var events = this.events;
+ var e;
+
+ for (var i = 0; !bailOut && i < events.length; i++) {
+ if (F.fuzzyEqual(time, events[i].time)) {
+ // Find the last event with the same time as `time`
+ do {
+ ++i;
+ } while (i < events.length && F.fuzzyEqual(time, events[i].time));
+
+ e = events[i - 1];
+
+ // `setTargetAtTime` can be handled no matter what their next event is (if they have one)
+ if (e.type === "setTargetAtTime") {
+ lastComputedValue = this._lastComputedValue(e);
+ return e.exponentialApproach(lastComputedValue, time);
+ }
+
+ // `setValueCurveAtTime` events can be handled no matter what their next event node is
+ // (if they have one)
+ if (e.type === "setValueCurveAtTime") {
+ return e.extractValueFromCurve(time);
+ }
+
+ // For other event types
+ return e.value;
+ }
+ previous = next;
+ next = events[i];
+
+ if (time < events[i].time) {
+ bailOut = true;
+ }
+ }
+
+ // Handle the case where the time is past all of the events
+ if (!bailOut) {
+ previous = next;
+ next = null;
+ }
+
+ // Just return the default value if we did not find anything
+ if (!previous && !next) {
+ return this._value;
+ }
+
+ // If the requested time is before all of the existing events
+ if (!previous) {
+ return this._value;
+ }
+
+ // `setTargetAtTime` can be handled no matter what their next event is (if they have one)
+ if (previous.type === "setTargetAtTime") {
+ lastComputedValue = this._lastComputedValue(previous);
+ return previous.exponentialApproach(lastComputedValue, time);
+ }
+
+ // `setValueCurveAtTime` events can be handled no matter what their next event node is
+ // (if they have one)
+ if (previous.type === "setValueCurveAtTime") {
+ return previous.extractValueFromCurve(time);
+ }
+
+ if (!next) {
+ if (~["setValueAtTime", "linearRampToValueAtTime", "exponentialRampToValueAtTime"].indexOf(previous.type)) {
+ return previous.value;
+ }
+ if (previous.type === "setValueCurveAtTime") {
+ return previous.extractValueFromCurve(time);
+ }
+ if (previous.type === "setTargetAtTime") {
+ throw new Error("unreached");
+ }
+ throw new Error("unreached");
+ }
+
+ // Finally handle the case where we have both a previous and a next event
+ // First handle the case where our range ends up in a ramp event
+ if (next.type === "linearRampToValueAtTime") {
+ return previous.linearInterpolate(next, time);
+ } else if (next.type === "exponentialRampToValueAtTime") {
+ return previous.exponentialInterpolate(next, time);
+ }
+
+ // Now handle all other cases
+ if (~["setValueAtTime", "linearRampToValueAtTime", "exponentialRampToValueAtTime"].indexOf(previous.type)) {
+ // If the next event type is neither linear or exponential ramp,
+ // the value is constant.
+ return previous.value;
+ }
+ if (previous.type === "setValueCurveAtTime") {
+ return previous.extractValueFromCurve(time);
+ }
+ if (previous.type === "setTargetAtTime") {
+ throw new Error("unreached");
+ }
+ throw new Error("unreached");
+};
+
+Timeline.prototype._insertEvent = function (ev) {
+ var events = this.events;
+
+ if (ev.type === "setValueCurveAtTime") {
+ if (!ev.value || !ev.value.length) {
+ throw new Error("NS_ERROR_DOM_SYNTAX_ERR");
+ }
+ }
+
+ if (ev.type === "setTargetAtTime") {
+ if (F.fuzzyEqual(ev.constant, 0)) {
+ throw new Error("NS_ERROR_DOM_SYNTAX_ERR");
+ }
+ }
+
+ // Make sure that non-curve events don't fall within the duration of a
+ // curve event.
+ for (var i = 0; i < events.length; i++) {
+ if (events[i].type === "setValueCurveAtTime" &&
+ events[i].time <= ev.time &&
+ (events[i].time + events[i].duration) >= ev.time) {
+ throw new Error("NS_ERROR_DOM_SYNTAX_ERR");
+ }
+ }
+
+ // Make sure that curve events don't fall in a range which includes other
+ // events.
+ if (ev.type === "setValueCurveAtTime") {
+ for (var i = 0; i < events.length; i++) {
+ if (events[i].time > ev.time &&
+ events[i].time < (ev.time + ev.duration)) {
+ throw new Error("NS_ERROR_DOM_SYNTAX_ERR");
+ }
+ }
+ }
+
+ // Make sure that invalid values are not used for exponential curves
+ if (ev.type === "exponentialRampToValueAtTime") {
+ if (ev.value <= 0) throw new Error("NS_ERROR_DOM_SYNTAX_ERR");
+ var prev = this._getPreviousEvent(ev.time);
+ if (prev) {
+ if (prev.value <= 0) {
+ throw new Error("NS_ERROR_DOM_SYNTAX_ERR");
+ }
+ } else {
+ if (this._value <= 0) {
+ throw new Error("NS_ERROR_DOM_SYNTAX_ERR");
+ }
+ }
+ }
+
+ for (var i = 0; i < events.length; i++) {
+ if (ev.time === events[i].time) {
+ if (ev.type === events[i].type) {
+ // If times and types are equal, replace the event;
+ events[i] = ev;
+ } else {
+ // Otherwise, place the element after the last event of another type
+ do { i++; }
+ while (i < events.length && ev.type !== events[i].type && ev.time === events[i].time);
+ events.splice(i, 0, ev);
+ }
+ return;
+ }
+ // Otherwise, place the event right after the latest existing event
+ if (ev.time < events[i].time) {
+ events.splice(i, 0, ev);
+ return;
+ }
+ }
+
+ // If we couldn't find a place for the event, just append it to the list
+ this.events.push(ev);
+};
+
+Timeline.prototype._getPreviousEvent = function (time) {
+ var previous = null, next = null;
+ var bailOut = false;
+ var events = this.events;
+
+ for (var i = 0; !bailOut && i < events.length; i++) {
+ if (time === events[i]) {
+ do { ++i; }
+ while (i < events.length && time === events[i].time);
+ return events[i - 1];
+ }
+ previous = next;
+ next = events[i];
+ if (time < events[i].time) {
+ bailOut = true;
+ }
+ }
+
+ // Handle the case where the time is past all the events
+ if (!bailOut) {
+ previous = next;
+ }
+
+ return previous;
+};
+
+/**
+ * Calculates the previous value of the timeline, used for
+ * `setTargetAtTime` nodes. Takes an event, and returns
+ * the previous computed value for any sample taken during that
+ * exponential approach node.
+ */
+Timeline.prototype._lastComputedValue = function (event) {
+ // If equal times, return the value for the previous event, before
+ // the `setTargetAtTime` node.
+ var lastEvent = this._getPreviousEvent(event.time - F.EPSILON);
+
+ // If no event before the setTargetAtTime event, then return the
+ // intrinsic value.
+ if (!lastEvent) {
+ return this._value;
+ }
+ // Otherwise, return the value for the previous event, which should
+ // always be the last computed value (? I think?)
+ else {
+ return lastEvent.value;
+ }
+};
+
+Timeline.prototype.setValueAtTime = function (value, startTime) {
+ this._insertEvent(new TimelineEvent("setValueAtTime", value, startTime));
+};
+
+Timeline.prototype.linearRampToValueAtTime = function (value, endTime) {
+ this._insertEvent(new TimelineEvent("linearRampToValueAtTime", value, endTime));
+};
+
+Timeline.prototype.exponentialRampToValueAtTime = function (value, endTime) {
+ this._insertEvent(new TimelineEvent("exponentialRampToValueAtTime", value, endTime));
+};
+
+Timeline.prototype.setTargetAtTime = function (value, startTime, timeConstant) {
+ this._insertEvent(new TimelineEvent("setTargetAtTime", value, startTime, timeConstant));
+};
+
+Timeline.prototype.setValueCurveAtTime = function (value, startTime, duration) {
+ this._insertEvent(new TimelineEvent("setValueCurveAtTime", value, startTime, null, duration));
+};
+
+Timeline.prototype.cancelScheduledValues = function (time) {
+ for (var i = 0; i < this.events.length; i++) {
+ if (this.events[i].time >= time) {
+ this.events = this.events.slice(0, i);
+ break;
+ }
+ }
+};
+
+Timeline.prototype.cancelAllEvents = function () {
+ this.events.length = 0;
+};
+
+},{"./event":2,"./formulas":3}]},{},[1])(1)
+}); \ No newline at end of file
diff --git a/devtools/server/actors/utils/css-grid-utils.js b/devtools/server/actors/utils/css-grid-utils.js
new file mode 100644
index 000000000..0b260117d
--- /dev/null
+++ b/devtools/server/actors/utils/css-grid-utils.js
@@ -0,0 +1,61 @@
+/* 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");
+
+/**
+ * Returns the grid fragment array with all the grid fragment data stringifiable.
+ *
+ * @param {Object} fragments
+ * Grid fragment object.
+ * @return {Array} representation with the grid fragment data stringifiable.
+ */
+function getStringifiableFragments(fragments = []) {
+ if (fragments[0] && Cu.isDeadWrapper(fragments[0])) {
+ return {};
+ }
+
+ return fragments.map(getStringifiableFragment);
+}
+
+/**
+ * Returns a string representation of the CSS Grid data as returned by
+ * node.getGridFragments. This is useful to compare grid state at each update and redraw
+ * the highlighter if needed. It also seralizes the grid fragment data so it can be used
+ * by protocol.js.
+ *
+ * @param {Object} fragments
+ * Grid fragment object.
+ * @return {String} representation of the CSS grid fragment data.
+ */
+function stringifyGridFragments(fragments) {
+ return JSON.stringify(getStringifiableFragments(fragments));
+}
+
+function getStringifiableFragment(fragment) {
+ return {
+ cols: getStringifiableDimension(fragment.cols),
+ rows: getStringifiableDimension(fragment.rows)
+ };
+}
+
+function getStringifiableDimension(dimension) {
+ return {
+ lines: [...dimension.lines].map(getStringifiableLine),
+ tracks: [...dimension.tracks].map(getStringifiableTrack),
+ };
+}
+
+function getStringifiableLine({ breadth, number, start, names }) {
+ return { breadth, number, start, names };
+}
+
+function getStringifiableTrack({ breadth, start, state, type }) {
+ return { breadth, start, state, type };
+}
+
+exports.getStringifiableFragments = getStringifiableFragments;
+exports.stringifyGridFragments = stringifyGridFragments;
diff --git a/devtools/server/actors/utils/make-debugger.js b/devtools/server/actors/utils/make-debugger.js
new file mode 100644
index 000000000..9bd43e567
--- /dev/null
+++ b/devtools/server/actors/utils/make-debugger.js
@@ -0,0 +1,101 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 EventEmitter = require("devtools/shared/event-emitter");
+const Debugger = require("Debugger");
+
+const { reportException } = require("devtools/shared/DevToolsUtils");
+
+/**
+ * Multiple actors that use a |Debugger| instance come in a few versions, each
+ * with a different set of debuggees. One version for content tabs (globals
+ * within a tab), one version for chrome debugging (all globals), and sometimes
+ * a third version for addon debugging (chrome globals the addon is loaded in
+ * and content globals the addon injects scripts into). The |makeDebugger|
+ * function helps us avoid repeating the logic for finding and maintaining the
+ * correct set of globals for a given |Debugger| instance across each version of
+ * all of our actors.
+ *
+ * The |makeDebugger| function expects a single object parameter with the
+ * following properties:
+ *
+ * @param Function findDebuggees
+ * Called with one argument: a |Debugger| instance. This function should
+ * return an iterable of globals to be added to the |Debugger|
+ * instance. The globals may be wrapped in a |Debugger.Object|, or
+ * unwrapped.
+ *
+ * @param Function shouldAddNewGlobalAsDebuggee
+ * Called with one argument: a |Debugger.Object| wrapping a global
+ * object. This function must return |true| if the global object should
+ * be added as debuggee, and |false| otherwise.
+ *
+ * @returns Debugger
+ * Returns a |Debugger| instance that can manage its set of debuggee
+ * globals itself and is decorated with the |EventEmitter| class.
+ *
+ * Events emitted by the returned |Debugger| instance:
+ *
+ * - "newGlobal": Emitted when a new global has been added as a
+ * debuggee. Passes the |Debugger.Object| wrapping the new
+ * debuggee global to listeners.
+ *
+ * Existing |Debugger| properties set on the returned |Debugger|
+ * instance:
+ *
+ * - onNewGlobalObject: The |Debugger| will automatically add new
+ * globals as debuggees if calling |shouldAddNewGlobalAsDebuggee|
+ * with the global returns true.
+ *
+ * - uncaughtExceptionHook: The |Debugger| already has an error
+ * reporter attached to |uncaughtExceptionHook|, so if any
+ * |Debugger| hooks fail, the error will be reported.
+ *
+ * New properties set on the returned |Debugger| instance:
+ *
+ * - addDebuggees: A function which takes no arguments. It adds all
+ * current globals that should be debuggees (as determined by
+ * |findDebuggees|) to the |Debugger| instance.
+ */
+module.exports = function makeDebugger({ findDebuggees, shouldAddNewGlobalAsDebuggee }) {
+ const dbg = new Debugger();
+ EventEmitter.decorate(dbg);
+
+ dbg.allowUnobservedAsmJS = true;
+ dbg.uncaughtExceptionHook = reportDebuggerHookException;
+
+ dbg.onNewGlobalObject = function (global) {
+ if (shouldAddNewGlobalAsDebuggee(global)) {
+ safeAddDebuggee(this, global);
+ }
+ };
+
+ dbg.addDebuggees = function () {
+ for (let global of findDebuggees(this)) {
+ safeAddDebuggee(this, global);
+ }
+ };
+
+ return dbg;
+};
+
+const reportDebuggerHookException = e => reportException("Debugger Hook", e);
+
+/**
+ * Add |global| as a debuggee to |dbg|, handling error cases.
+ */
+function safeAddDebuggee(dbg, global) {
+ try {
+ let wrappedGlobal = dbg.addDebuggee(global);
+ if (wrappedGlobal) {
+ dbg.emit("newGlobal", wrappedGlobal);
+ }
+ } catch (e) {
+ // Ignoring attempt to add the debugger's compartment as a debuggee.
+ }
+}
diff --git a/devtools/server/actors/utils/map-uri-to-addon-id.js b/devtools/server/actors/utils/map-uri-to-addon-id.js
new file mode 100644
index 000000000..6f3316b14
--- /dev/null
+++ b/devtools/server/actors/utils/map-uri-to-addon-id.js
@@ -0,0 +1,44 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const Services = require("Services");
+
+loader.lazyServiceGetter(this, "AddonPathService",
+ "@mozilla.org/addon-path-service;1",
+ "amIAddonPathService");
+
+const B2G_ID = "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}";
+const GRAPHENE_ID = "{d1bfe7d9-c01e-4237-998b-7b5f960a4314}";
+
+/**
+ * This is a wrapper around amIAddonPathService.mapURIToAddonID which always returns
+ * false on B2G and graphene to avoid loading the add-on manager there and
+ * reports any exceptions rather than throwing so that the caller doesn't have
+ * to worry about them.
+ */
+if (!Services.appinfo
+ || Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT
+ || Services.appinfo.ID === undefined /* XPCShell */
+ || Services.appinfo.ID == B2G_ID
+ || Services.appinfo.ID == GRAPHENE_ID
+ || !AddonPathService) {
+ module.exports = function mapURIToAddonId(uri) {
+ return false;
+ };
+} else {
+ module.exports = function mapURIToAddonId(uri) {
+ try {
+ return AddonPathService.mapURIToAddonId(uri);
+ }
+ catch (e) {
+ DevToolsUtils.reportException("mapURIToAddonId", e);
+ return false;
+ }
+ };
+}
diff --git a/devtools/server/actors/utils/moz.build b/devtools/server/actors/utils/moz.build
new file mode 100644
index 000000000..0dcf40faf
--- /dev/null
+++ b/devtools/server/actors/utils/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'actor-registry-utils.js',
+ 'audionodes.json',
+ 'automation-timeline.js',
+ 'css-grid-utils.js',
+ 'make-debugger.js',
+ 'map-uri-to-addon-id.js',
+ 'stack.js',
+ 'TabSources.js',
+ 'walker-search.js',
+ 'webconsole-utils.js',
+ 'webconsole-worker-utils.js',
+)
diff --git a/devtools/server/actors/utils/stack.js b/devtools/server/actors/utils/stack.js
new file mode 100644
index 000000000..a6a3d1137
--- /dev/null
+++ b/devtools/server/actors/utils/stack.js
@@ -0,0 +1,185 @@
+/* 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";
+
+var {Class} = require("sdk/core/heritage");
+
+/**
+ * A helper class that stores stack frame objects. Each frame is
+ * assigned an index, and if a frame is added more than once, the same
+ * index is used. Users of the class can get an array of all frames
+ * that have been added.
+ */
+var StackFrameCache = Class({
+ /**
+ * Initialize this object.
+ */
+ initialize: function () {
+ this._framesToIndices = null;
+ this._framesToForms = null;
+ this._lastEventSize = 0;
+ },
+
+ /**
+ * Prepare to accept frames.
+ */
+ initFrames: function () {
+ if (this._framesToIndices) {
+ // The maps are already initialized.
+ return;
+ }
+
+ this._framesToIndices = new Map();
+ this._framesToForms = new Map();
+ this._lastEventSize = 0;
+ },
+
+ /**
+ * Forget all stored frames and reset to the initialized state.
+ */
+ clearFrames: function () {
+ this._framesToIndices.clear();
+ this._framesToIndices = null;
+ this._framesToForms.clear();
+ this._framesToForms = null;
+ this._lastEventSize = 0;
+ },
+
+ /**
+ * Add a frame to this stack frame cache, and return the index of
+ * the frame.
+ */
+ addFrame: function (frame) {
+ this._assignFrameIndices(frame);
+ this._createFrameForms(frame);
+ return this._framesToIndices.get(frame);
+ },
+
+ /**
+ * A helper method for the memory actor. This populates the packet
+ * object with "frames" property. Each of these
+ * properties will be an array indexed by frame ID. "frames" will
+ * contain frame objects (see makeEvent).
+ *
+ * @param packet
+ * The packet to update.
+ *
+ * @returns packet
+ */
+ updateFramePacket: function (packet) {
+ // Now that we are guaranteed to have a form for every frame, we know the
+ // size the "frames" property's array must be. We use that information to
+ // create dense arrays even though we populate them out of order.
+ const size = this._framesToForms.size;
+ packet.frames = Array(size).fill(null);
+
+ // Populate the "frames" properties.
+ for (let [stack, index] of this._framesToIndices) {
+ packet.frames[index] = this._framesToForms.get(stack);
+ }
+
+ return packet;
+ },
+
+ /**
+ * If any new stack frames have been added to this cache since the
+ * last call to makeEvent (clearing the cache also resets the "last
+ * call"), then return a new array describing the new frames. If no
+ * new frames are available, return null.
+ *
+ * The frame cache assumes that the user of the cache keeps track of
+ * all previously-returned arrays and, in theory, concatenates them
+ * all to form a single array holding all frames added to the cache
+ * since the last reset. This concatenated array can be indexed by
+ * the frame ID. The array returned by this function, though, is
+ * dense and starts at 0.
+ *
+ * Each element in the array is an object of the form:
+ * {
+ * line: <line number for this frame>,
+ * column: <column number for this frame>,
+ * source: <filename string for this frame>,
+ * functionDisplayName: <this frame's inferred function name function or null>,
+ * parent: <frame ID -- an index into the concatenated array mentioned above>
+ * asyncCause: the async cause, or null
+ * asyncParent: <frame ID -- an index into the concatenated array mentioned above>
+ * }
+ *
+ * The intent of this approach is to make it simpler to efficiently
+ * send frame information over the debugging protocol, by only
+ * sending new frames.
+ *
+ * @returns array or null
+ */
+ makeEvent: function () {
+ const size = this._framesToForms.size;
+ if (!size || size <= this._lastEventSize) {
+ return null;
+ }
+
+ let packet = Array(size - this._lastEventSize).fill(null);
+ for (let [stack, index] of this._framesToIndices) {
+ if (index >= this._lastEventSize) {
+ packet[index - this._lastEventSize] = this._framesToForms.get(stack);
+ }
+ }
+
+ this._lastEventSize = size;
+
+ return packet;
+ },
+
+ /**
+ * Assigns an index to the given frame and its parents, if an index is not
+ * already assigned.
+ *
+ * @param SavedFrame frame
+ * A frame to assign an index to.
+ */
+ _assignFrameIndices: function (frame) {
+ if (this._framesToIndices.has(frame)) {
+ return;
+ }
+
+ if (frame) {
+ this._assignFrameIndices(frame.parent);
+ this._assignFrameIndices(frame.asyncParent);
+ }
+
+ const index = this._framesToIndices.size;
+ this._framesToIndices.set(frame, index);
+ },
+
+ /**
+ * Create the form for the given frame, if one doesn't already exist.
+ *
+ * @param SavedFrame frame
+ * A frame to create a form for.
+ */
+ _createFrameForms: function (frame) {
+ if (this._framesToForms.has(frame)) {
+ return;
+ }
+
+ let form = null;
+ if (frame) {
+ form = {
+ line: frame.line,
+ column: frame.column,
+ source: frame.source,
+ functionDisplayName: frame.functionDisplayName,
+ parent: this._framesToIndices.get(frame.parent),
+ asyncParent: this._framesToIndices.get(frame.asyncParent),
+ asyncCause: frame.asyncCause
+ };
+ this._createFrameForms(frame.parent);
+ this._createFrameForms(frame.asyncParent);
+ }
+
+ this._framesToForms.set(frame, form);
+ },
+});
+
+exports.StackFrameCache = StackFrameCache;
diff --git a/devtools/server/actors/utils/walker-search.js b/devtools/server/actors/utils/walker-search.js
new file mode 100644
index 000000000..0955a3919
--- /dev/null
+++ b/devtools/server/actors/utils/walker-search.js
@@ -0,0 +1,278 @@
+/* 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";
+
+/**
+ * The walker-search module provides a simple API to index and search strings
+ * and elements inside a given document.
+ * It indexes tag names, attribute names and values, and text contents.
+ * It provides a simple search function that returns a list of nodes that
+ * matched.
+ */
+
+const {Ci, Cu} = require("chrome");
+
+/**
+ * The WalkerIndex class indexes the document (and all subdocs) from
+ * a given walker.
+ *
+ * It is only indexed the first time the data is accessed and will be
+ * re-indexed if a mutation happens between requests.
+ *
+ * @param {Walker} walker The walker to be indexed
+ */
+function WalkerIndex(walker) {
+ this.walker = walker;
+ this.clearIndex = this.clearIndex.bind(this);
+
+ // Kill the index when mutations occur, the next data get will re-index.
+ this.walker.on("any-mutation", this.clearIndex);
+}
+
+WalkerIndex.prototype = {
+ /**
+ * Destroy this instance, releasing all data and references
+ */
+ destroy: function () {
+ this.walker.off("any-mutation", this.clearIndex);
+ },
+
+ clearIndex: function () {
+ if (!this.currentlyIndexing) {
+ this._data = null;
+ }
+ },
+
+ get doc() {
+ return this.walker.rootDoc;
+ },
+
+ /**
+ * Get the indexed data
+ * This getter also indexes if it hasn't been done yet or if the state is
+ * dirty
+ *
+ * @returns Map<String, Array<{type:String, node:DOMNode}>>
+ * A Map keyed on the searchable value, containing an array with
+ * objects containing the 'type' (one of ALL_RESULTS_TYPES), and
+ * the DOM Node.
+ */
+ get data() {
+ if (!this._data) {
+ this._data = new Map();
+ this.index();
+ }
+
+ return this._data;
+ },
+
+ _addToIndex: function (type, node, value) {
+ // Add an entry for this value if there isn't one
+ let entry = this._data.get(value);
+ if (!entry) {
+ this._data.set(value, []);
+ }
+
+ // Add the type/node to the list
+ this._data.get(value).push({
+ type: type,
+ node: node
+ });
+ },
+
+ index: function () {
+ // Handle case where iterating nextNode() with the deepTreeWalker triggers
+ // a mutation (Bug 1222558)
+ this.currentlyIndexing = true;
+
+ let documentWalker = this.walker.getDocumentWalker(this.doc);
+ while (documentWalker.nextNode()) {
+ let node = documentWalker.currentNode;
+
+ if (node.nodeType === 1) {
+ // For each element node, we get the tagname and all attributes names
+ // and values
+ let localName = node.localName;
+ if (localName === "_moz_generated_content_before") {
+ this._addToIndex("tag", node, "::before");
+ this._addToIndex("text", node, node.textContent.trim());
+ } else if (localName === "_moz_generated_content_after") {
+ this._addToIndex("tag", node, "::after");
+ this._addToIndex("text", node, node.textContent.trim());
+ } else {
+ this._addToIndex("tag", node, node.localName);
+ }
+
+ for (let {name, value} of node.attributes) {
+ this._addToIndex("attributeName", node, name);
+ this._addToIndex("attributeValue", node, value);
+ }
+ } else if (node.textContent && node.textContent.trim().length) {
+ // For comments and text nodes, we get the text
+ this._addToIndex("text", node, node.textContent.trim());
+ }
+ }
+
+ this.currentlyIndexing = false;
+ }
+};
+
+exports.WalkerIndex = WalkerIndex;
+
+/**
+ * The WalkerSearch class provides a way to search an indexed document as well
+ * as find elements that match a given css selector.
+ *
+ * Usage example:
+ * let s = new WalkerSearch(doc);
+ * let res = s.search("lang", index);
+ * for (let {matched, results} of res) {
+ * for (let {node, type} of results) {
+ * console.log("The query matched a node's " + type);
+ * console.log("Node that matched", node);
+ * }
+ * }
+ * s.destroy();
+ *
+ * @param {Walker} the walker to be searched
+ */
+function WalkerSearch(walker) {
+ this.walker = walker;
+ this.index = new WalkerIndex(this.walker);
+}
+
+WalkerSearch.prototype = {
+ destroy: function () {
+ this.index.destroy();
+ this.walker = null;
+ },
+
+ _addResult: function (node, type, results) {
+ if (!results.has(node)) {
+ results.set(node, []);
+ }
+
+ let matches = results.get(node);
+
+ // Do not add if the exact same result is already in the list
+ let isKnown = false;
+ for (let match of matches) {
+ if (match.type === type) {
+ isKnown = true;
+ break;
+ }
+ }
+
+ if (!isKnown) {
+ matches.push({type});
+ }
+ },
+
+ _searchIndex: function (query, options, results) {
+ for (let [matched, res] of this.index.data) {
+ if (!options.searchMethod(query, matched)) {
+ continue;
+ }
+
+ // Add any relevant results (skipping non-requested options).
+ res.filter(entry => {
+ return options.types.indexOf(entry.type) !== -1;
+ }).forEach(({node, type}) => {
+ this._addResult(node, type, results);
+ });
+ }
+ },
+
+ _searchSelectors: function (query, options, results) {
+ // If the query is just one "word", no need to search because _searchIndex
+ // will lead the same results since it has access to tagnames anyway
+ let isSelector = query && query.match(/[ >~.#\[\]]/);
+ if (options.types.indexOf("selector") === -1 || !isSelector) {
+ return;
+ }
+
+ let nodes = this.walker._multiFrameQuerySelectorAll(query);
+ for (let node of nodes) {
+ this._addResult(node, "selector", results);
+ }
+ },
+
+ /**
+ * Search the document
+ * @param {String} query What to search for
+ * @param {Object} options The following options are accepted:
+ * - searchMethod {String} one of WalkerSearch.SEARCH_METHOD_*
+ * defaults to WalkerSearch.SEARCH_METHOD_CONTAINS (does not apply to
+ * selector search type)
+ * - types {Array} a list of things to search for (tag, text, attributes, etc)
+ * defaults to WalkerSearch.ALL_RESULTS_TYPES
+ * @return {Array} An array is returned with each item being an object like:
+ * {
+ * node: <the dom node that matched>,
+ * type: <the type of match: one of WalkerSearch.ALL_RESULTS_TYPES>
+ * }
+ */
+ search: function (query, options = {}) {
+ options.searchMethod = options.searchMethod || WalkerSearch.SEARCH_METHOD_CONTAINS;
+ options.types = options.types || WalkerSearch.ALL_RESULTS_TYPES;
+
+ // Empty strings will return no results, as will non-string input
+ if (typeof query !== "string") {
+ query = "";
+ }
+
+ // Store results in a map indexed by nodes to avoid duplicate results
+ let results = new Map();
+
+ // Search through the indexed data
+ this._searchIndex(query, options, results);
+
+ // Search with querySelectorAll
+ this._searchSelectors(query, options, results);
+
+ // Concatenate all results into an Array to return
+ let resultList = [];
+ for (let [node, matches] of results) {
+ for (let {type} of matches) {
+ resultList.push({
+ node: node,
+ type: type,
+ });
+
+ // For now, just do one result per node since the frontend
+ // doesn't have a way to highlight each result individually
+ // yet.
+ break;
+ }
+ }
+
+ let documents = this.walker.tabActor.windows.map(win=>win.document);
+
+ // Sort the resulting nodes by order of appearance in the DOM
+ resultList.sort((a, b) => {
+ // Disconnected nodes won't get good results from compareDocumentPosition
+ // so check the order of their document instead.
+ if (a.node.ownerDocument != b.node.ownerDocument) {
+ let indA = documents.indexOf(a.node.ownerDocument);
+ let indB = documents.indexOf(b.node.ownerDocument);
+ return indA - indB;
+ }
+ // If the same document, then sort on DOCUMENT_POSITION_FOLLOWING (4)
+ // which means B is after A.
+ return a.node.compareDocumentPosition(b.node) & 4 ? -1 : 1;
+ });
+
+ return resultList;
+ }
+};
+
+WalkerSearch.SEARCH_METHOD_CONTAINS = (query, candidate) => {
+ return query && candidate.toLowerCase().indexOf(query.toLowerCase()) !== -1;
+};
+
+WalkerSearch.ALL_RESULTS_TYPES = ["tag", "text", "attributeName",
+ "attributeValue", "selector"];
+
+exports.WalkerSearch = WalkerSearch;
diff --git a/devtools/server/actors/utils/webconsole-utils.js b/devtools/server/actors/utils/webconsole-utils.js
new file mode 100644
index 000000000..597f1ddb3
--- /dev/null
+++ b/devtools/server/actors/utils/webconsole-utils.js
@@ -0,0 +1,1063 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft= javascript ts=2 et sw=2 tw=80: */
+/* 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 {Cc, Ci, Cu, components} = require("chrome");
+const {isWindowIncluded} = require("devtools/shared/layout/utils");
+const Services = require("Services");
+const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
+
+// TODO: Bug 842672 - browser/ imports modules from toolkit/.
+// Note that these are only used in WebConsoleCommands, see $0 and pprint().
+loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this,
+ "swm",
+ "@mozilla.org/serviceworkers/manager;1",
+ "nsIServiceWorkerManager");
+
+const CONSOLE_WORKER_IDS = exports.CONSOLE_WORKER_IDS = [
+ "SharedWorker",
+ "ServiceWorker",
+ "Worker"
+];
+
+var WebConsoleUtils = {
+
+ /**
+ * Given a message, return one of CONSOLE_WORKER_IDS if it matches
+ * one of those.
+ *
+ * @return string
+ */
+ getWorkerType: function (message) {
+ let id = message ? message.innerID : null;
+ return CONSOLE_WORKER_IDS[CONSOLE_WORKER_IDS.indexOf(id)] || null;
+ },
+
+ /**
+ * Clone an object.
+ *
+ * @param object object
+ * The object you want cloned.
+ * @param boolean recursive
+ * Tells if you want to dig deeper into the object, to clone
+ * recursively.
+ * @param function [filter]
+ * Optional, filter function, called for every property. Three
+ * arguments are passed: key, value and object. Return true if the
+ * property should be added to the cloned object. Return false to skip
+ * the property.
+ * @return object
+ * The cloned object.
+ */
+ cloneObject: function (object, recursive, filter) {
+ if (typeof object != "object") {
+ return object;
+ }
+
+ let temp;
+
+ if (Array.isArray(object)) {
+ temp = [];
+ Array.forEach(object, function (value, index) {
+ if (!filter || filter(index, value, object)) {
+ temp.push(recursive ? WebConsoleUtils.cloneObject(value) : value);
+ }
+ });
+ } else {
+ temp = {};
+ for (let key in object) {
+ let value = object[key];
+ if (object.hasOwnProperty(key) &&
+ (!filter || filter(key, value, object))) {
+ temp[key] = recursive ? WebConsoleUtils.cloneObject(value) : value;
+ }
+ }
+ }
+
+ return temp;
+ },
+
+ /**
+ * Gets the ID of the inner window of this DOM window.
+ *
+ * @param nsIDOMWindow window
+ * @return integer
+ * Inner ID for the given window.
+ */
+ getInnerWindowId: function (window) {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
+ },
+
+ /**
+ * Recursively gather a list of inner window ids given a
+ * top level window.
+ *
+ * @param nsIDOMWindow window
+ * @return Array
+ * list of inner window ids.
+ */
+ getInnerWindowIDsForFrames: function (window) {
+ let innerWindowID = this.getInnerWindowId(window);
+ let ids = [innerWindowID];
+
+ if (window.frames) {
+ for (let i = 0; i < window.frames.length; i++) {
+ let frame = window.frames[i];
+ ids = ids.concat(this.getInnerWindowIDsForFrames(frame));
+ }
+ }
+
+ return ids;
+ },
+
+ /**
+ * Get the property descriptor for the given object.
+ *
+ * @param object object
+ * The object that contains the property.
+ * @param string prop
+ * The property you want to get the descriptor for.
+ * @return object
+ * Property descriptor.
+ */
+ getPropertyDescriptor: function (object, prop) {
+ let desc = null;
+ while (object) {
+ try {
+ if ((desc = Object.getOwnPropertyDescriptor(object, prop))) {
+ break;
+ }
+ } catch (ex) {
+ // Native getters throw here. See bug 520882.
+ // null throws TypeError.
+ if (ex.name != "NS_ERROR_XPC_BAD_CONVERT_JS" &&
+ ex.name != "NS_ERROR_XPC_BAD_OP_ON_WN_PROTO" &&
+ ex.name != "TypeError") {
+ throw ex;
+ }
+ }
+
+ try {
+ object = Object.getPrototypeOf(object);
+ } catch (ex) {
+ if (ex.name == "TypeError") {
+ return desc;
+ }
+ throw ex;
+ }
+ }
+ return desc;
+ },
+
+ /**
+ * Create a grip for the given value. If the value is an object,
+ * an object wrapper will be created.
+ *
+ * @param mixed value
+ * The value you want to create a grip for, before sending it to the
+ * client.
+ * @param function objectWrapper
+ * If the value is an object then the objectWrapper function is
+ * invoked to give us an object grip. See this.getObjectGrip().
+ * @return mixed
+ * The value grip.
+ */
+ createValueGrip: function (value, objectWrapper) {
+ switch (typeof value) {
+ case "boolean":
+ return value;
+ case "string":
+ return objectWrapper(value);
+ case "number":
+ if (value === Infinity) {
+ return { type: "Infinity" };
+ } else if (value === -Infinity) {
+ return { type: "-Infinity" };
+ } else if (Number.isNaN(value)) {
+ return { type: "NaN" };
+ } else if (!value && 1 / value === -Infinity) {
+ return { type: "-0" };
+ }
+ return value;
+ case "undefined":
+ return { type: "undefined" };
+ case "object":
+ if (value === null) {
+ return { type: "null" };
+ }
+ // Fall through.
+ case "function":
+ return objectWrapper(value);
+ default:
+ console.error("Failed to provide a grip for value of " + typeof value
+ + ": " + value);
+ return null;
+ }
+ },
+};
+
+exports.Utils = WebConsoleUtils;
+
+// The page errors listener
+
+/**
+ * The nsIConsoleService listener. This is used to send all of the console
+ * messages (JavaScript, CSS and more) to the remote Web Console instance.
+ *
+ * @constructor
+ * @param nsIDOMWindow [window]
+ * Optional - the window object for which we are created. This is used
+ * for filtering out messages that belong to other windows.
+ * @param object listener
+ * The listener object must have one method:
+ * - onConsoleServiceMessage(). This method is invoked with one argument,
+ * the nsIConsoleMessage, whenever a relevant message is received.
+ */
+function ConsoleServiceListener(window, listener) {
+ this.window = window;
+ this.listener = listener;
+}
+exports.ConsoleServiceListener = ConsoleServiceListener;
+
+ConsoleServiceListener.prototype =
+{
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleListener]),
+
+ /**
+ * The content window for which we listen to page errors.
+ * @type nsIDOMWindow
+ */
+ window: null,
+
+ /**
+ * The listener object which is notified of messages from the console service.
+ * @type object
+ */
+ listener: null,
+
+ /**
+ * Initialize the nsIConsoleService listener.
+ */
+ init: function () {
+ Services.console.registerListener(this);
+ },
+
+ /**
+ * The nsIConsoleService observer. This method takes all the script error
+ * messages belonging to the current window and sends them to the remote Web
+ * Console instance.
+ *
+ * @param nsIConsoleMessage message
+ * The message object coming from the nsIConsoleService.
+ */
+ observe: function (message) {
+ if (!this.listener) {
+ return;
+ }
+
+ if (this.window) {
+ if (!(message instanceof Ci.nsIScriptError) ||
+ !message.outerWindowID ||
+ !this.isCategoryAllowed(message.category)) {
+ return;
+ }
+
+ let errorWindow = Services.wm.getOuterWindowWithId(message.outerWindowID);
+ if (!errorWindow || !isWindowIncluded(this.window, errorWindow)) {
+ return;
+ }
+ }
+
+ this.listener.onConsoleServiceMessage(message);
+ },
+
+ /**
+ * Check if the given message category is allowed to be tracked or not.
+ * We ignore chrome-originating errors as we only care about content.
+ *
+ * @param string category
+ * The message category you want to check.
+ * @return boolean
+ * True if the category is allowed to be logged, false otherwise.
+ */
+ isCategoryAllowed: function (category) {
+ if (!category) {
+ return false;
+ }
+
+ switch (category) {
+ case "XPConnect JavaScript":
+ case "component javascript":
+ case "chrome javascript":
+ case "chrome registration":
+ case "XBL":
+ case "XBL Prototype Handler":
+ case "XBL Content Sink":
+ case "xbl javascript":
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Get the cached page errors for the current inner window and its (i)frames.
+ *
+ * @param boolean [includePrivate=false]
+ * Tells if you want to also retrieve messages coming from private
+ * windows. Defaults to false.
+ * @return array
+ * The array of cached messages. Each element is an nsIScriptError or
+ * an nsIConsoleMessage
+ */
+ getCachedMessages: function (includePrivate = false) {
+ let errors = Services.console.getMessageArray() || [];
+
+ // if !this.window, we're in a browser console. Still need to filter
+ // private messages.
+ if (!this.window) {
+ return errors.filter((error) => {
+ if (error instanceof Ci.nsIScriptError) {
+ if (!includePrivate && error.isFromPrivateWindow) {
+ return false;
+ }
+ }
+
+ return true;
+ });
+ }
+
+ let ids = WebConsoleUtils.getInnerWindowIDsForFrames(this.window);
+
+ return errors.filter((error) => {
+ if (error instanceof Ci.nsIScriptError) {
+ if (!includePrivate && error.isFromPrivateWindow) {
+ return false;
+ }
+ if (ids &&
+ (ids.indexOf(error.innerWindowID) == -1 ||
+ !this.isCategoryAllowed(error.category))) {
+ return false;
+ }
+ } else if (ids && ids[0]) {
+ // If this is not an nsIScriptError and we need to do window-based
+ // filtering we skip this message.
+ return false;
+ }
+
+ return true;
+ });
+ },
+
+ /**
+ * Remove the nsIConsoleService listener.
+ */
+ destroy: function () {
+ Services.console.unregisterListener(this);
+ this.listener = this.window = null;
+ },
+};
+
+// The window.console API observer
+
+/**
+ * The window.console API observer. This allows the window.console API messages
+ * to be sent to the remote Web Console instance.
+ *
+ * @constructor
+ * @param nsIDOMWindow window
+ * Optional - the window object for which we are created. This is used
+ * for filtering out messages that belong to other windows.
+ * @param object owner
+ * The owner object must have the following methods:
+ * - onConsoleAPICall(). This method is invoked with one argument, the
+ * Console API message that comes from the observer service, whenever
+ * a relevant console API call is received.
+ * @param object filteringOptions
+ * Optional - The filteringOptions that this listener should listen to:
+ * - addonId: filter console messages based on the addonId.
+ */
+function ConsoleAPIListener(window, owner, {addonId} = {}) {
+ this.window = window;
+ this.owner = owner;
+ this.addonId = addonId;
+}
+exports.ConsoleAPIListener = ConsoleAPIListener;
+
+ConsoleAPIListener.prototype =
+{
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ /**
+ * The content window for which we listen to window.console API calls.
+ * @type nsIDOMWindow
+ */
+ window: null,
+
+ /**
+ * The owner object which is notified of window.console API calls. It must
+ * have a onConsoleAPICall method which is invoked with one argument: the
+ * console API call object that comes from the observer service.
+ *
+ * @type object
+ * @see WebConsoleActor
+ */
+ owner: null,
+
+ /**
+ * The addonId that we listen for. If not null then only messages from this
+ * console will be returned.
+ */
+ addonId: null,
+
+ /**
+ * Initialize the window.console API observer.
+ */
+ init: function () {
+ // Note that the observer is process-wide. We will filter the messages as
+ // needed, see CAL_observe().
+ Services.obs.addObserver(this, "console-api-log-event", false);
+ },
+
+ /**
+ * The console API message observer. When messages are received from the
+ * observer service we forward them to the remote Web Console instance.
+ *
+ * @param object message
+ * The message object receives from the observer service.
+ * @param string topic
+ * The message topic received from the observer service.
+ */
+ observe: function (message, topic) {
+ if (!this.owner) {
+ return;
+ }
+
+ // Here, wrappedJSObject is not a security wrapper but a property defined
+ // by the XPCOM component which allows us to unwrap the XPCOM interface and
+ // access the underlying JSObject.
+ let apiMessage = message.wrappedJSObject;
+
+ if (!this.isMessageRelevant(apiMessage)) {
+ return;
+ }
+
+ this.owner.onConsoleAPICall(apiMessage);
+ },
+
+ /**
+ * Given a message, return true if this window should show it and false
+ * if it should be ignored.
+ *
+ * @param message
+ * The message from the Storage Service
+ * @return bool
+ * Do we care about this message?
+ */
+ isMessageRelevant: function (message) {
+ let workerType = WebConsoleUtils.getWorkerType(message);
+
+ if (this.window && workerType === "ServiceWorker") {
+ // For messages from Service Workers, message.ID is the
+ // scope, which can be used to determine whether it's controlling
+ // a window.
+ let scope = message.ID;
+
+ if (!swm.shouldReportToWindow(this.window, scope)) {
+ return false;
+ }
+ }
+
+ if (this.window && !workerType) {
+ let msgWindow = Services.wm.getCurrentInnerWindowWithId(message.innerID);
+ if (!msgWindow || !isWindowIncluded(this.window, msgWindow)) {
+ // Not the same window!
+ return false;
+ }
+ }
+
+ if (this.addonId) {
+ // ConsoleAPI.jsm messages contains a consoleID, (and it is currently
+ // used in Addon SDK add-ons), the standard 'console' object
+ // (which is used in regular webpages and in WebExtensions pages)
+ // contains the originAttributes of the source document principal.
+
+ // Filtering based on the originAttributes used by
+ // the Console API object.
+ if (message.originAttributes &&
+ message.originAttributes.addonId == this.addonId) {
+ return true;
+ }
+
+ // Filtering based on the old-style consoleID property used by
+ // the legacy Console JSM module.
+ if (message.consoleID && message.consoleID == `addon/${this.addonId}`) {
+ return true;
+ }
+
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Get the cached messages for the current inner window and its (i)frames.
+ *
+ * @param boolean [includePrivate=false]
+ * Tells if you want to also retrieve messages coming from private
+ * windows. Defaults to false.
+ * @return array
+ * The array of cached messages.
+ */
+ getCachedMessages: function (includePrivate = false) {
+ let messages = [];
+ let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"]
+ .getService(Ci.nsIConsoleAPIStorage);
+
+ // if !this.window, we're in a browser console. Retrieve all events
+ // for filtering based on privacy.
+ if (!this.window) {
+ messages = ConsoleAPIStorage.getEvents();
+ } else {
+ let ids = WebConsoleUtils.getInnerWindowIDsForFrames(this.window);
+ ids.forEach((id) => {
+ messages = messages.concat(ConsoleAPIStorage.getEvents(id));
+ });
+ }
+
+ CONSOLE_WORKER_IDS.forEach((id) => {
+ messages = messages.concat(ConsoleAPIStorage.getEvents(id));
+ });
+
+ messages = messages.filter(msg => {
+ return this.isMessageRelevant(msg);
+ });
+
+ if (includePrivate) {
+ return messages;
+ }
+
+ return messages.filter((m) => !m.private);
+ },
+
+ /**
+ * Destroy the console API listener.
+ */
+ destroy: function () {
+ Services.obs.removeObserver(this, "console-api-log-event");
+ this.window = this.owner = null;
+ },
+};
+
+/**
+ * WebConsole commands manager.
+ *
+ * Defines a set of functions /variables ("commands") that are available from
+ * the Web Console but not from the web page.
+ *
+ */
+var WebConsoleCommands = {
+ _registeredCommands: new Map(),
+ _originalCommands: new Map(),
+
+ /**
+ * @private
+ * Reserved for built-in commands. To register a command from the code of an
+ * add-on, see WebConsoleCommands.register instead.
+ *
+ * @see WebConsoleCommands.register
+ */
+ _registerOriginal: function (name, command) {
+ this.register(name, command);
+ this._originalCommands.set(name, this.getCommand(name));
+ },
+
+ /**
+ * Register a new command.
+ * @param {string} name The command name (exemple: "$")
+ * @param {(function|object)} command The command to register.
+ * It can be a function so the command is a function (like "$()"),
+ * or it can also be a property descriptor to describe a getter / value (like
+ * "$0").
+ *
+ * The command function or the command getter are passed a owner object as
+ * their first parameter (see the example below).
+ *
+ * Note that setters don't work currently and "enumerable" and "configurable"
+ * are forced to true.
+ *
+ * @example
+ *
+ * WebConsoleCommands.register("$", function JSTH_$(owner, selector)
+ * {
+ * return owner.window.document.querySelector(selector);
+ * });
+ *
+ * WebConsoleCommands.register("$0", {
+ * get: function(owner) {
+ * return owner.makeDebuggeeValue(owner.selectedNode);
+ * }
+ * });
+ */
+ register: function (name, command) {
+ this._registeredCommands.set(name, command);
+ },
+
+ /**
+ * Unregister a command.
+ *
+ * If the command being unregister overrode a built-in command,
+ * the latter is restored.
+ *
+ * @param {string} name The name of the command
+ */
+ unregister: function (name) {
+ this._registeredCommands.delete(name);
+ if (this._originalCommands.has(name)) {
+ this.register(name, this._originalCommands.get(name));
+ }
+ },
+
+ /**
+ * Returns a command by its name.
+ *
+ * @param {string} name The name of the command.
+ *
+ * @return {(function|object)} The command.
+ */
+ getCommand: function (name) {
+ return this._registeredCommands.get(name);
+ },
+
+ /**
+ * Returns true if a command is registered with the given name.
+ *
+ * @param {string} name The name of the command.
+ *
+ * @return {boolean} True if the command is registered.
+ */
+ hasCommand: function (name) {
+ return this._registeredCommands.has(name);
+ },
+};
+
+exports.WebConsoleCommands = WebConsoleCommands;
+
+/*
+ * Built-in commands.
+ *
+ * A list of helper functions used by Firebug can be found here:
+ * http://getfirebug.com/wiki/index.php/Command_Line_API
+ */
+
+/**
+ * Find a node by ID.
+ *
+ * @param string id
+ * The ID of the element you want.
+ * @return nsIDOMNode or null
+ * The result of calling document.querySelector(selector).
+ */
+WebConsoleCommands._registerOriginal("$", function (owner, selector) {
+ return owner.window.document.querySelector(selector);
+});
+
+/**
+ * Find the nodes matching a CSS selector.
+ *
+ * @param string selector
+ * A string that is passed to window.document.querySelectorAll.
+ * @return nsIDOMNodeList
+ * Returns the result of document.querySelectorAll(selector).
+ */
+WebConsoleCommands._registerOriginal("$$", function (owner, selector) {
+ let nodes = owner.window.document.querySelectorAll(selector);
+
+ // Calling owner.window.Array.from() doesn't work without accessing the
+ // wrappedJSObject, so just loop through the results instead.
+ let result = new owner.window.Array();
+ for (let i = 0; i < nodes.length; i++) {
+ result.push(nodes[i]);
+ }
+ return result;
+});
+
+/**
+ * Returns the result of the last console input evaluation
+ *
+ * @return object|undefined
+ * Returns last console evaluation or undefined
+ */
+WebConsoleCommands._registerOriginal("$_", {
+ get: function (owner) {
+ return owner.consoleActor.getLastConsoleInputEvaluation();
+ }
+});
+
+/**
+ * Runs an xPath query and returns all matched nodes.
+ *
+ * @param string xPath
+ * xPath search query to execute.
+ * @param [optional] nsIDOMNode context
+ * Context to run the xPath query on. Uses window.document if not set.
+ * @return array of nsIDOMNode
+ */
+WebConsoleCommands._registerOriginal("$x", function (owner, xPath, context) {
+ let nodes = new owner.window.Array();
+
+ // Not waiving Xrays, since we want the original Document.evaluate function,
+ // instead of anything that's been redefined.
+ let doc = owner.window.document;
+ context = context || doc;
+
+ let results = doc.evaluate(xPath, context, null,
+ Ci.nsIDOMXPathResult.ANY_TYPE, null);
+ let node;
+ while ((node = results.iterateNext())) {
+ nodes.push(node);
+ }
+
+ return nodes;
+});
+
+/**
+ * Returns the currently selected object in the highlighter.
+ *
+ * @return Object representing the current selection in the
+ * Inspector, or null if no selection exists.
+ */
+WebConsoleCommands._registerOriginal("$0", {
+ get: function (owner) {
+ return owner.makeDebuggeeValue(owner.selectedNode);
+ }
+});
+
+/**
+ * Clears the output of the WebConsole.
+ */
+WebConsoleCommands._registerOriginal("clear", function (owner) {
+ owner.helperResult = {
+ type: "clearOutput",
+ };
+});
+
+/**
+ * Clears the input history of the WebConsole.
+ */
+WebConsoleCommands._registerOriginal("clearHistory", function (owner) {
+ owner.helperResult = {
+ type: "clearHistory",
+ };
+});
+
+/**
+ * Returns the result of Object.keys(object).
+ *
+ * @param object object
+ * Object to return the property names from.
+ * @return array of strings
+ */
+WebConsoleCommands._registerOriginal("keys", function (owner, object) {
+ // Need to waive Xrays so we can iterate functions and accessor properties
+ return Cu.cloneInto(Object.keys(Cu.waiveXrays(object)), owner.window);
+});
+
+/**
+ * Returns the values of all properties on object.
+ *
+ * @param object object
+ * Object to display the values from.
+ * @return array of string
+ */
+WebConsoleCommands._registerOriginal("values", function (owner, object) {
+ let values = [];
+ // Need to waive Xrays so we can iterate functions and accessor properties
+ let waived = Cu.waiveXrays(object);
+ let names = Object.getOwnPropertyNames(waived);
+
+ for (let name of names) {
+ values.push(waived[name]);
+ }
+
+ return Cu.cloneInto(values, owner.window);
+});
+
+/**
+ * Opens a help window in MDN.
+ */
+WebConsoleCommands._registerOriginal("help", function (owner) {
+ owner.helperResult = { type: "help" };
+});
+
+/**
+ * Change the JS evaluation scope.
+ *
+ * @param DOMElement|string|window window
+ * The window object to use for eval scope. This can be a string that
+ * is used to perform document.querySelector(), to find the iframe that
+ * you want to cd() to. A DOMElement can be given as well, the
+ * .contentWindow property is used. Lastly, you can directly pass
+ * a window object. If you call cd() with no arguments, the current
+ * eval scope is cleared back to its default (the top window).
+ */
+WebConsoleCommands._registerOriginal("cd", function (owner, window) {
+ if (!window) {
+ owner.consoleActor.evalWindow = null;
+ owner.helperResult = { type: "cd" };
+ return;
+ }
+
+ if (typeof window == "string") {
+ window = owner.window.document.querySelector(window);
+ }
+ if (window instanceof Ci.nsIDOMElement && window.contentWindow) {
+ window = window.contentWindow;
+ }
+ if (!(window instanceof Ci.nsIDOMWindow)) {
+ owner.helperResult = {
+ type: "error",
+ message: "cdFunctionInvalidArgument"
+ };
+ return;
+ }
+
+ owner.consoleActor.evalWindow = window;
+ owner.helperResult = { type: "cd" };
+});
+
+/**
+ * Inspects the passed object. This is done by opening the PropertyPanel.
+ *
+ * @param object object
+ * Object to inspect.
+ */
+WebConsoleCommands._registerOriginal("inspect", function (owner, object) {
+ let dbgObj = owner.makeDebuggeeValue(object);
+ let grip = owner.createValueGrip(dbgObj);
+ owner.helperResult = {
+ type: "inspectObject",
+ input: owner.evalInput,
+ object: grip,
+ };
+});
+
+/**
+ * Prints object to the output.
+ *
+ * @param object object
+ * Object to print to the output.
+ * @return string
+ */
+WebConsoleCommands._registerOriginal("pprint", function (owner, object) {
+ if (object === null || object === undefined || object === true ||
+ object === false) {
+ owner.helperResult = {
+ type: "error",
+ message: "helperFuncUnsupportedTypeError",
+ };
+ return null;
+ }
+
+ owner.helperResult = { rawOutput: true };
+
+ if (typeof object == "function") {
+ return object + "\n";
+ }
+
+ let output = [];
+
+ let obj = object;
+ for (let name in obj) {
+ let desc = WebConsoleUtils.getPropertyDescriptor(obj, name) || {};
+ if (desc.get || desc.set) {
+ // TODO: Bug 842672 - toolkit/ imports modules from browser/.
+ let getGrip = VariablesView.getGrip(desc.get);
+ let setGrip = VariablesView.getGrip(desc.set);
+ let getString = VariablesView.getString(getGrip);
+ let setString = VariablesView.getString(setGrip);
+ output.push(name + ":", " get: " + getString, " set: " + setString);
+ } else {
+ let valueGrip = VariablesView.getGrip(obj[name]);
+ let valueString = VariablesView.getString(valueGrip);
+ output.push(name + ": " + valueString);
+ }
+ }
+
+ return " " + output.join("\n ");
+});
+
+/**
+ * Print the String representation of a value to the output, as-is.
+ *
+ * @param any value
+ * A value you want to output as a string.
+ * @return void
+ */
+WebConsoleCommands._registerOriginal("print", function (owner, value) {
+ owner.helperResult = { rawOutput: true };
+ if (typeof value === "symbol") {
+ return Symbol.prototype.toString.call(value);
+ }
+ // Waiving Xrays here allows us to see a closer representation of the
+ // underlying object. This may execute arbitrary content code, but that
+ // code will run with content privileges, and the result will be rendered
+ // inert by coercing it to a String.
+ return String(Cu.waiveXrays(value));
+});
+
+/**
+ * Copy the String representation of a value to the clipboard.
+ *
+ * @param any value
+ * A value you want to copy as a string.
+ * @return void
+ */
+WebConsoleCommands._registerOriginal("copy", function (owner, value) {
+ let payload;
+ try {
+ if (value instanceof Ci.nsIDOMElement) {
+ payload = value.outerHTML;
+ } else if (typeof value == "string") {
+ payload = value;
+ } else {
+ payload = JSON.stringify(value, null, " ");
+ }
+ } catch (ex) {
+ payload = "/* " + ex + " */";
+ }
+ owner.helperResult = {
+ type: "copyValueToClipboard",
+ value: payload,
+ };
+});
+
+/**
+ * (Internal only) Add the bindings to |owner.sandbox|.
+ * This is intended to be used by the WebConsole actor only.
+ *
+ * @param object owner
+ * The owning object.
+ */
+function addWebConsoleCommands(owner) {
+ if (!owner) {
+ throw new Error("The owner is required");
+ }
+ for (let [name, command] of WebConsoleCommands._registeredCommands) {
+ if (typeof command === "function") {
+ owner.sandbox[name] = command.bind(undefined, owner);
+ } else if (typeof command === "object") {
+ let clone = Object.assign({}, command, {
+ // We force the enumerability and the configurability (so the
+ // WebConsoleActor can reconfigure the property).
+ enumerable: true,
+ configurable: true
+ });
+
+ if (typeof command.get === "function") {
+ clone.get = command.get.bind(undefined, owner);
+ }
+ if (typeof command.set === "function") {
+ clone.set = command.set.bind(undefined, owner);
+ }
+
+ Object.defineProperty(owner.sandbox, name, clone);
+ }
+ }
+}
+
+exports.addWebConsoleCommands = addWebConsoleCommands;
+
+/**
+ * A ReflowObserver that listens for reflow events from the page.
+ * Implements nsIReflowObserver.
+ *
+ * @constructor
+ * @param object window
+ * The window for which we need to track reflow.
+ * @param object owner
+ * The listener owner which needs to implement:
+ * - onReflowActivity(reflowInfo)
+ */
+
+function ConsoleReflowListener(window, listener) {
+ this.docshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ this.listener = listener;
+ this.docshell.addWeakReflowObserver(this);
+}
+
+exports.ConsoleReflowListener = ConsoleReflowListener;
+
+ConsoleReflowListener.prototype =
+{
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver,
+ Ci.nsISupportsWeakReference]),
+ docshell: null,
+ listener: null,
+
+ /**
+ * Forward reflow event to listener.
+ *
+ * @param DOMHighResTimeStamp start
+ * @param DOMHighResTimeStamp end
+ * @param boolean interruptible
+ */
+ sendReflow: function (start, end, interruptible) {
+ let frame = components.stack.caller.caller;
+
+ let filename = frame ? frame.filename : null;
+
+ if (filename) {
+ // Because filename could be of the form "xxx.js -> xxx.js -> xxx.js",
+ // we only take the last part.
+ filename = filename.split(" ").pop();
+ }
+
+ this.listener.onReflowActivity({
+ interruptible: interruptible,
+ start: start,
+ end: end,
+ sourceURL: filename,
+ sourceLine: frame ? frame.lineNumber : null,
+ functionName: frame ? frame.name : null
+ });
+ },
+
+ /**
+ * On uninterruptible reflow
+ *
+ * @param DOMHighResTimeStamp start
+ * @param DOMHighResTimeStamp end
+ */
+ reflow: function (start, end) {
+ this.sendReflow(start, end, false);
+ },
+
+ /**
+ * On interruptible reflow
+ *
+ * @param DOMHighResTimeStamp start
+ * @param DOMHighResTimeStamp end
+ */
+ reflowInterruptible: function (start, end) {
+ this.sendReflow(start, end, true);
+ },
+
+ /**
+ * Unregister listener.
+ */
+ destroy: function () {
+ this.docshell.removeWeakReflowObserver(this);
+ this.listener = this.docshell = null;
+ },
+};
diff --git a/devtools/server/actors/utils/webconsole-worker-utils.js b/devtools/server/actors/utils/webconsole-worker-utils.js
new file mode 100644
index 000000000..0c1142967
--- /dev/null
+++ b/devtools/server/actors/utils/webconsole-worker-utils.js
@@ -0,0 +1,20 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft= javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+// XXXworkers This file is loaded on the server side for worker debugging.
+// Since the server is running in the worker thread, it doesn't
+// have access to Services / Components. This functionality
+// is stubbed out to prevent errors, and will need to implemented
+// for Bug 1209353.
+
+exports.Utils = { L10n: function () {} };
+exports.ConsoleServiceListener = function () {};
+exports.ConsoleAPIListener = function () {};
+exports.addWebConsoleCommands = function () {};
+exports.ConsoleReflowListener = function () {};
+exports.CONSOLE_WORKER_IDS = [];