forked from mirrors/gecko-dev
It has some properties which make it footgunny, especially in the face of Fission. Callers should use WindowGlobalChild.innerWindowId instead. Differential Revision: https://phabricator.services.mozilla.com/D82801
742 lines
20 KiB
JavaScript
742 lines
20 KiB
JavaScript
/* 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, Cc } = require("chrome");
|
|
|
|
// Note that this is only used in WebConsoleCommands, see $0 and screenshot.
|
|
if (!isWorker) {
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"captureScreenshot",
|
|
"devtools/shared/screenshot/capture",
|
|
true
|
|
);
|
|
}
|
|
|
|
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) {
|
|
const 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 = [];
|
|
object.forEach(function(value, index) {
|
|
if (!filter || filter(index, value, object)) {
|
|
temp.push(recursive ? WebConsoleUtils.cloneObject(value) : value);
|
|
}
|
|
});
|
|
} else {
|
|
temp = {};
|
|
for (const key in object) {
|
|
const 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|null
|
|
* Inner ID for the given window, null if we can't access it.
|
|
*/
|
|
getInnerWindowId: function(window) {
|
|
// Might throw with SecurityError: Permission denied to access property
|
|
// "windowGlobalChild" on cross-origin object.
|
|
try {
|
|
return window.windowGlobalChild.innerWindowId;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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) {
|
|
const innerWindowID = this.getInnerWindowId(window);
|
|
if (innerWindowID === null) {
|
|
return [];
|
|
}
|
|
|
|
let ids = [innerWindowID];
|
|
|
|
if (window.frames) {
|
|
for (let i = 0; i < window.frames.length; i++) {
|
|
const frame = window.frames[i];
|
|
ids = ids.concat(this.getInnerWindowIDsForFrames(frame));
|
|
}
|
|
}
|
|
|
|
return ids;
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Remove any frames in a stack that are above a debugger-triggered evaluation
|
|
* and will correspond with devtools server code, which we never want to show
|
|
* to the user.
|
|
*
|
|
* @param array stack
|
|
* An array of frames, with the topmost first, and each of which has a
|
|
* 'filename' property.
|
|
* @return array
|
|
* An array of stack frames with any devtools server frames removed.
|
|
* The original array is not modified.
|
|
*/
|
|
removeFramesAboveDebuggerEval(stack) {
|
|
const debuggerEvalFilename = "debugger eval code";
|
|
|
|
// Remove any frames for server code above the last debugger eval frame.
|
|
const evalIndex = stack.findIndex(({ filename }, idx, arr) => {
|
|
const nextFrame = arr[idx + 1];
|
|
return (
|
|
filename == debuggerEvalFilename &&
|
|
(!nextFrame || nextFrame.filename !== debuggerEvalFilename)
|
|
);
|
|
});
|
|
if (evalIndex != -1) {
|
|
return stack.slice(0, evalIndex + 1);
|
|
}
|
|
|
|
// In some cases (e.g. evaluated expression with SyntaxError), we might not have a
|
|
// "debugger eval code" frame but still have internal ones. If that's the case, we
|
|
// return null as the end user shouldn't see those frames.
|
|
if (
|
|
stack.some(
|
|
({ filename }) =>
|
|
filename && filename.startsWith("resource://devtools/")
|
|
)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return stack;
|
|
},
|
|
};
|
|
|
|
exports.WebConsoleUtils = WebConsoleUtils;
|
|
|
|
/**
|
|
* 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 Node or null
|
|
* The result of calling document.querySelector(selector).
|
|
*/
|
|
WebConsoleCommands._registerOriginal("$", function(owner, selector) {
|
|
try {
|
|
return owner.window.document.querySelector(selector);
|
|
} catch (err) {
|
|
// Throw an error like `err` but that belongs to `owner.window`.
|
|
throw new owner.window.DOMException(err.message, err.name);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Find the nodes matching a CSS selector.
|
|
*
|
|
* @param string selector
|
|
* A string that is passed to window.document.querySelectorAll.
|
|
* @return NodeList
|
|
* Returns the result of document.querySelectorAll(selector).
|
|
*/
|
|
WebConsoleCommands._registerOriginal("$$", function(owner, selector) {
|
|
let nodes;
|
|
try {
|
|
nodes = owner.window.document.querySelectorAll(selector);
|
|
} catch (err) {
|
|
// Throw an error like `err` but that belongs to `owner.window`.
|
|
throw new owner.window.DOMException(err.message, err.name);
|
|
}
|
|
|
|
// Calling owner.window.Array.from() doesn't work without accessing the
|
|
// wrappedJSObject, so just loop through the results instead.
|
|
const 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] Node context
|
|
* Context to run the xPath query on. Uses window.document if not set.
|
|
* @param [optional] string|number resultType
|
|
Specify the result type. Default value XPathResult.ANY_TYPE
|
|
* @return array of Node
|
|
*/
|
|
WebConsoleCommands._registerOriginal("$x", function(
|
|
owner,
|
|
xPath,
|
|
context,
|
|
resultType = owner.window.XPathResult.ANY_TYPE
|
|
) {
|
|
const nodes = new owner.window.Array();
|
|
// Not waiving Xrays, since we want the original Document.evaluate function,
|
|
// instead of anything that's been redefined.
|
|
const doc = owner.window.document;
|
|
context = context || doc;
|
|
switch (resultType) {
|
|
case "number":
|
|
resultType = owner.window.XPathResult.NUMBER_TYPE;
|
|
break;
|
|
|
|
case "string":
|
|
resultType = owner.window.XPathResult.STRING_TYPE;
|
|
break;
|
|
|
|
case "bool":
|
|
resultType = owner.window.XPathResult.BOOLEAN_TYPE;
|
|
break;
|
|
|
|
case "node":
|
|
resultType = owner.window.XPathResult.FIRST_ORDERED_NODE_TYPE;
|
|
break;
|
|
|
|
case "nodes":
|
|
resultType = owner.window.XPathResult.UNORDERED_NODE_ITERATOR_TYPE;
|
|
break;
|
|
}
|
|
const results = doc.evaluate(xPath, context, null, resultType, null);
|
|
if (results.resultType === owner.window.XPathResult.NUMBER_TYPE) {
|
|
return results.numberValue;
|
|
}
|
|
if (results.resultType === owner.window.XPathResult.STRING_TYPE) {
|
|
return results.stringValue;
|
|
}
|
|
if (results.resultType === owner.window.XPathResult.BOOLEAN_TYPE) {
|
|
return results.booleanValue;
|
|
}
|
|
if (
|
|
results.resultType === owner.window.XPathResult.ANY_UNORDERED_NODE_TYPE ||
|
|
results.resultType === owner.window.XPathResult.FIRST_ORDERED_NODE_TYPE
|
|
) {
|
|
return results.singleNodeValue;
|
|
}
|
|
if (
|
|
results.resultType ===
|
|
owner.window.XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE ||
|
|
results.resultType === owner.window.XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
|
|
) {
|
|
for (let i = 0; i < results.snapshotLength; i++) {
|
|
nodes.push(results.snapshotItem(i));
|
|
}
|
|
return nodes;
|
|
}
|
|
|
|
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) {
|
|
const values = [];
|
|
// Need to waive Xrays so we can iterate functions and accessor properties
|
|
const waived = Cu.waiveXrays(object);
|
|
const names = Object.getOwnPropertyNames(waived);
|
|
|
|
for (const 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) {
|
|
// Log a deprecation warning.
|
|
const scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
|
|
const scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);
|
|
|
|
const deprecationMessage =
|
|
"The `cd` command will be disabled in a future release. " +
|
|
"See https://bugzilla.mozilla.org/show_bug.cgi?id=1605327 for more information.";
|
|
|
|
scriptError.initWithWindowID(
|
|
deprecationMessage,
|
|
null,
|
|
null,
|
|
0,
|
|
0,
|
|
1,
|
|
"content javascript",
|
|
owner.window.windowGlobalChild.innerWindowId
|
|
);
|
|
const Services = require("Services");
|
|
Services.console.logMessage(scriptError);
|
|
|
|
if (!window) {
|
|
owner.consoleActor.evalGlobal = null;
|
|
owner.helperResult = { type: "cd" };
|
|
return;
|
|
}
|
|
|
|
if (typeof window == "string") {
|
|
window = owner.window.document.querySelector(window);
|
|
}
|
|
if (Element.isInstance(window) && window.contentWindow) {
|
|
window = window.contentWindow;
|
|
}
|
|
if (!(window instanceof Ci.nsIDOMWindow)) {
|
|
owner.helperResult = {
|
|
type: "error",
|
|
message: "cdFunctionInvalidArgument",
|
|
};
|
|
return;
|
|
}
|
|
|
|
owner.consoleActor.evalGlobal = 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,
|
|
forceExpandInConsole = false
|
|
) {
|
|
const dbgObj = owner.preprocessDebuggerObject(
|
|
owner.makeDebuggeeValue(object)
|
|
);
|
|
|
|
const grip = owner.createValueGrip(dbgObj);
|
|
owner.helperResult = {
|
|
type: "inspectObject",
|
|
input: owner.evalInput,
|
|
object: grip,
|
|
forceExpandInConsole,
|
|
};
|
|
});
|
|
|
|
/**
|
|
* 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 (Element.isInstance(value)) {
|
|
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,
|
|
};
|
|
});
|
|
|
|
/**
|
|
* Take a screenshot of a page.
|
|
*
|
|
* @param object args
|
|
* The arguments to be passed to the screenshot
|
|
* @return void
|
|
*/
|
|
WebConsoleCommands._registerOriginal("screenshot", function(owner, args = {}) {
|
|
owner.helperResult = (async () => {
|
|
// creates data for saving the screenshot
|
|
// help is handled on the client side
|
|
const value = await captureScreenshot(args, owner.window.document);
|
|
return {
|
|
type: "screenshotOutput",
|
|
value,
|
|
// pass args through to the client, so that the client can take care of copying
|
|
// and saving the screenshot data on the client machine instead of on the
|
|
// remote machine
|
|
args,
|
|
};
|
|
})();
|
|
});
|
|
|
|
/**
|
|
* Block specific resource from loading
|
|
*
|
|
* @param object args
|
|
* an object with key "url", i.e. a filter
|
|
*
|
|
* @return void
|
|
*/
|
|
WebConsoleCommands._registerOriginal("block", function(owner, args = {}) {
|
|
if (!args.url) {
|
|
owner.helperResult = {
|
|
type: "error",
|
|
message: "webconsole.messages.commands.blockArgMissing",
|
|
};
|
|
return;
|
|
}
|
|
|
|
owner.helperResult = (async () => {
|
|
await owner.consoleActor.blockRequest(args);
|
|
|
|
return {
|
|
type: "blockURL",
|
|
args,
|
|
};
|
|
})();
|
|
});
|
|
|
|
/*
|
|
* Unblock a blocked a resource
|
|
*
|
|
* @param object filter
|
|
* an object with key "url", i.e. a filter
|
|
*
|
|
* @return void
|
|
*/
|
|
WebConsoleCommands._registerOriginal("unblock", function(owner, args = {}) {
|
|
if (!args.url) {
|
|
owner.helperResult = {
|
|
type: "error",
|
|
message: "webconsole.messages.commands.blockArgMissing",
|
|
};
|
|
return;
|
|
}
|
|
|
|
owner.helperResult = (async () => {
|
|
await owner.consoleActor.unblockRequest(args);
|
|
|
|
return {
|
|
type: "unblockURL",
|
|
args,
|
|
};
|
|
})();
|
|
});
|
|
|
|
/**
|
|
* (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) {
|
|
// Not supporting extra commands in workers yet. This should be possible to
|
|
// add one by one as long as they don't require jsm, Cu, etc.
|
|
const commands = isWorker ? [] : WebConsoleCommands._registeredCommands;
|
|
if (!owner) {
|
|
throw new Error("The owner is required");
|
|
}
|
|
for (const [name, command] of commands) {
|
|
if (typeof command === "function") {
|
|
owner.sandbox[name] = command.bind(undefined, owner);
|
|
} else if (typeof command === "object") {
|
|
const 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;
|