Bug 768096 - Web Console remote debugging protocol support - Part 2: window.console API and JS evaluation; r=past,robcee

This commit is contained in:
Mihai Sucan 2012-09-26 18:02:04 +01:00
parent 67c85e3411
commit fa385a4925
10 changed files with 1828 additions and 396 deletions

View File

@ -536,12 +536,8 @@ WebConsole.prototype = {
* @private
* @type array
*/
_messageListeners: ["JSTerm:EvalObject", "WebConsole:ConsoleAPI",
"WebConsole:CachedMessages", "WebConsole:Initialized", "JSTerm:EvalResult",
"JSTerm:AutocompleteProperties", "JSTerm:ClearOutput",
"JSTerm:InspectObject", "WebConsole:NetworkActivity",
"WebConsole:FileActivity", "WebConsole:LocationChange",
"JSTerm:NonNativeConsoleAPI"],
_messageListeners: ["WebConsole:Initialized", "WebConsole:NetworkActivity",
"WebConsole:FileActivity", "WebConsole:LocationChange"],
/**
* The xul:panel that holds the Web Console when it is positioned as a window.
@ -901,10 +897,10 @@ WebConsole.prototype = {
/**
* The clear output button handler.
* @private
*/
onClearButton: function WC_onClearButton()
_onClearButton: function WC__onClearButton()
{
this.ui.jsterm.clearOutput(true);
this.chromeWindow.DeveloperToolbar.resetErrorsCount(this.tab);
},
@ -924,10 +920,8 @@ WebConsole.prototype = {
}, this);
let message = {
features: ["ConsoleAPI", "JSTerm", "NetworkMonitor", "LocationChange"],
cachedMessages: ["ConsoleAPI", "PageError"],
features: ["NetworkMonitor", "LocationChange"],
NetworkMonitor: { monitorFileActivity: true },
JSTerm: { notifyNonNativeConsoleAPI: true },
preferences: {
"NetworkMonitor.saveRequestAndResponseBodies":
this.ui.saveRequestAndResponseBodies,

View File

@ -27,7 +27,7 @@ var EXPORTED_SYMBOLS = ["PropertyPanel", "PropertyTreeView"];
*/
var PropertyTreeView = function() {
this._rows = [];
this._objectCache = {};
this._objectActors = [];
};
PropertyTreeView.prototype = {
@ -44,10 +44,24 @@ PropertyTreeView.prototype = {
_treeBox: null,
/**
* Stores cached information about local objects being inspected.
* Track known object actor IDs. We clean these when the panel is
* destroyed/cleaned up.
*
* @private
* @type array
*/
_objectCache: null,
_objectActors: null,
/**
* Map fake object actors to their IDs. This is used when we inspect local
* objects.
* @private
* @type Object
*/
_localObjectActors: null,
_releaseObject: null,
_objectPropertiesProvider: null,
/**
* Use this setter to update the content of the tree.
@ -58,54 +72,47 @@ PropertyTreeView.prototype = {
* - object:
* This is the raw object you want to display. You can only provide
* this object if you want the property panel to work in sync mode.
* - remoteObject:
* - objectProperties:
* An array that holds information on the remote object being
* inspected. Each element in this array describes each property in the
* remote object. See WebConsoleUtils.namesAndValuesOf() for details.
* - rootCacheId:
* The cache ID where the objects referenced in remoteObject are found.
* - panelCacheId:
* The cache ID where any object retrieved by this property panel
* instance should be stored into.
* - remoteObjectProvider:
* remote object. See WebConsoleUtils.inspectObject() for details.
* - objectPropertiesProvider:
* A function that is invoked when a new object is needed. This is
* called when the user tries to expand an inspectable property. The
* callback must take four arguments:
* - fromCacheId:
* Tells from where to retrieve the object the user picked (from
* which cache ID).
* - objectId:
* The object ID the user wants.
* - panelCacheId:
* Tells in which cache ID to store the objects referenced by
* objectId so they can be retrieved later.
* - actorID:
* The object actor ID from which we request the properties.
* - callback:
* The callback function to be invoked when the remote object is
* received. This function takes one argument: the raw message
* received from the Web Console content script.
* received. This function takes one argument: the array of
* descriptors for each property in the object represented by the
* actor.
* - releaseObject:
* Function to invoke when an object actor should be released. The
* function must take one argument: the object actor ID.
*/
set data(aData) {
let oldLen = this._rows.length;
this._cleanup();
this.cleanup();
if (!aData) {
return;
}
if (aData.remoteObject) {
this._rootCacheId = aData.rootCacheId;
this._panelCacheId = aData.panelCacheId;
this._remoteObjectProvider = aData.remoteObjectProvider;
this._rows = [].concat(aData.remoteObject);
this._updateRemoteObject(this._rows, 0);
if (aData.objectPropertiesProvider) {
this._objectPropertiesProvider = aData.objectPropertiesProvider;
this._releaseObject = aData.releaseObject;
this._propertiesToRows(aData.objectProperties, 0);
this._rows = aData.objectProperties;
}
else if (aData.object) {
this._localObjectActors = Object.create(null);
this._rows = this._inspectObject(aData.object);
}
else {
throw new Error("First argument must have a .remoteObject or " +
"an .object property!");
throw new Error("First argument must have an objectActor or an " +
"object property!");
}
if (this._treeBox) {
@ -128,13 +135,22 @@ PropertyTreeView.prototype = {
* @param number aLevel
* The level you want to give to each property in the remote object.
*/
_updateRemoteObject: function PTV__updateRemoteObject(aObject, aLevel)
_propertiesToRows: function PTV__propertiesToRows(aObject, aLevel)
{
aObject.forEach(function(aElement) {
aElement.level = aLevel;
aElement.isOpened = false;
aElement.children = null;
});
aObject.forEach(function(aItem) {
aItem._level = aLevel;
aItem._open = false;
aItem._children = null;
if (this._releaseObject) {
["value", "get", "set"].forEach(function(aProp) {
let val = aItem[aProp];
if (val && val.actor) {
this._objectActors.push(val.actor);
}
}, this);
}
}, this);
},
/**
@ -143,42 +159,53 @@ PropertyTreeView.prototype = {
* @private
* @param object aObject
* The object you want to inspect.
* @return array
* The array of properties, each being described in a way that is
* usable by the tree view.
*/
_inspectObject: function PTV__inspectObject(aObject)
{
this._objectCache = {};
this._remoteObjectProvider = this._localObjectProvider.bind(this);
let children = WebConsoleUtils.namesAndValuesOf(aObject, this._objectCache);
this._updateRemoteObject(children, 0);
this._objectPropertiesProvider = this._localPropertiesProvider.bind(this);
let children =
WebConsoleUtils.inspectObject(aObject, this._localObjectGrip.bind(this));
this._propertiesToRows(children, 0);
return children;
},
/**
* An object provider for when the user inspects local objects (not remote
* Make a local fake object actor for the given object.
*
* @private
* @param object aObject
* The object to make an actor for.
* @return object
* The fake actor grip that represents the given object.
*/
_localObjectGrip: function PTV__localObjectGrip(aObject)
{
let grip = WebConsoleUtils.getObjectGrip(aObject);
grip.actor = "obj" + gSequenceId();
this._localObjectActors[grip.actor] = aObject;
return grip;
},
/**
* A properties provider for when the user inspects local objects (not remote
* ones).
*
* @private
* @param string aFromCacheId
* The cache ID from where to retrieve the desired object.
* @param string aObjectId
* The ID of the object you want.
* @param string aDestCacheId
* The ID of the cache where to store any objects referenced by the
* desired object.
* @param string aActor
* The ID of the object actor you want.
* @param function aCallback
* The function you want to receive the object.
* The function you want to receive the list of properties.
*/
_localObjectProvider:
function PTV__localObjectProvider(aFromCacheId, aObjectId, aDestCacheId,
aCallback)
_localPropertiesProvider:
function PTV__localPropertiesProvider(aActor, aCallback)
{
let object = WebConsoleUtils.namesAndValuesOf(this._objectCache[aObjectId],
this._objectCache);
aCallback({cacheId: aFromCacheId,
objectId: aObjectId,
object: object,
childrenCacheId: aDestCacheId || aFromCacheId,
});
let object = this._localObjectActors[aActor];
let properties =
WebConsoleUtils.inspectObject(object, this._localObjectGrip.bind(this));
aCallback(properties);
},
/** nsITreeView interface implementation **/
@ -187,18 +214,20 @@ PropertyTreeView.prototype = {
get rowCount() { return this._rows.length; },
setTree: function(treeBox) { this._treeBox = treeBox; },
getCellText: function(idx, column) {
getCellText: function PTV_getCellText(idx, column)
{
let row = this._rows[idx];
return row.name + ": " + row.value;
return row.name + ": " + WebConsoleUtils.getPropertyPanelValue(row);
},
getLevel: function(idx) {
return this._rows[idx].level;
return this._rows[idx]._level;
},
isContainer: function(idx) {
return !!this._rows[idx].inspectable;
return typeof this._rows[idx].value == "object" && this._rows[idx].value &&
this._rows[idx].value.inspectable;
},
isContainerOpen: function(idx) {
return this._rows[idx].isOpened;
return this._rows[idx]._open;
},
isContainerEmpty: function(idx) { return false; },
isSeparator: function(idx) { return false; },
@ -221,22 +250,22 @@ PropertyTreeView.prototype = {
hasNextSibling: function(idx, after)
{
var thisLevel = this.getLevel(idx);
return this._rows.slice(after + 1).some(function (r) r.level == thisLevel);
let thisLevel = this.getLevel(idx);
return this._rows.slice(after + 1).some(function (r) r._level == thisLevel);
},
toggleOpenState: function(idx)
{
let item = this._rows[idx];
if (!item.inspectable) {
if (!this.isContainer(idx)) {
return;
}
if (item.isOpened) {
if (item._open) {
this._treeBox.beginUpdateBatch();
item.isOpened = false;
item._open = false;
var thisLevel = item.level;
var thisLevel = item._level;
var t = idx + 1, deleteCount = 0;
while (t < this._rows.length && this.getLevel(t++) > thisLevel) {
deleteCount++;
@ -251,31 +280,27 @@ PropertyTreeView.prototype = {
}
else {
let levelUpdate = true;
let callback = function _onRemoteResponse(aResponse) {
let callback = function _onRemoteResponse(aProperties) {
this._treeBox.beginUpdateBatch();
item.isOpened = true;
if (levelUpdate) {
this._updateRemoteObject(aResponse.object, item.level + 1);
item.children = aResponse.object;
this._propertiesToRows(aProperties, item._level + 1);
item._children = aProperties;
}
this._rows.splice.apply(this._rows, [idx + 1, 0].concat(item.children));
this._rows.splice.apply(this._rows, [idx + 1, 0].concat(item._children));
this._treeBox.rowCountChanged(idx + 1, item.children.length);
this._treeBox.rowCountChanged(idx + 1, item._children.length);
this._treeBox.invalidateRow(idx);
this._treeBox.endUpdateBatch();
item._open = true;
}.bind(this);
if (!item.children) {
let fromCacheId = item.level > 0 ? this._panelCacheId :
this._rootCacheId;
this._remoteObjectProvider(fromCacheId, item.objectId,
this._panelCacheId, callback);
if (!item._children) {
this._objectPropertiesProvider(item.value.actor, callback);
}
else {
levelUpdate = false;
callback({object: item.children});
callback(item._children);
}
}
},
@ -298,18 +323,23 @@ PropertyTreeView.prototype = {
drop: function(index, orientation, dataTransfer) { },
canDrop: function(index, orientation, dataTransfer) { return false; },
_cleanup: function PTV__cleanup()
/**
* Cleanup the property tree view.
*/
cleanup: function PTV_cleanup()
{
if (this._rows.length) {
// Reset the existing _rows children to the initial state.
this._updateRemoteObject(this._rows, 0);
this._rows = [];
if (this._releaseObject) {
this._objectActors.forEach(this._releaseObject);
delete this._objectPropertiesProvider;
delete this._releaseObject;
}
if (this._localObjectActors) {
delete this._localObjectActors;
delete this._objectPropertiesProvider;
}
delete this._objectCache;
delete this._rootCacheId;
delete this._panelCacheId;
delete this._remoteObjectProvider;
this._rows = [];
this._objectActors = [];
},
};
@ -459,3 +489,9 @@ PropertyPanel.prototype.destroy = function PP_destroy()
this.tree = null;
}
function gSequenceId()
{
return gSequenceId.n++;
}
gSequenceId.n = 0;

View File

@ -117,7 +117,7 @@ function testPropertyPanel(aPanel) {
ok(find("iter1: Iterator", false),
"iter1 is correctly displayed in the Property Panel");
ok(find("iter2: Iterator", false),
ok(find("iter2: Object", false),
"iter2 is correctly displayed in the Property Panel");
executeSoon(finishTest);

View File

@ -41,7 +41,7 @@ function testConsoleDir(outputNode) {
if (text == "querySelectorAll: function querySelectorAll()") {
foundQSA = true;
}
else if (text == "location: Object") {
else if (text == "location: Location") {
foundLocation = true;
}
else if (text == "write: function write()") {

View File

@ -111,10 +111,10 @@ function testJSTerm(hud)
let foundTab = null;
waitForSuccess({
name: "help tab opened",
name: "help tabs opened",
validatorFn: function()
{
let newTabOpen = gBrowser.tabs.length == tabs + 1;
let newTabOpen = gBrowser.tabs.length == tabs + 3;
if (!newTabOpen) {
return false;
}
@ -124,7 +124,9 @@ function testJSTerm(hud)
},
successFn: function()
{
gBrowser.removeTab(foundTab);
gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
nextTest();
},
failureFn: nextTest,
@ -176,7 +178,7 @@ function testJSTerm(hud)
jsterm.clearOutput();
jsterm.execute("pprint(print)");
checkResult(function(nodes) {
return nodes[0].textContent.indexOf("aJSTerm.") > -1;
return nodes[0].textContent.indexOf("aOwner.helperResult") > -1;
}, "pprint(function) shows source", 1);
yield;

View File

@ -49,6 +49,8 @@ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/en/Security/MixedContent";
const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers";
// The amount of time in milliseconds that must pass between messages to
// trigger the display of a new group.
const NEW_GROUP_DELAY = 5000;
@ -307,6 +309,12 @@ WebConsoleFrame.prototype = {
*/
filterBox: null,
/**
* Getter for the debugger WebConsoleClient.
* @type object
*/
get webConsoleClient() this.proxy ? this.proxy.webConsoleClient : null,
_saveRequestAndResponseBodies: false,
/**
@ -413,8 +421,10 @@ WebConsoleFrame.prototype = {
this.owner.onCloseButton.bind(this.owner));
let clearButton = doc.getElementsByClassName("webconsole-clear-console-button")[0];
clearButton.addEventListener("command",
this.owner.onClearButton.bind(this.owner));
clearButton.addEventListener("command", function WCF__onClearButton() {
this.owner._onClearButton();
this.jsterm.clearOutput(true);
}.bind(this));
},
/**
@ -657,27 +667,9 @@ WebConsoleFrame.prototype = {
}
switch (aMessage.name) {
case "JSTerm:EvalResult":
case "JSTerm:EvalObject":
case "JSTerm:AutocompleteProperties":
this.owner._receiveMessageWithCallback(aMessage.json);
break;
case "JSTerm:ClearOutput":
this.jsterm.clearOutput();
break;
case "JSTerm:InspectObject":
this.jsterm.handleInspectObject(aMessage.json);
break;
case "WebConsole:ConsoleAPI":
this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage,
[aMessage.json]);
break;
case "WebConsole:Initialized":
this._onMessageManagerInitComplete();
break;
case "WebConsole:CachedMessages":
this._displayCachedConsoleMessages(aMessage.json.messages);
break;
case "WebConsole:NetworkActivity":
this.handleNetworkActivity(aMessage.json);
break;
@ -688,9 +680,6 @@ WebConsoleFrame.prototype = {
case "WebConsole:LocationChange":
this.owner.onLocationChange(aMessage.json);
break;
case "JSTerm:NonNativeConsoleAPI":
this.outputMessage(CATEGORY_JS, this.logWarningAboutReplacedAPI);
break;
}
},
@ -1033,13 +1022,11 @@ WebConsoleFrame.prototype = {
* Display cached messages that may have been collected before the UI is
* displayed.
*
* @private
* @param array aRemoteMessages
* Array of cached messages coming from the remote Web Console
* content instance.
*/
_displayCachedConsoleMessages:
function WCF__displayCachedConsoleMessages(aRemoteMessages)
displayCachedMessages: function WCF_displayCachedMessages(aRemoteMessages)
{
if (!aRemoteMessages.length) {
return;
@ -1062,19 +1049,11 @@ WebConsoleFrame.prototype = {
},
/**
* Logs a message to the Web Console that originates from the remote Web
* Console instance.
* Logs a message to the Web Console that originates from the Web Console
* server.
*
* @param object aMessage
* The message received from the remote Web Console instance.
* console service. This object needs to hold:
* - hudId - the Web Console ID.
* - apiMessage - a representation of the object sent by the console
* storage service. This object holds the console message level, the
* arguments that were passed to the console method and other
* information.
* - argumentsToString - the array of arguments passed to the console
* method, each converted to a string.
* The message received from the server.
* @return nsIDOMElement|undefined
* The message element to display in the Web Console output.
*/
@ -1084,9 +1063,9 @@ WebConsoleFrame.prototype = {
let clipboardText = null;
let sourceURL = null;
let sourceLine = 0;
let level = aMessage.apiMessage.level;
let args = aMessage.apiMessage.arguments;
let argsToString = aMessage.argumentsToString;
let level = aMessage.level;
let args = aMessage.arguments;
let objectActors = [];
switch (level) {
case "log":
@ -1094,17 +1073,36 @@ WebConsoleFrame.prototype = {
case "warn":
case "error":
case "debug":
body = {
cacheId: aMessage.objectsCacheId,
remoteObjects: args,
argsToString: argsToString,
};
clipboardText = argsToString.join(" ");
sourceURL = aMessage.apiMessage.filename;
sourceLine = aMessage.apiMessage.lineNumber;
break;
case "dir":
case "groupEnd": {
body = { arguments: args };
let clipboardArray = [];
args.forEach(function(aValue) {
clipboardArray.push(WebConsoleUtils.objectActorGripToString(aValue));
if (aValue && typeof aValue == "object" && aValue.actor) {
objectActors.push(aValue.actor);
}
}, this);
clipboardText = clipboardArray.join(" ");
sourceURL = aMessage.filename;
sourceLine = aMessage.lineNumber;
case "trace":
if (level == "dir") {
body.objectProperties = aMessage.objectProperties;
}
else if (level == "groupEnd") {
objectActors.forEach(this._releaseObject, this);
if (this.groupDepth > 0) {
this.groupDepth--;
}
return; // no need to continue
}
break;
}
case "trace": {
let filename = WebConsoleUtils.abbreviateSourceURL(args[0].filename);
let functionName = args[0].functionName ||
l10n.getStr("stacktrace.anonymousFunction");
@ -1126,34 +1124,16 @@ WebConsoleFrame.prototype = {
clipboardText = clipboardText.trimRight();
break;
case "dir":
body = {
cacheId: aMessage.objectsCacheId,
resultString: argsToString[0],
remoteObject: args[0],
remoteObjectProvider:
this.jsterm.remoteObjectProvider.bind(this.jsterm),
};
clipboardText = body.resultString;
sourceURL = aMessage.apiMessage.filename;
sourceLine = aMessage.apiMessage.lineNumber;
break;
}
case "group":
case "groupCollapsed":
clipboardText = body = args;
sourceURL = aMessage.apiMessage.filename;
sourceLine = aMessage.apiMessage.lineNumber;
sourceURL = aMessage.filename;
sourceLine = aMessage.lineNumber;
this.groupDepth++;
break;
case "groupEnd":
if (this.groupDepth > 0) {
this.groupDepth--;
}
return;
case "time":
if (!args) {
return;
@ -1164,8 +1144,8 @@ WebConsoleFrame.prototype = {
}
body = l10n.getFormatStr("timerStarted", [args.name]);
clipboardText = body;
sourceURL = aMessage.apiMessage.filename;
sourceLine = aMessage.apiMessage.lineNumber;
sourceURL = aMessage.filename;
sourceLine = aMessage.lineNumber;
break;
case "timeEnd":
@ -1174,8 +1154,8 @@ WebConsoleFrame.prototype = {
}
body = l10n.getFormatStr("timeEnd", [args.name, args.duration]);
clipboardText = body;
sourceURL = aMessage.apiMessage.filename;
sourceLine = aMessage.apiMessage.lineNumber;
sourceURL = aMessage.filename;
sourceLine = aMessage.lineNumber;
break;
default:
@ -1187,6 +1167,10 @@ WebConsoleFrame.prototype = {
sourceURL, sourceLine, clipboardText,
level, aMessage.timeStamp);
if (objectActors.length) {
node._objectActors = objectActors;
}
// Make the node bring up the property panel, to allow the user to inspect
// the stack trace.
if (level == "trace") {
@ -1208,10 +1192,6 @@ WebConsoleFrame.prototype = {
}
if (level == "dir") {
// Make sure the cached evaluated object will be purged when the node is
// removed.
node._evalCacheId = aMessage.objectsCacheId;
// Initialize the inspector message node, by setting the PropertyTreeView
// object on the tree view. This has to be done *after* the node is
// shown, because the tree binding must be attached first.
@ -1223,6 +1203,18 @@ WebConsoleFrame.prototype = {
return node;
},
/**
* Handle ConsoleAPICall objects received from the server. This method outputs
* the window.console API call.
*
* @param object aMessage
* The console API message received from the server.
*/
handleConsoleAPICall: function WCF_handleConsoleAPICall(aMessage)
{
this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, [aMessage]);
},
/**
* The click event handler for objects shown inline coming from the
* window.console API.
@ -1233,11 +1225,11 @@ WebConsoleFrame.prototype = {
* @param nsIDOMNode aAnchor
* The object inspector anchor element. This is the clickable element
* in the console.log message we display.
* @param array aRemoteObject
* The remote object representation.
* @param object aObjectActor
* The object actor grip.
*/
_consoleLogClick:
function WCF__consoleLogClick(aMessage, aAnchor, aRemoteObject)
function WCF__consoleLogClick(aMessage, aAnchor, aObjectActor)
{
if (aAnchor._panelOpen) {
return;
@ -1249,29 +1241,28 @@ WebConsoleFrame.prototype = {
// Data to inspect.
data: {
// This is where the resultObject children are cached.
rootCacheId: aMessage._evalCacheId,
remoteObject: aRemoteObject,
// This is where all objects retrieved by the panel will be cached.
panelCacheId: "HUDPanel-" + gSequenceId(),
remoteObjectProvider: this.jsterm.remoteObjectProvider.bind(this.jsterm),
objectPropertiesProvider: this.objectPropertiesProvider.bind(this),
releaseObject: this._releaseObject.bind(this),
},
};
let propPanel = this.jsterm.openPropertyPanel(options);
propPanel.panel.setAttribute("hudId", this.hudId);
let onPopupHide = function JST__evalInspectPopupHide() {
let propPanel;
let onPopupHide = function _onPopupHide() {
propPanel.panel.removeEventListener("popuphiding", onPopupHide, false);
this.jsterm.clearObjectCache(options.data.panelCacheId);
if (!aMessage.parentNode && aMessage._evalCacheId) {
this.jsterm.clearObjectCache(aMessage._evalCacheId);
if (!aMessage.parentNode && aMessage._objectActors) {
aMessage._objectActors.forEach(this._releaseObject, this);
aMessage._objectActors = null;
}
}.bind(this);
this.objectPropertiesProvider(aObjectActor.actor,
function _onObjectProperties(aProperties) {
options.data.objectProperties = aProperties;
propPanel = this.jsterm.openPropertyPanel(options);
propPanel.panel.setAttribute("hudId", this.hudId);
propPanel.panel.addEventListener("popuphiding", onPopupHide, false);
}.bind(this));
},
/**
@ -1453,14 +1444,12 @@ WebConsoleFrame.prototype = {
/**
* Inform user that the Web Console API has been replaced by a script
* in a content page.
*
* @return nsIDOMElement|undefined
* The message element to display in the Web Console output.
*/
logWarningAboutReplacedAPI: function WCF_logWarningAboutReplacedAPI()
{
return this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING,
let node = this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING,
l10n.getStr("ConsoleAPIDisabled"));
this.outputMessage(CATEGORY_JS, node);
},
/**
@ -1852,8 +1841,8 @@ WebConsoleFrame.prototype = {
{
let [category, methodOrNode, args] = aItem;
if (typeof methodOrNode != "function" &&
methodOrNode._evalCacheId && !methodOrNode._panelOpen) {
this.jsterm.clearObjectCache(methodOrNode._evalCacheId);
methodOrNode._objectActors && !methodOrNode._panelOpen) {
methodOrNode._objectActors.forEach(this._releaseObject, this);
}
if (category == CATEGORY_NETWORK) {
@ -1870,9 +1859,29 @@ WebConsoleFrame.prototype = {
}
else if (category == CATEGORY_WEBDEV &&
methodOrNode == this.logConsoleAPIMessage) {
let level = args[0].apiMessage.level;
let level = args[0].level;
let releaseObject = function _releaseObject(aValue) {
if (aValue && typeof aValue == "object" && aValue.actor) {
this._releaseObject(aValue.actor);
}
}.bind(this);
switch (level) {
case "log":
case "info":
case "warn":
case "error":
case "debug":
case "dir":
case "groupEnd": {
args[0].arguments.forEach(releaseObject);
if (level == "dir") {
this.jsterm.clearObjectCache(args[0].objectsCacheId);
args[0].objectProperties.forEach(function(aObject) {
["value", "get", "set"].forEach(function(aProp) {
releaseObject(aObject[aProp]);
});
});
}
}
}
}
},
@ -1916,10 +1925,8 @@ WebConsoleFrame.prototype = {
let tree = aMessageNode.querySelector("tree");
tree.parentNode.removeChild(tree);
aMessageNode.propertyTreeView.data = null;
aMessageNode.propertyTreeView = null;
if (tree.view) {
tree.view.data = null;
}
tree.view = null;
},
@ -1931,8 +1938,8 @@ WebConsoleFrame.prototype = {
*/
removeOutputMessage: function WCF_removeOutputMessage(aNode)
{
if (aNode._evalCacheId && !aNode._panelOpen) {
this.jsterm.clearObjectCache(aNode._evalCacheId);
if (aNode._objectActors && !aNode._panelOpen) {
aNode._objectActors.forEach(this._releaseObject, this);
}
if (aNode.classList.contains("webconsole-msg-cssparser")) {
@ -2057,7 +2064,7 @@ WebConsoleFrame.prototype = {
else {
let str = undefined;
if (aLevel == "dir") {
str = aBody.resultString;
str = WebConsoleUtils.objectActorGripToString(aBody.arguments[0]);
}
else if (["log", "info", "warn", "error", "debug"].indexOf(aLevel) > -1 &&
typeof aBody == "object") {
@ -2132,10 +2139,9 @@ WebConsoleFrame.prototype = {
let treeView = node.propertyTreeView = new PropertyTreeView();
treeView.data = {
rootCacheId: body.cacheId,
panelCacheId: body.cacheId,
remoteObject: Array.isArray(body.remoteObject) ? body.remoteObject : [],
remoteObjectProvider: body.remoteObjectProvider,
objectPropertiesProvider: this.objectPropertiesProvider.bind(this),
releaseObject: this._releaseObject.bind(this),
objectProperties: body.objectProperties,
};
tree.setAttribute("rows", treeView.rowCount);
@ -2164,13 +2170,12 @@ WebConsoleFrame.prototype = {
* output.
* @param object aBody
* The object given by this.logConsoleAPIMessage(). This object holds
* the call information that we need to display.
* the call information that we need to display - mainly the arguments
* array of the given API call.
*/
_makeConsoleLogMessageBody:
function WCF__makeConsoleLogMessageBody(aMessage, aContainer, aBody)
{
aMessage._evalCacheId = aBody.cacheId;
Object.defineProperty(aMessage, "_panelOpen", {
get: function() {
let nodes = aContainer.querySelectorAll(".hud-clickable");
@ -2182,17 +2187,19 @@ WebConsoleFrame.prototype = {
configurable: false
});
aBody.remoteObjects.forEach(function(aItem, aIndex) {
aBody.arguments.forEach(function(aItem) {
if (aContainer.firstChild) {
aContainer.appendChild(this.document.createTextNode(" "));
}
let text = aBody.argsToString[aIndex];
if (!Array.isArray(aItem)) {
let text = WebConsoleUtils.objectActorGripToString(aItem);
if (aItem && typeof aItem != "object" || !aItem.inspectable) {
aContainer.appendChild(this.document.createTextNode(text));
return;
}
// For inspectable objects.
let elem = this.document.createElement("description");
elem.classList.add("hud-clickable");
elem.setAttribute("aria-haspopup", "true");
@ -2393,6 +2400,41 @@ WebConsoleFrame.prototype = {
clipboardHelper.copyString(strings.join("\n"), this.document);
},
/**
* Object properties provider. This function gives you the properties of the
* remote object you want.
*
* @param string aActor
* The object actor ID from which you want the properties.
* @param function aCallback
* Function you want invoked once the properties are received.
*/
objectPropertiesProvider:
function WCF_objectPropertiesProvider(aActor, aCallback)
{
this.webConsoleClient.inspectObjectProperties(aActor,
function(aResponse) {
if (aResponse.error) {
Cu.reportError("Failed to retrieve the object properties from the " +
"server. Error: " + aResponse.error);
return;
}
aCallback(aResponse.properties);
});
},
/**
* Release an object actor.
*
* @private
* @param string aActor
* The object actor ID you want to release.
*/
_releaseObject: function WCF__releaseObject(aActor)
{
this.proxy.releaseActor(aActor);
},
/**
* Open the selected item's URL in a new tab.
*/
@ -2478,6 +2520,12 @@ JSTerm.prototype = {
*/
get outputNode() this.hud.outputNode,
/**
* Getter for the debugger WebConsoleClient.
* @type object
*/
get webConsoleClient() this.hud.webConsoleClient,
COMPLETE_FORWARD: 0,
COMPLETE_BACKWARD: 1,
COMPLETE_HINT_ONLY: 2,
@ -2499,59 +2547,59 @@ JSTerm.prototype = {
},
/**
* Asynchronously evaluate a string in the content process sandbox.
*
* @param string aString
* String to evaluate in the content process JavaScript sandbox.
* @param function [aCallback]
* Optional function to be invoked when the evaluation result is
* received.
*/
evalInContentSandbox: function JST_evalInContentSandbox(aString, aCallback)
{
let message = {
str: aString,
resultCacheId: "HUDEval-" + gSequenceId(),
};
this.hud.owner.sendMessageToContent("JSTerm:EvalRequest", message, aCallback);
return message;
},
/**
* The "JSTerm:EvalResult" message handler. This is the JSTerm execution
* result callback which is invoked whenever JavaScript code evaluation
* results come from the content process.
* The JavaScript evaluation response handler.
*
* @private
* @param nsIDOMElement [aAfterNode]
* Optional DOM element after which the evaluation result will be
* inserted.
* @param function [aCallback]
* Optional function to invoke when the evaluation result is added to
* the output.
* @param object aResponse
* The JSTerm:EvalResult message received from the content process. See
* JSTerm.handleEvalRequest() in HUDService-content.js for further
* details.
* @param object aRequest
* The JSTerm:EvalRequest message we sent to the content process.
* @see JSTerm.handleEvalRequest() in HUDService-content.js
* The message received from the server.
*/
_executeResultCallback:
function JST__executeResultCallback(aCallback, aResponse, aRequest)
function JST__executeResultCallback(aAfterNode, aCallback, aResponse)
{
let errorMessage = aResponse.errorMessage;
let resultString = aResponse.resultString;
let result = aResponse.result;
let inspectable = result && typeof result == "object" && result.inspectable;
let helperResult = aResponse.helperResult;
let helperHasRawOutput = !!(helperResult || {}).rawOutput;
let resultString =
WebConsoleUtils.objectActorGripToString(result,
!helperHasRawOutput);
// Hide undefined results coming from JSTerm helper functions.
if (!errorMessage &&
resultString == "undefined" &&
aResponse.helperResult &&
!aResponse.inspectable &&
!aResponse.helperRawOutput) {
return;
if (helperResult && helperResult.type) {
switch (helperResult.type) {
case "clearOutput":
this.clearOutput();
break;
case "inspectObject":
this.handleInspectObject(helperResult.input, helperResult.object);
break;
case "error":
try {
errorMessage = l10n.getStr(helperResult.message);
}
catch (ex) {
errorMessage = helperResult.message;
}
break;
case "help":
this.hud.owner.openLink(HELP_URL);
break;
}
}
let afterNode = aRequest.outputNode;
// Hide undefined results coming from JSTerm helper functions.
if (!errorMessage && result && typeof result == "object" &&
result.type == "undefined" &&
helperResult && !helperHasRawOutput) {
aCallback && aCallback();
return;
}
if (aCallback) {
let oldFlushCallback = this.hud._flushCallback;
@ -2562,19 +2610,24 @@ JSTerm.prototype = {
}.bind(this);
}
if (aResponse.errorMessage) {
this.writeOutput(aResponse.errorMessage, CATEGORY_OUTPUT, SEVERITY_ERROR,
afterNode, aResponse.timestamp);
let node;
if (errorMessage) {
node = this.writeOutput(errorMessage, CATEGORY_OUTPUT, SEVERITY_ERROR,
aAfterNode, aResponse.timestamp);
}
else if (aResponse.inspectable) {
let node = this.writeOutputJS(aResponse.resultString,
else if (inspectable) {
node = this.writeOutputJS(resultString,
this._evalOutputClick.bind(this, aResponse),
afterNode, aResponse.timestamp);
node._evalCacheId = aResponse.childrenCacheId;
aAfterNode, aResponse.timestamp);
}
else {
this.writeOutput(aResponse.resultString, CATEGORY_OUTPUT, SEVERITY_LOG,
afterNode, aResponse.timestamp);
node = this.writeOutput(resultString, CATEGORY_OUTPUT, SEVERITY_LOG,
aAfterNode, aResponse.timestamp);
}
if (result && typeof result == "object" && result.actor) {
node._objectActors = [result.actor];
}
},
@ -2597,10 +2650,9 @@ JSTerm.prototype = {
}
let node = this.writeOutput(aExecuteString, CATEGORY_INPUT, SEVERITY_LOG);
let onResult = this._executeResultCallback.bind(this, node, aCallback);
let onResult = this._executeResultCallback.bind(this, aCallback);
let messageToContent = this.evalInContentSandbox(aExecuteString, onResult);
messageToContent.outputNode = node;
this.webConsoleClient.evaluateJS(aExecuteString, onResult);
this.history.push(aExecuteString);
this.historyIndex++;
@ -2751,7 +2803,7 @@ JSTerm.prototype = {
hud._cssNodes = {};
if (aClearStorage) {
hud.owner.sendMessageToContent("ConsoleAPI:ClearCache", {});
this.webConsoleClient.clearMessagesCache();
}
},
@ -3078,25 +3130,30 @@ JSTerm.prototype = {
return;
}
let message = {
id: "HUDComplete-" + gSequenceId(),
input: this.inputNode.value,
};
let requestId = gSequenceId();
let input = this.inputNode.value;
let cursor = this.inputNode.selectionStart;
// TODO: Bug 787986 - throttle/disable updates, deal with slow/high latency
// network connections.
this.lastCompletion = {
requestId: message.id,
requestId: requestId,
completionType: aType,
value: null,
};
let callback = this._receiveAutocompleteProperties.bind(this, aCallback);
this.hud.owner.sendMessageToContent("JSTerm:Autocomplete", message, callback);
let callback = this._receiveAutocompleteProperties.bind(this, requestId,
aCallback);
this.webConsoleClient.autocomplete(input, cursor, callback);
},
/**
* Handler for the "JSTerm:AutocompleteProperties" message. This method takes
* the completion result received from the content process and updates the UI
* Handler for the autocompletion results. This method takes
* the completion result received from the server and updates the UI
* accordingly.
*
* @param number aRequestId
* Request ID.
* @param function [aCallback=null]
* Optional, function to invoke when the completion result is received.
* @param object aMessage
@ -3104,13 +3161,12 @@ JSTerm.prototype = {
* the content process.
*/
_receiveAutocompleteProperties:
function JST__receiveAutocompleteProperties(aCallback, aMessage)
function JST__receiveAutocompleteProperties(aRequestId, aCallback, aMessage)
{
let inputNode = this.inputNode;
let inputValue = inputNode.value;
if (aMessage.input != inputValue ||
this.lastCompletion.value == inputValue ||
aMessage.id != this.lastCompletion.requestId) {
if (this.lastCompletion.value == inputValue ||
aRequestId != this.lastCompletion.requestId) {
return;
}
@ -3266,7 +3322,7 @@ JSTerm.prototype = {
},
/**
* The "JSTerm:InspectObject" remote message handler. This allows the content
* The JSTerm InspectObject remote message handler. This allows the remote
* process to open the Property Panel for a given object.
*
* @param object aRequest
@ -3274,29 +3330,31 @@ JSTerm.prototype = {
* the user input string that was evaluated to inspect an object and
* the result object which is to be inspected.
*/
handleInspectObject: function JST_handleInspectObject(aRequest)
handleInspectObject: function JST_handleInspectObject(aInput, aActor)
{
let options = {
title: aRequest.input,
title: aInput,
data: {
rootCacheId: aRequest.objectCacheId,
panelCacheId: aRequest.objectCacheId,
remoteObject: aRequest.resultObject,
remoteObjectProvider: this.remoteObjectProvider.bind(this),
objectPropertiesProvider: this.hud.objectPropertiesProvider.bind(this.hud),
releaseObject: this.hud._releaseObject.bind(this.hud),
},
};
let propPanel = this.openPropertyPanel(options);
propPanel.panel.setAttribute("hudId", this.hudId);
let propPanel;
let onPopupHide = function JST__onPopupHide() {
propPanel.panel.removeEventListener("popuphiding", onPopupHide, false);
this.clearObjectCache(options.data.panelCacheId);
this.hud._releaseObject(aActor.actor);
}.bind(this);
this.hud.objectPropertiesProvider(aActor.actor,
function _onObjectProperties(aProperties) {
options.data.objectProperties = aProperties;
propPanel = this.openPropertyPanel(options);
propPanel.panel.setAttribute("hudId", this.hudId);
propPanel.panel.addEventListener("popuphiding", onPopupHide, false);
}.bind(this));
},
/**
@ -3304,7 +3362,7 @@ JSTerm.prototype = {
*
* @private
* @param object aResponse
* The JSTerm:EvalResult message received from the content process.
* The JavaScript evaluation response received from the server.
* @param nsIDOMNode aLink
* The message node for which we are handling events.
*/
@ -3320,35 +3378,36 @@ JSTerm.prototype = {
// Data to inspect.
data: {
// This is where the resultObject children are cached.
rootCacheId: aResponse.childrenCacheId,
remoteObject: aResponse.resultObject,
// This is where all objects retrieved by the panel will be cached.
panelCacheId: "HUDPanel-" + gSequenceId(),
remoteObjectProvider: this.remoteObjectProvider.bind(this),
objectPropertiesProvider: this.hud.objectPropertiesProvider.bind(this.hud),
releaseObject: this.hud._releaseObject.bind(this.hud),
},
};
options.updateButtonCallback = function JST__evalUpdateButton() {
this.evalInContentSandbox(aResponse.input,
this._evalOutputUpdatePanelCallback.bind(this, options, propPanel,
aResponse));
}.bind(this);
let propPanel;
let propPanel = this.openPropertyPanel(options);
propPanel.panel.setAttribute("hudId", this.hudId);
options.updateButtonCallback = function JST__evalUpdateButton() {
let onResult =
this._evalOutputUpdatePanelCallback.bind(this, options, propPanel,
aResponse);
this.webConsoleClient.evaluateJS(aResponse.input, onResult);
}.bind(this);
let onPopupHide = function JST__evalInspectPopupHide() {
propPanel.panel.removeEventListener("popuphiding", onPopupHide, false);
this.clearObjectCache(options.data.panelCacheId);
if (!aLinkNode.parentNode && aLinkNode._evalCacheId) {
this.clearObjectCache(aLinkNode._evalCacheId);
if (!aLinkNode.parentNode && aLinkNode._objectActors) {
aLinkNode._objectActors.forEach(this.hud._releaseObject, this.hud);
aLinkNode._objectActors = null;
}
}.bind(this);
this.hud.objectPropertiesProvider(aResponse.result.actor,
function _onObjectProperties(aProperties) {
options.data.objectProperties = aProperties;
propPanel = this.openPropertyPanel(options);
propPanel.panel.setAttribute("hudId", this.hudId);
propPanel.panel.addEventListener("popuphiding", onPopupHide, false);
}.bind(this));
},
/**
@ -3377,32 +3436,40 @@ JSTerm.prototype = {
return;
}
if (!aNewResponse.inspectable) {
let result = aNewResponse.result;
let inspectable = result && typeof result == "object" && result.inspectable;
let newActor = result && typeof result == "object" ? result.actor : null;
let anchor = aOptions.anchor;
if (anchor && newActor) {
if (!anchor._objectActors) {
anchor._objectActors = [];
}
if (anchor._objectActors.indexOf(newActor) == -1) {
anchor._objectActors.push(newActor);
}
}
if (!inspectable) {
this.writeOutput(l10n.getStr("JSTerm.updateNotInspectable"), CATEGORY_OUTPUT, SEVERITY_ERROR);
return;
}
this.clearObjectCache(aOptions.data.panelCacheId);
this.clearObjectCache(aOptions.data.rootCacheId);
if (aOptions.anchor && aOptions.anchor._evalCacheId) {
aOptions.anchor._evalCacheId = aNewResponse.childrenCacheId;
}
// Update the old response object such that when the panel is reopen, the
// user sees the new response.
aOldResponse.id = aNewResponse.id;
aOldResponse.childrenCacheId = aNewResponse.childrenCacheId;
aOldResponse.resultObject = aNewResponse.resultObject;
aOldResponse.resultString = aNewResponse.resultString;
aOptions.data.rootCacheId = aNewResponse.childrenCacheId;
aOptions.data.remoteObject = aNewResponse.resultObject;
aOldResponse.result = aNewResponse.result;
aOldResponse.error = aNewResponse.error;
aOldResponse.errorMessage = aNewResponse.errorMessage;
aOldResponse.timestamp = aNewResponse.timestamp;
this.hud.objectPropertiesProvider(newActor,
function _onObjectProperties(aProperties) {
aOptions.data.objectProperties = aProperties;
// TODO: This updates the value of the tree.
// However, the states of open nodes is not saved.
// See bug 586246.
aPropPanel.treeView.data = aOptions.data;
}.bind(this));
},
/**
@ -3633,6 +3700,7 @@ function WebConsoleConnectionProxy(aWebConsole)
this.owner = aWebConsole;
this._onPageError = this._onPageError.bind(this);
this._onConsoleAPICall = this._onConsoleAPICall.bind(this);
}
WebConsoleConnectionProxy.prototype = {
@ -3666,6 +3734,14 @@ WebConsoleConnectionProxy.prototype = {
*/
_consoleActor: null,
/**
* Tells if the window.console object of the remote web page is the native
* object or not.
* @private
* @type boolean
*/
_hasNativeConsoleAPI: false,
/**
* Initialize the debugger server.
*/
@ -3689,8 +3765,9 @@ WebConsoleConnectionProxy.prototype = {
let client = this.client = new DebuggerClient(transport);
client.addListener("pageError", this._onPageError);
client.addListener("consoleAPICall", this._onConsoleAPICall);
let listeners = ["PageError"];
let listeners = ["PageError", "ConsoleAPI"];
client.connect(function(aType, aTraits) {
client.listTabs(function(aResponse) {
@ -3725,6 +3802,36 @@ WebConsoleConnectionProxy.prototype = {
this.webConsoleClient = aWebConsoleClient;
this._hasNativeConsoleAPI = aResponse.nativeConsoleAPI;
let msgs = ["PageError", "ConsoleAPI"];
this.webConsoleClient.getCachedMessages(msgs,
this._onCachedMessages.bind(this, aCallback));
},
/**
* The "cachedMessages" response handler.
*
* @private
* @param function [aCallback]
* Optional function to invoke once the connection is established.
* @param object aResponse
* The JSON response object received from the server.
*/
_onCachedMessages: function WCCP__onCachedMessages(aCallback, aResponse)
{
if (aResponse.error) {
Cu.reportError("Web Console getCachedMessages error: " + aResponse.error +
" " + aResponse.message);
return;
}
this.owner.displayCachedMessages(aResponse.messages);
if (!this._hasNativeConsoleAPI) {
this.owner.logWarningAboutReplacedAPI();
}
this.connected = true;
aCallback && aCallback();
},
@ -3746,6 +3853,36 @@ WebConsoleConnectionProxy.prototype = {
}
},
/**
* The "consoleAPICall" message type handler. We redirect any message to
* the UI for displaying.
*
* @private
* @param string aType
* Message type.
* @param object aPacket
* The message received from the server.
*/
_onConsoleAPICall: function WCCP__onConsoleAPICall(aType, aPacket)
{
if (this.owner && aPacket.from == this._consoleActor) {
this.owner.handleConsoleAPICall(aPacket.message);
}
},
/**
* Release an object actor.
*
* @param string aActor
* The actor ID to send the request to.
*/
releaseActor: function WCCP_releaseActor(aActor)
{
if (this.client) {
this.client.release(aActor);
}
},
/**
* Disconnect the Web Console from the remote server.
*
@ -3760,6 +3897,7 @@ WebConsoleConnectionProxy.prototype = {
}
this.client.removeListener("pageError", this._onPageError);
this.client.removeListener("consoleAPICall", this._onConsoleAPICall);
this.client.close(aOnDisconnect);
this.client = null;

View File

@ -167,6 +167,7 @@ const ThreadStateTypes = {
* by the client.
*/
const UnsolicitedNotifications = {
"consoleAPICall": "consoleAPICall",
"newScript": "newScript",
"tabDetached": "tabDetached",
"tabNavigated": "tabNavigated",
@ -371,6 +372,20 @@ DebuggerClient.prototype = {
});
},
/**
* Release an object actor.
*
* @param string aActor
* The actor ID to send the request to.
*/
release: function DC_release(aActor) {
let packet = {
to: aActor,
type: "release",
};
this.request(packet);
},
/**
* Send a request to the debugging server.
*

View File

@ -48,6 +48,75 @@ WebConsoleClient.prototype = {
this._client.request(packet, aOnResponse);
},
/**
* Inspect the properties of an object.
*
* @param string aActor
* The WebConsoleObjectActor ID to send the request to.
* @param function aOnResponse
* The function invoked when the response is received.
*/
inspectObjectProperties:
function WCC_inspectObjectProperties(aActor, aOnResponse)
{
let packet = {
to: aActor,
type: "inspectProperties",
};
this._client.request(packet, aOnResponse);
},
/**
* Evaluate a JavaScript expression.
*
* @param string aString
* The code you want to evaluate.
* @param function aOnResponse
* The function invoked when the response is received.
*/
evaluateJS: function WCC_evaluateJS(aString, aOnResponse)
{
let packet = {
to: this._actor,
type: "evaluateJS",
text: aString,
};
this._client.request(packet, aOnResponse);
},
/**
* Autocomplete a JavaScript expression.
*
* @param string aString
* The code you want to autocomplete.
* @param number aCursor
* Cursor location inside the string. Index starts from 0.
* @param function aOnResponse
* The function invoked when the response is received.
*/
autocomplete: function WCC_autocomplete(aString, aCursor, aOnResponse)
{
let packet = {
to: this._actor,
type: "autocomplete",
text: aString,
cursor: aCursor,
};
this._client.request(packet, aOnResponse);
},
/**
* Clear the cache of messages (page errors and console API calls).
*/
clearMessagesCache: function WCC_clearMessagesCache()
{
let packet = {
to: this._actor,
type: "clearMessagesCache",
};
this._client.request(packet);
},
/**
* Start the given Web Console listeners.
*

View File

@ -15,8 +15,22 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
var EXPORTED_SYMBOLS = ["WebConsoleUtils", "JSPropertyProvider",
"PageErrorListener"];
XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPIStorage",
"resource://gre/modules/ConsoleAPIStorage.jsm");
var EXPORTED_SYMBOLS = ["WebConsoleUtils", "JSPropertyProvider", "JSTermHelpers",
"PageErrorListener", "ConsoleAPIListener"];
// Match the function name from the result of toString() or toSource().
//
// Examples:
// (function foobar(a, b) { ...
// function foobar2(a) { ...
// function() { ...
const REGEX_MATCH_FUNCTION_NAME = /^\(?function\s+([^(\s]+)\s*\(/;
// Match the function arguments from the result of toString() or toSource().
const REGEX_MATCH_FUNCTION_ARGS = /^\(?function\s*[^\s(]*\s*\((.+?)\)/;
const TYPES = { OBJECT: 0,
FUNCTION: 1,
@ -213,7 +227,12 @@ var WebConsoleUtils = {
case "error":
case "number":
case "regexp":
output = aResult.toString();
try {
output = aResult + "";
}
catch (ex) {
output = ex;
}
break;
case "null":
case "undefined":
@ -309,8 +328,15 @@ var WebConsoleUtils = {
getResultType: function WCU_getResultType(aResult)
{
let type = aResult === null ? "null" : typeof aResult;
try {
if (type == "object" && aResult.constructor && aResult.constructor.name) {
type = aResult.constructor.name;
type = aResult.constructor.name + "";
}
}
catch (ex) {
// Prevent potential exceptions in page-provided objects from taking down
// the Web Console. If the constructor.name is a getter that throws, or
// something else bad happens.
}
return type.toLowerCase();
@ -442,27 +468,43 @@ var WebConsoleUtils = {
if (typeof aObject != "object") {
return false;
}
let desc;
let desc = this.getPropertyDescriptor(aObject, aProp);
return desc && desc.get && !this.isNativeFunction(desc.get);
},
/**
* Get the property descriptor for the given object.
*
* @param object aObject
* The object that contains the property.
* @param string aProp
* The property you want to get the descriptor for.
* @return object
* Property descriptor.
*/
getPropertyDescriptor: function WCU_getPropertyDescriptor(aObject, aProp)
{
let desc = null;
while (aObject) {
try {
if (desc = Object.getOwnPropertyDescriptor(aObject, aProp)) {
break;
}
}
catch (ex) {
catch (ex if (ex.name == "NS_ERROR_XPC_BAD_CONVERT_JS" ||
ex.name == "NS_ERROR_XPC_BAD_OP_ON_WN_PROTO" ||
ex.name == "TypeError")) {
// Native getters throw here. See bug 520882.
if (ex.name == "NS_ERROR_XPC_BAD_CONVERT_JS" ||
ex.name == "NS_ERROR_XPC_BAD_OP_ON_WN_PROTO") {
return false;
}
throw ex;
// null throws TypeError.
}
try {
aObject = Object.getPrototypeOf(aObject);
}
if (desc && desc.get && !this.isNativeFunction(desc.get)) {
return true;
catch (ex if (ex.name == "TypeError")) {
return desc;
}
return false;
}
return desc;
},
/**
@ -549,7 +591,24 @@ var WebConsoleUtils = {
pairs.push(pair);
}
pairs.sort(function(a, b)
pairs.sort(this.propertiesSort);
return pairs;
},
/**
* Sort function for object properties.
*
* @param object a
* Property descriptor.
* @param object b
* Property descriptor.
* @return integer
* -1 if a.name < b.name,
* 1 if a.name > b.name,
* 0 otherwise.
*/
propertiesSort: function WCU_propertiesSort(a, b)
{
// Convert the pair.name to a number for later sorting.
let aNumber = parseFloat(a.name);
@ -575,9 +634,164 @@ var WebConsoleUtils = {
else {
return 0;
}
});
},
return pairs;
/**
* Inspect the properties of the given object. For each property a descriptor
* object is created. The descriptor gives you information about the property
* name, value, type, getter and setter. When the property value references
* another object you get a wrapper that holds information about that object.
*
* @see this.inspectObjectProperty
* @param object aObject
* The object you want to inspect.
* @param function aObjectWrapper
* The function that creates wrappers for property values which
* reference other objects. This function must take one argument, the
* object to wrap, and it must return an object grip that gives
* information about the referenced object.
* @return array
* An array of property descriptors.
*/
inspectObject: function WCU_inspectObject(aObject, aObjectWrapper)
{
let properties = [];
let isDOMDocument = aObject instanceof Ci.nsIDOMDocument;
let deprecated = ["width", "height", "inputEncoding"];
for (let name in aObject) {
// See bug 632275: skip deprecated properties.
if (isDOMDocument && deprecated.indexOf(name) > -1) {
continue;
}
properties.push(this.inspectObjectProperty(aObject, name, aObjectWrapper));
}
return properties.sort(this.propertiesSort);
},
/**
* A helper method that creates a property descriptor for the provided object,
* properly formatted for sending in a protocol response.
*
* The property value can reference other objects. Since actual objects cannot
* be sent to the client, we need to send simple object grips - descriptors
* for those objects. This is why you need to give an object wrapper function
* that creates object grips.
*
* @param string aProperty
* Property name for which we have the descriptor.
* @param object aObject
* The object that the descriptor is generated for.
* @param function aObjectWrapper
* This function is given the property value. Whatever the function
* returns is used as the representation of the property value.
* @return object
* The property descriptor formatted for sending to the client.
*/
inspectObjectProperty:
function WCU_inspectObjectProperty(aObject, aProperty, aObjectWrapper)
{
let descriptor = this.getPropertyDescriptor(aObject, aProperty) || {};
let result = { name: aProperty };
result.configurable = descriptor.configurable;
result.enumerable = descriptor.enumerable;
result.writable = descriptor.writable;
if (descriptor.value !== undefined) {
result.value = this.createValueGrip(descriptor.value, aObjectWrapper);
}
else if (descriptor.get) {
if (this.isNativeFunction(descriptor.get)) {
result.value = this.createValueGrip(aObject[aProperty], aObjectWrapper);
}
else {
result.get = this.createValueGrip(descriptor.get, aObjectWrapper);
result.set = this.createValueGrip(descriptor.set, aObjectWrapper);
}
}
// There are cases with properties that have no value and no getter. For
// example window.screen.width.
if (result.value === undefined && result.get === undefined) {
result.value = this.createValueGrip(aObject[aProperty], aObjectWrapper);
}
return result;
},
/**
* Make an object grip for the given object. An object grip of the simplest
* form with minimal information about the given object is returned. This
* method is usually combined with other functions that add further state
* information and object ID such that, later, the client is able to retrieve
* more information about the object being represented by this grip.
*
* @param object aObject
* The object you want to create a grip for.
* @return object
* The object grip.
*/
getObjectGrip: function WCU_getObjectGrip(aObject)
{
let className = null;
let type = typeof aObject;
let result = {
"type": type,
"className": this.getObjectClassName(aObject),
"displayString": this.formatResult(aObject),
"inspectable": this.isObjectInspectable(aObject),
};
if (type == "function") {
result.functionName = this.getFunctionName(aObject);
result.functionArguments = this.getFunctionArguments(aObject);
}
return result;
},
/**
* Create a grip for the given value. If the value is an object,
* an object wrapper will be created.
*
* @param mixed aValue
* The value you want to create a grip for, before sending it to the
* client.
* @param function aObjectWrapper
* If the value is an object then the aObjectWrapper function is
* invoked to give us an object grip. See this.getObjectGrip().
* @return mixed
* The value grip.
*/
createValueGrip: function WCU_createValueGrip(aValue, aObjectWrapper)
{
let type = typeof(aValue);
switch (type) {
case "boolean":
case "string":
case "number":
return aValue;
case "object":
case "function":
if (aValue) {
return aObjectWrapper(aValue);
}
default:
if (aValue === null) {
return { type: "null" };
}
if (aValue === undefined) {
return { type: "undefined" };
}
Cu.reportError("Failed to provide a grip for value of " + type + ": " +
aValue);
return null;
}
},
/**
@ -638,6 +852,159 @@ var WebConsoleUtils = {
return false;
}
},
/**
* Make a string representation for an object actor grip.
*
* @param object aGrip
* The object grip received from the server.
* @param boolean [aFormatString=false]
* Optional boolean that tells if you want strings to be unevaled or
* not.
* @return string
* The object grip converted to a string.
*/
objectActorGripToString: function WCU_objectActorGripToString(aGrip, aFormatString)
{
// Primitives like strings and numbers are not sent as objects.
// But null and undefined are sent as objects with the type property
// telling which type of value we have.
let type = typeof(aGrip);
if (aGrip && type == "object") {
return aGrip.displayString || aGrip.className || aGrip.type || type;
}
return type == "string" && aFormatString ?
this.formatResultString(aGrip) : aGrip + "";
},
/**
* Helper function to deduce the name of the provided function.
*
* @param funtion aFunction
* The function whose name will be returned.
* @return string
* Function name.
*/
getFunctionName: function WCF_getFunctionName(aFunction)
{
let name = null;
if (aFunction.name) {
name = aFunction.name;
}
else {
let desc;
try {
desc = aFunction.getOwnPropertyDescriptor("displayName");
}
catch (ex) { }
if (desc && typeof desc.value == "string") {
name = desc.value;
}
}
if (!name) {
try {
let str = (aFunction.toString() || aFunction.toSource()) + "";
name = (str.match(REGEX_MATCH_FUNCTION_NAME) || [])[1];
}
catch (ex) { }
}
return name;
},
/**
* Helper function to deduce the arguments of the provided function.
*
* @param funtion aFunction
* The function whose name will be returned.
* @return array
* Function arguments.
*/
getFunctionArguments: function WCF_getFunctionArguments(aFunction)
{
let args = [];
try {
let str = (aFunction.toString() || aFunction.toSource()) + "";
let argsString = (str.match(REGEX_MATCH_FUNCTION_ARGS) || [])[1];
if (argsString) {
args = argsString.split(/\s*,\s*/);
}
}
catch (ex) { }
return args;
},
/**
* Get the object class name. For example, the |window| object has the Window
* class name (based on [object Window]).
*
* @param object aObject
* The object you want to get the class name for.
* @return string
* The object class name.
*/
getObjectClassName: function WCF_getObjectClassName(aObject)
{
if (aObject === null) {
return "null";
}
if (aObject === undefined) {
return "undefined";
}
let type = typeof aObject;
if (type != "object") {
return type;
}
let className;
try {
className = ((aObject + "").match(/^\[object (\S+)\]$/) || [])[1];
if (!className) {
className = ((aObject.constructor + "").match(/^\[object (\S+)\]$/) || [])[1];
}
if (!className && typeof aObject.constructor == "function") {
className = this.getFunctionName(aObject.constructor);
}
}
catch (ex) { }
return className;
},
/**
* Determine the string to display as a property value in the property panel.
*
* @param object aActor
* Object actor grip.
* @return string
* Property value as suited for the property panel.
*/
getPropertyPanelValue: function WCU_getPropertyPanelValue(aActor)
{
if (aActor.get) {
return "Getter";
}
let val = aActor.value;
if (typeof val == "string") {
return this.formatResultString(val);
}
if (typeof val != "object" || !val) {
return val;
}
if (val.type == "function" && val.functionName) {
return "function " + val.functionName + "(" +
val.functionArguments.join(", ") + ")";
}
if (val.type == "object" && val.className) {
return val.className;
}
return val.displayString || val.type;
},
};
//////////////////////////////////////////////////////////////////////////
@ -1163,3 +1530,327 @@ PageErrorListener.prototype =
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 aWindow
* The window object for which we are created.
* @param object aOwner
* 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.
*/
function ConsoleAPIListener(aWindow, aOwner)
{
this.window = aWindow;
this.owner = aOwner;
}
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,
/**
* Initialize the window.console API observer.
*/
init: function CAL_init()
{
// 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 aMessage
* The message object receives from the observer service.
* @param string aTopic
* The message topic received from the observer service.
*/
observe: function CAL_observe(aMessage, aTopic)
{
if (!this.owner || !this.window) {
return;
}
let apiMessage = aMessage.wrappedJSObject;
let msgWindow = WebConsoleUtils.getWindowByOuterId(apiMessage.ID,
this.window);
if (!msgWindow || msgWindow.top != this.window) {
// Not the same window!
return;
}
this.owner.onConsoleAPICall(apiMessage);
},
/**
* Get the cached messages for the current inner window.
*
* @return array
* The array of cached messages. Each element is a Console API
* prepared to be sent to the remote Web Console instance.
*/
getCachedMessages: function CAL_getCachedMessages()
{
let innerWindowId = WebConsoleUtils.getInnerWindowId(this.window);
let messages = ConsoleAPIStorage.getEvents(innerWindowId);
return messages;
},
/**
* Destroy the console API listener.
*/
destroy: function CAL_destroy()
{
Services.obs.removeObserver(this, "console-api-log-event");
this.window = this.owner = null;
},
};
/**
* JSTerm helper functions.
*
* Defines a set of functions ("helper functions") that are available from the
* Web Console but not from the web page.
*
* A list of helper functions used by Firebug can be found here:
* http://getfirebug.com/wiki/index.php/Command_Line_API
*
* @param object aOwner
* The owning object.
*/
function JSTermHelpers(aOwner)
{
/**
* Find a node by ID.
*
* @param string aId
* The ID of the element you want.
* @return nsIDOMNode or null
* The result of calling document.querySelector(aSelector).
*/
aOwner.sandbox.$ = function JSTH_$(aSelector)
{
return aOwner.window.document.querySelector(aSelector);
};
/**
* Find the nodes matching a CSS selector.
*
* @param string aSelector
* A string that is passed to window.document.querySelectorAll.
* @return nsIDOMNodeList
* Returns the result of document.querySelectorAll(aSelector).
*/
aOwner.sandbox.$$ = function JSTH_$$(aSelector)
{
return aOwner.window.document.querySelectorAll(aSelector);
};
/**
* Runs an xPath query and returns all matched nodes.
*
* @param string aXPath
* xPath search query to execute.
* @param [optional] nsIDOMNode aContext
* Context to run the xPath query on. Uses window.document if not set.
* @return array of nsIDOMNode
*/
aOwner.sandbox.$x = function JSTH_$x(aXPath, aContext)
{
let nodes = [];
let doc = aOwner.window.document;
let aContext = aContext || doc;
try {
let results = doc.evaluate(aXPath, aContext, null,
Ci.nsIDOMXPathResult.ANY_TYPE, null);
let node;
while (node = results.iterateNext()) {
nodes.push(node);
}
}
catch (ex) {
aOwner.window.console.error(ex.message);
}
return nodes;
};
/**
* Returns the currently selected object in the highlighter.
*
* TODO: this implementation crosses the client/server boundaries! This is not
* usable within a remote browser. To implement this feature correctly we need
* support for remote inspection capabilities within the Inspector as well.
* See bug 787975.
*
* @return nsIDOMElement|null
* The DOM element currently selected in the highlighter.
*/
Object.defineProperty(aOwner.sandbox, "$0", {
get: function() {
try {
return aOwner.chromeWindow().InspectorUI.selection;
}
catch (ex) {
aOwner.window.console.error(ex.message);
}
},
enumerable: true,
configurable: false
});
/**
* Clears the output of the JSTerm.
*/
aOwner.sandbox.clear = function JSTH_clear()
{
aOwner.helperResult = {
type: "clearOutput",
};
};
/**
* Returns the result of Object.keys(aObject).
*
* @param object aObject
* Object to return the property names from.
* @return array of strings
*/
aOwner.sandbox.keys = function JSTH_keys(aObject)
{
return Object.keys(WebConsoleUtils.unwrap(aObject));
};
/**
* Returns the values of all properties on aObject.
*
* @param object aObject
* Object to display the values from.
* @return array of string
*/
aOwner.sandbox.values = function JSTH_values(aObject)
{
let arrValues = [];
let obj = WebConsoleUtils.unwrap(aObject);
try {
for (let prop in obj) {
arrValues.push(obj[prop]);
}
}
catch (ex) {
aOwner.window.console.error(ex.message);
}
return arrValues;
};
/**
* Opens a help window in MDN.
*/
aOwner.sandbox.help = function JSTH_help()
{
aOwner.helperResult = { type: "help" };
};
/**
* Inspects the passed aObject. This is done by opening the PropertyPanel.
*
* @param object aObject
* Object to inspect.
*/
aOwner.sandbox.inspect = function JSTH_inspect(aObject)
{
let obj = WebConsoleUtils.unwrap(aObject);
if (!WebConsoleUtils.isObjectInspectable(obj)) {
return aObject;
}
aOwner.helperResult = {
type: "inspectObject",
input: aOwner.evalInput,
object: aOwner.createValueGrip(obj),
};
};
/**
* Prints aObject to the output.
*
* @param object aObject
* Object to print to the output.
* @return string
*/
aOwner.sandbox.pprint = function JSTH_pprint(aObject)
{
if (aObject === null || aObject === undefined || aObject === true ||
aObject === false) {
aOwner.helperResult = {
type: "error",
message: "helperFuncUnsupportedTypeError",
};
return;
}
aOwner.helperResult = { rawOutput: true };
if (typeof aObject == "function") {
return aObject + "\n";
}
let output = [];
let getObjectGrip = WebConsoleUtils.getObjectGrip.bind(WebConsoleUtils);
let obj = WebConsoleUtils.unwrap(aObject);
let props = WebConsoleUtils.inspectObject(obj, getObjectGrip);
props.forEach(function(aProp) {
output.push(aProp.name + ": " +
WebConsoleUtils.getPropertyPanelValue(aProp));
});
return " " + output.join("\n ");
};
/**
* Print a string to the output, as-is.
*
* @param string aString
* A string you want to output.
* @return void
*/
aOwner.sandbox.print = function JSTH_print(aString)
{
aOwner.helperResult = { rawOutput: true };
return String(aString);
};
}

View File

@ -15,9 +15,25 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils",
"resource://gre/modules/devtools/WebConsoleUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PageErrorListener",
"resource://gre/modules/devtools/WebConsoleUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPIListener",
"resource://gre/modules/devtools/WebConsoleUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "JSTermHelpers",
"resource://gre/modules/devtools/WebConsoleUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "JSPropertyProvider",
"resource://gre/modules/devtools/WebConsoleUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPIStorage",
"resource://gre/modules/ConsoleAPIStorage.jsm");
/**
* The WebConsoleActor implements capabilities needed for the Web Console
* feature.
@ -32,6 +48,9 @@ function WebConsoleActor(aConnection, aTabActor)
{
this.conn = aConnection;
this._browser = aTabActor.browser;
this._objectActorsPool = new ActorPool(this.conn);
this.conn.addActorPool(this._objectActorsPool);
}
WebConsoleActor.prototype =
@ -43,6 +62,29 @@ WebConsoleActor.prototype =
*/
_browser: null,
/**
* Actor pool for all of the object actors for objects we send to the client.
* @private
* @type object
* @see ActorPool
* @see this.objectGrip()
*/
_objectActorsPool: null,
/**
* Tells the current page location associated to the sandbox. When the page
* location is changed, we recreate the sandbox.
* @private
* @type object
*/
_sandboxLocation: null,
/**
* The JavaScript Sandbox where code is evaluated.
* @type object
*/
sandbox: null,
/**
* The debugger server connection instance.
* @type object
@ -61,6 +103,11 @@ WebConsoleActor.prototype =
*/
pageErrorListener: null,
/**
* The ConsoleAPIListener instance.
*/
consoleAPIListener: null,
actorPrefix: "console",
grip: function WCA_grip()
@ -68,6 +115,24 @@ WebConsoleActor.prototype =
return { actor: this.actorID };
},
/**
* Tells if the window.console object is native or overwritten by script in
* the page.
*
* @return boolean
* True if the window.console object is native, or false otherwise.
*/
hasNativeConsoleAPI: function WCA_hasNativeConsoleAPI()
{
let isNative = false;
try {
let consoleObject = WebConsoleUtils.unwrap(this.window).console;
isNative = "__mozillaConsole__" in consoleObject;
}
catch (ex) { }
return isNative;
},
/**
* Destroy the current WebConsoleActor instance.
*/
@ -77,9 +142,69 @@ WebConsoleActor.prototype =
this.pageErrorListener.destroy();
this.pageErrorListener = null;
}
if (this.consoleAPIListener) {
this.consoleAPIListener.destroy();
this.consoleAPIListener = null;
}
this.conn.removeActorPool(this._objectActorsPool);
this._objectActorsPool = null;
this._sandboxLocation = this.sandbox = null;
this.conn = this._browser = null;
},
/**
* Create a grip for the given value. If the value is an object,
* a WebConsoleObjectActor will be created.
*
* @param mixed aValue
* @return object
*/
createValueGrip: function WCA_createValueGrip(aValue)
{
return WebConsoleUtils.createValueGrip(aValue,
this.createObjectActor.bind(this));
},
/**
* Create a grip for the given object.
*
* @param object aObject
* The object you want.
* @param object
* The object grip.
*/
createObjectActor: function WCA_createObjectActor(aObject)
{
// We need to unwrap the object, otherwise we cannot access the properties
// and methods added by the content scripts.
let obj = WebConsoleUtils.unwrap(aObject);
let actor = new WebConsoleObjectActor(obj, this);
this._objectActorsPool.addActor(actor);
return actor.grip();
},
/**
* Get an object actor by its ID.
*
* @param string aActorID
* @return object
*/
getObjectActorByID: function WCA_getObjectActorByID(aActorID)
{
return this._objectActorsPool.get(aActorID);
},
/**
* Release an object grip for the given object actor.
*
* @param object aActor
* The WebConsoleObjectActor instance you want to release.
*/
releaseObject: function WCA_releaseObject(aActor)
{
this._objectActorsPool.removeActor(aActor.actorID);
},
/**
* Handler for the "startListeners" request.
*
@ -103,9 +228,20 @@ WebConsoleActor.prototype =
}
startedListeners.push(listener);
break;
case "ConsoleAPI":
if (!this.consoleAPIListener) {
this.consoleAPIListener =
new ConsoleAPIListener(this.window, this);
this.consoleAPIListener.init();
}
startedListeners.push(listener);
break;
}
}
return { startedListeners: startedListeners };
return {
startedListeners: startedListeners,
nativeConsoleAPI: this.hasNativeConsoleAPI(),
};
},
/**
@ -123,7 +259,7 @@ WebConsoleActor.prototype =
// If no specific listeners are requested to be detached, we stop all
// listeners.
let toDetach = aRequest.listeners || ["PageError"];
let toDetach = aRequest.listeners || ["PageError", "ConsoleAPI"];
while (toDetach.length > 0) {
let listener = toDetach.shift();
@ -135,12 +271,209 @@ WebConsoleActor.prototype =
}
stoppedListeners.push(listener);
break;
case "ConsoleAPI":
if (this.consoleAPIListener) {
this.consoleAPIListener.destroy();
this.consoleAPIListener = null;
}
stoppedListeners.push(listener);
break;
}
}
return { stoppedListeners: stoppedListeners };
},
/**
* Handler for the "getCachedMessages" request. This method sends the cached
* error messages and the window.console API calls to the client.
*
* @param object aRequest
* The JSON request object received from the Web Console client.
* @return object
* The response packet to send to the client: it holds the cached
* messages array.
*/
onGetCachedMessages: function WCA_onGetCachedMessages(aRequest)
{
let types = aRequest.messageTypes;
if (!types) {
return {
error: "missingParameter",
message: "The messageTypes parameter is missing.",
};
}
let messages = [];
while (types.length > 0) {
let type = types.shift();
switch (type) {
case "ConsoleAPI":
if (this.consoleAPIListener) {
let cache = this.consoleAPIListener.getCachedMessages();
cache.forEach(function(aMessage) {
let message = this.prepareConsoleMessageForRemote(aMessage);
message._type = type;
messages.push(message);
}, this);
}
break;
case "PageError":
if (this.pageErrorListener) {
let cache = this.pageErrorListener.getCachedMessages();
cache.forEach(function(aMessage) {
let message = this.preparePageErrorForRemote(aMessage);
message._type = type;
messages.push(message);
}, this);
}
break;
}
}
messages.sort(function(a, b) { return a.timeStamp - b.timeStamp; });
return {
from: this.actorID,
messages: messages,
};
},
/**
* Handler for the "evaluateJS" request. This method evaluates the given
* JavaScript string and sends back the result.
*
* @param object aRequest
* The JSON request object received from the Web Console client.
* @return object
* The evaluation response packet.
*/
onEvaluateJS: function WCA_onEvaluateJS(aRequest)
{
let input = aRequest.text;
let result, error = null;
let timestamp;
this.helperResult = null;
this.evalInput = input;
try {
timestamp = Date.now();
result = this.evalInSandbox(input);
}
catch (ex) {
error = ex;
}
let helperResult = this.helperResult;
delete this.helperResult;
delete this.evalInput;
return {
from: this.actorID,
input: input,
result: this.createValueGrip(result),
timestamp: timestamp,
error: error,
errorMessage: error ? String(error) : null,
helperResult: helperResult,
};
},
/**
* The Autocomplete request handler.
*
* @param object aRequest
* The request message - what input to autocomplete.
* @return object
* The response message - matched properties.
*/
onAutocomplete: function WCA_onAutocomplete(aRequest)
{
let result = JSPropertyProvider(this.window, aRequest.text) || {};
return {
from: this.actorID,
matches: result.matches || [],
matchProp: result.matchProp,
};
},
/**
* The "clearMessagesCache" request handler.
*/
onClearMessagesCache: function WCA_onClearMessagesCache()
{
// TODO: Bug 717611 - Web Console clear button does not clear cached errors
let windowId = WebConsoleUtils.getInnerWindowId(this.window);
ConsoleAPIStorage.clearEvents(windowId);
return {};
},
/**
* Create the JavaScript sandbox where user input is evaluated.
* @private
*/
_createSandbox: function WCA__createSandbox()
{
this._sandboxLocation = this.window.location;
this.sandbox = new Cu.Sandbox(this.window, {
sandboxPrototype: this.window,
wantXrays: false,
});
this.sandbox.console = this.window.console;
JSTermHelpers(this);
},
/**
* Evaluates a string in the sandbox.
*
* @param string aString
* String to evaluate in the sandbox.
* @return mixed
* The result of the evaluation.
*/
evalInSandbox: function WCA_evalInSandbox(aString)
{
// If the user changed to a different location, we need to update the
// sandbox.
if (this._sandboxLocation !== this.window.location) {
this._createSandbox();
}
// The help function needs to be easy to guess, so we make the () optional
if (aString.trim() == "help" || aString.trim() == "?") {
aString = "help()";
}
let window = WebConsoleUtils.unwrap(this.sandbox.window);
let $ = null, $$ = null;
// We prefer to execute the page-provided implementations for the $() and
// $$() functions.
if (typeof window.$ == "function") {
$ = this.sandbox.$;
delete this.sandbox.$;
}
if (typeof window.$$ == "function") {
$$ = this.sandbox.$$;
delete this.sandbox.$$;
}
let result = Cu.evalInSandbox(aString, this.sandbox, "1.8",
"Web Console", 1);
if ($) {
this.sandbox.$ = $;
}
if ($$) {
this.sandbox.$$ = $$;
}
return result;
},
/**
* Handler for page errors received from the PageErrorListener. This method
* sends the nsIScriptError to the remote Web Console client.
@ -183,11 +516,165 @@ WebConsoleActor.prototype =
strict: !!(aPageError.flags & aPageError.strictFlag),
};
},
/**
* Handler for window.console API calls received from the ConsoleAPIListener.
* This method sends the object to the remote Web Console client.
*
* @param object aMessage
* The console API call we need to send to the remote client.
*/
onConsoleAPICall: function WCA_onConsoleAPICall(aMessage)
{
let packet = {
from: this.actorID,
type: "consoleAPICall",
message: this.prepareConsoleMessageForRemote(aMessage),
};
this.conn.send(packet);
},
/**
* Prepare a message from the console API to be sent to the remote Web Console
* instance.
*
* @param object aMessage
* The original message received from console-api-log-event.
* @return object
* The object that can be sent to the remote client.
*/
prepareConsoleMessageForRemote:
function WCA_prepareConsoleMessageForRemote(aMessage)
{
let result = {
level: aMessage.level,
filename: aMessage.filename,
lineNumber: aMessage.lineNumber,
functionName: aMessage.functionName,
timeStamp: aMessage.timeStamp,
};
switch (result.level) {
case "trace":
case "group":
case "groupCollapsed":
case "time":
case "timeEnd":
result.arguments = aMessage.arguments;
break;
default:
result.arguments = Array.map(aMessage.arguments || [],
function(aObj) {
return this.createValueGrip(aObj);
}, this);
if (result.level == "dir") {
result.objectProperties = [];
let first = result.arguments[0];
if (typeof first == "object" && first && first.inspectable) {
let actor = this.getObjectActorByID(first.actor);
result.objectProperties = actor.onInspectProperties().properties;
}
}
break;
}
return result;
},
/**
* Find the XUL window that owns the content window.
*
* @return Window
* The XUL window that owns the content window.
*/
chromeWindow: function WCA_chromeWindow()
{
return this.window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIDocShell)
.chromeEventHandler.ownerDocument.defaultView;
},
};
WebConsoleActor.prototype.requestTypes =
{
startListeners: WebConsoleActor.prototype.onStartListeners,
stopListeners: WebConsoleActor.prototype.onStopListeners,
getCachedMessages: WebConsoleActor.prototype.onGetCachedMessages,
evaluateJS: WebConsoleActor.prototype.onEvaluateJS,
autocomplete: WebConsoleActor.prototype.onAutocomplete,
clearMessagesCache: WebConsoleActor.prototype.onClearMessagesCache,
};
/**
* Creates an actor for the specified object.
*
* @constructor
* @param object aObj
* The object you want.
* @param object aWebConsoleActor
* The parent WebConsoleActor instance for this object.
*/
function WebConsoleObjectActor(aObj, aWebConsoleActor)
{
this.obj = aObj;
this.parent = aWebConsoleActor;
}
WebConsoleObjectActor.prototype =
{
actorPrefix: "consoleObj",
/**
* Returns a grip for this actor for returning in a protocol message.
*/
grip: function WCOA_grip()
{
let grip = WebConsoleUtils.getObjectGrip(this.obj);
grip.actor = this.actorID;
return grip;
},
/**
* Releases this actor from the pool.
*/
release: function WCOA_release()
{
this.parent.releaseObject(this);
this.parent = this.obj = null;
},
/**
* Handle a protocol request to inspect the properties of the object.
*
* @return object
* Message to send to the client. This holds the 'properties' property
* - an array with a descriptor for each property in the object.
*/
onInspectProperties: function WCOA_onInspectProperties()
{
// TODO: Bug 787981 - use LongStringActor for strings that are too long.
let createObjectActor = this.parent.createObjectActor.bind(this.parent);
let props = WebConsoleUtils.inspectObject(this.obj, createObjectActor);
return {
from: this.actorID,
properties: props,
};
},
/**
* Handle a protocol request to release a grip.
*/
onRelease: function WCOA_onRelease()
{
this.release();
return {};
},
};
WebConsoleObjectActor.prototype.requestTypes =
{
"inspectProperties": WebConsoleObjectActor.prototype.onInspectProperties,
"release": WebConsoleObjectActor.prototype.onRelease,
};