fune/devtools/server/actors/utils/webconsole-utils.js
2016-12-23 16:45:22 +01:00

621 lines
17 KiB
JavaScript

/* -*- 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 {Ci, Cu} = require("chrome");
// Note that this is only used in WebConsoleCommands, see $0 and pprint().
if (!isWorker) {
loader.lazyImporter(this, "VariablesView", "resource://devtools/client/shared/widgets/VariablesView.jsm");
}
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.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 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) {
// 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.
let commands = isWorker ? [] : WebConsoleCommands._registeredCommands;
if (!owner) {
throw new Error("The owner is required");
}
for (let [name, command] of commands) {
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;