gecko-dev/browser/devtools/webconsole/HUDService.jsm

7045 lines
213 KiB
JavaScript
Raw Normal View History

/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is DevTools (HeadsUpDisplay) Console Code
*
* The Initial Developer of the Original Code is
* Mozilla Foundation
* Portions created by the Initial Developer are Copyright (C) 2010
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* David Dahl <ddahl@mozilla.com> (original author)
* Rob Campbell <rcampbell@mozilla.com>
* Johnathan Nightingale <jnightingale@mozilla.com>
* Patrick Walton <pcwalton@mozilla.com>
* Julian Viereck <jviereck@mozilla.com>
* Mihai Șucan <mihai.sucan@gmail.com>
* Michael Ratcliffe <mratcliffe@mozilla.com>
* Joe Walker <jwalker@mozilla.com>
* Sonny Piers <sonny.piers@gmail.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const CONSOLEAPI_CLASS_ID = "{b49c18f8-3379-4fc0-8c90-d7772c1a9ff3}";
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource:///modules/NetworkHelper.jsm");
var EXPORTED_SYMBOLS = ["HUDService", "ConsoleUtils"];
XPCOMUtils.defineLazyServiceGetter(this, "scriptError",
"@mozilla.org/scripterror;1",
"nsIScriptError");
XPCOMUtils.defineLazyServiceGetter(this, "activityDistributor",
"@mozilla.org/network/http-activity-distributor;1",
"nsIHttpActivityDistributor");
XPCOMUtils.defineLazyServiceGetter(this, "mimeService",
"@mozilla.org/mime;1",
"nsIMIMEService");
XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
"@mozilla.org/widget/clipboardhelper;1",
"nsIClipboardHelper");
XPCOMUtils.defineLazyGetter(this, "gcli", function () {
var obj = {};
Cu.import("resource:///modules/gcli.jsm", obj);
return obj.gcli;
});
XPCOMUtils.defineLazyGetter(this, "CssRuleView", function() {
let tmp = {};
Cu.import("resource:///modules/devtools/CssRuleView.jsm", tmp);
return tmp.CssRuleView;
});
XPCOMUtils.defineLazyGetter(this, "NetUtil", function () {
var obj = {};
Cu.import("resource://gre/modules/NetUtil.jsm", obj);
return obj.NetUtil;
});
XPCOMUtils.defineLazyGetter(this, "template", function () {
var obj = {};
Cu.import("resource:///modules/devtools/Templater.jsm", obj);
return obj.template;
});
XPCOMUtils.defineLazyGetter(this, "PropertyPanel", function () {
var obj = {};
try {
Cu.import("resource:///modules/PropertyPanel.jsm", obj);
} catch (err) {
Cu.reportError(err);
}
return obj.PropertyPanel;
});
XPCOMUtils.defineLazyGetter(this, "PropertyPanelAsync", function () {
let obj = {};
Cu.import("resource:///modules/PropertyPanelAsync.jsm", obj);
return obj.PropertyPanel;
});
XPCOMUtils.defineLazyGetter(this, "PropertyTreeViewAsync", function () {
let obj = {};
Cu.import("resource:///modules/PropertyPanelAsync.jsm", obj);
return obj.PropertyTreeView;
});
XPCOMUtils.defineLazyGetter(this, "AutocompletePopup", function () {
var obj = {};
try {
Cu.import("resource:///modules/AutocompletePopup.jsm", obj);
}
catch (err) {
Cu.reportError(err);
}
return obj.AutocompletePopup;
});
XPCOMUtils.defineLazyGetter(this, "ScratchpadManager", function () {
var obj = {};
try {
Cu.import("resource:///modules/devtools/scratchpad-manager.jsm", obj);
}
catch (err) {
Cu.reportError(err);
}
return obj.ScratchpadManager;
});
XPCOMUtils.defineLazyGetter(this, "namesAndValuesOf", function () {
var obj = {};
Cu.import("resource:///modules/PropertyPanel.jsm", obj);
return obj.namesAndValuesOf;
});
XPCOMUtils.defineLazyGetter(this, "WebConsoleUtils", function () {
let obj = {};
Cu.import("resource:///modules/WebConsoleUtils.jsm", obj);
return obj.WebConsoleUtils;
});
XPCOMUtils.defineLazyGetter(this, "l10n", function() {
return WebConsoleUtils.l10n;
});
function LogFactory(aMessagePrefix)
{
function log(aMessage) {
var _msg = aMessagePrefix + " " + aMessage + "\n";
dump(_msg);
}
return log;
}
let log = LogFactory("*** HUDService:");
const HUD_STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
XPCOMUtils.defineLazyGetter(this, "stringBundle", function () {
return Services.strings.createBundle(HUD_STRINGS_URI);
});
// The amount of time in milliseconds that must pass between messages to
// trigger the display of a new group.
const NEW_GROUP_DELAY = 5000;
// The amount of time in milliseconds that we wait before performing a live
// search.
const SEARCH_DELAY = 200;
// The number of lines that are displayed in the console output by default, for
// each category. The user can change this number by adjusting the hidden
// "devtools.hud.loglimit.{network,cssparser,exception,console}" preferences.
const DEFAULT_LOG_LIMIT = 200;
// The various categories of messages. We start numbering at zero so we can
// use these as indexes into the MESSAGE_PREFERENCE_KEYS matrix below.
const CATEGORY_NETWORK = 0;
const CATEGORY_CSS = 1;
const CATEGORY_JS = 2;
const CATEGORY_WEBDEV = 3;
const CATEGORY_INPUT = 4; // always on
const CATEGORY_OUTPUT = 5; // always on
// The possible message severities. As before, we start at zero so we can use
// these as indexes into MESSAGE_PREFERENCE_KEYS.
const SEVERITY_ERROR = 0;
const SEVERITY_WARNING = 1;
const SEVERITY_INFO = 2;
const SEVERITY_LOG = 3;
// A mapping from the console API log event levels to the Web Console
// severities.
const LEVELS = {
error: SEVERITY_ERROR,
warn: SEVERITY_WARNING,
info: SEVERITY_INFO,
log: SEVERITY_LOG,
trace: SEVERITY_LOG,
dir: SEVERITY_LOG,
group: SEVERITY_LOG,
groupCollapsed: SEVERITY_LOG,
groupEnd: SEVERITY_LOG,
time: SEVERITY_LOG,
timeEnd: SEVERITY_LOG
};
// The lowest HTTP response code (inclusive) that is considered an error.
const MIN_HTTP_ERROR_CODE = 400;
// The highest HTTP response code (exclusive) that is considered an error.
const MAX_HTTP_ERROR_CODE = 600;
// HTTP status codes.
const HTTP_MOVED_PERMANENTLY = 301;
const HTTP_FOUND = 302;
const HTTP_SEE_OTHER = 303;
const HTTP_TEMPORARY_REDIRECT = 307;
// The HTML namespace.
const HTML_NS = "http://www.w3.org/1999/xhtml";
// The XUL namespace.
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
// The fragment of a CSS class name that identifies each category.
const CATEGORY_CLASS_FRAGMENTS = [
"network",
"cssparser",
"exception",
"console",
"input",
"output",
];
// The fragment of a CSS class name that identifies each severity.
const SEVERITY_CLASS_FRAGMENTS = [
"error",
"warn",
"info",
"log",
];
// The preference keys to use for each category/severity combination, indexed
// first by category (rows) and then by severity (columns).
//
// Most of these rather idiosyncratic names are historical and predate the
// division of message type into "category" and "severity".
const MESSAGE_PREFERENCE_KEYS = [
// Error Warning Info Log
[ "network", null, null, "networkinfo", ], // Network
[ "csserror", "cssparser", null, null, ], // CSS
[ "exception", "jswarn", null, null, ], // JS
[ "error", "warn", "info", "log", ], // Web Developer
[ null, null, null, null, ], // Input
[ null, null, null, null, ], // Output
];
// Possible directions that can be passed to HUDService.animate().
const ANIMATE_OUT = 0;
const ANIMATE_IN = 1;
// Constants used for defining the direction of JSTerm input history navigation.
const HISTORY_BACK = -1;
const HISTORY_FORWARD = 1;
// The maximum number of bytes a Network ResponseListener can hold.
const RESPONSE_BODY_LIMIT = 1024*1024; // 1 MB
// The maximum uint32 value.
const PR_UINT32_MAX = 4294967295;
// Minimum console height, in pixels.
const MINIMUM_CONSOLE_HEIGHT = 150;
// Minimum page height, in pixels. This prevents the Web Console from
// remembering a height that covers the whole page.
const MINIMUM_PAGE_HEIGHT = 50;
// The default console height, as a ratio from the content window inner height.
const DEFAULT_CONSOLE_HEIGHT = 0.33;
// Constant used when checking the typeof objects.
const TYPEOF_FUNCTION = "function";
// This script is inserted into the content process.
const CONTENT_SCRIPT_URL = "chrome://browser/content/devtools/HUDService-content.js";
const ERRORS = { LOG_MESSAGE_MISSING_ARGS:
"Missing arguments: aMessage, aConsoleNode and aMessageNode are required.",
CANNOT_GET_HUD: "Cannot getHeads Up Display with provided ID",
MISSING_ARGS: "Missing arguments",
LOG_OUTPUT_FAILED: "Log Failure: Could not append messageNode to outputNode",
};
// The indent of a console group in pixels.
const GROUP_INDENT = 12;
// The pref prefix for webconsole filters
const PREFS_PREFIX = "devtools.webconsole.filter.";
/**
* Implements the nsIStreamListener and nsIRequestObserver interface. Used
* within the HS_httpObserverFactory function to get the response body of
* requests.
*
* The code is mostly based on code listings from:
*
* http://www.softwareishard.com/blog/firebug/
* nsitraceablechannel-intercept-http-traffic/
*
* @param object aHttpActivity
* HttpActivity object associated with this request (see
* HS_httpObserverFactory). As the response is done, the response header,
* body and status is stored on aHttpActivity.
*/
function ResponseListener(aHttpActivity) {
this.receivedData = "";
this.httpActivity = aHttpActivity;
}
ResponseListener.prototype =
{
/**
* The response will be written into the outputStream of this nsIPipe.
* Both ends of the pipe must be blocking.
*/
sink: null,
/**
* The HttpActivity object associated with this response.
*/
httpActivity: null,
/**
* Stores the received data as a string.
*/
receivedData: null,
/**
* The nsIRequest we are started for.
*/
request: null,
/**
* Sets the httpActivity object's response header if it isn't set already.
*
* @param nsIRequest aRequest
*/
setResponseHeader: function RL_setResponseHeader(aRequest)
{
let httpActivity = this.httpActivity;
// Check if the header isn't set yet.
if (!httpActivity.response.header) {
if (aRequest instanceof Ci.nsIHttpChannel) {
httpActivity.response.header = {};
try {
aRequest.visitResponseHeaders({
visitHeader: function(aName, aValue) {
httpActivity.response.header[aName] = aValue;
}
});
}
// Accessing the response header can throw an NS_ERROR_NOT_AVAILABLE
// exception. Catch it and stop it to make it not show up in the.
// This can happen if the response is not finished yet and the user
// reloades the page.
catch (ex) {
delete httpActivity.response.header;
}
}
}
},
/**
* Set the async listener for the given nsIAsyncInputStream. This allows us to
* wait asynchronously for any data coming from the stream.
*
* @param nsIAsyncInputStream aStream
* The input stream from where we are waiting for data to come in.
*
* @param nsIInputStreamCallback aListener
* The input stream callback you want. This is an object that must have
* the onInputStreamReady() method. If the argument is null, then the
* current callback is removed.
*
* @returns void
*/
setAsyncListener: function RL_setAsyncListener(aStream, aListener)
{
// Asynchronously wait for the stream to be readable or closed.
aStream.asyncWait(aListener, 0, 0, Services.tm.mainThread);
},
/**
* Stores the received data, if request/response body logging is enabled. It
* also does limit the number of stored bytes, based on the
* RESPONSE_BODY_LIMIT constant.
*
* Learn more about nsIStreamListener at:
* https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIStreamListener
*
* @param nsIRequest aRequest
* @param nsISupports aContext
* @param nsIInputStream aInputStream
* @param unsigned long aOffset
* @param unsigned long aCount
*/
onDataAvailable: function RL_onDataAvailable(aRequest, aContext, aInputStream,
aOffset, aCount)
{
this.setResponseHeader(aRequest);
let data = NetUtil.readInputStreamToString(aInputStream, aCount);
if (!this.httpActivity.response.bodyDiscarded &&
this.receivedData.length < RESPONSE_BODY_LIMIT) {
this.receivedData += NetworkHelper.
convertToUnicode(data, aRequest.contentCharset);
}
},
/**
* See documentation at
* https://developer.mozilla.org/En/NsIRequestObserver
*
* @param nsIRequest aRequest
* @param nsISupports aContext
*/
onStartRequest: function RL_onStartRequest(aRequest, aContext)
{
this.request = aRequest;
// Always discard the response body if logging is not enabled in the Web
// Console.
this.httpActivity.response.bodyDiscarded =
!HUDService.saveRequestAndResponseBodies;
// Check response status and discard the body for redirects.
if (!this.httpActivity.response.bodyDiscarded &&
this.httpActivity.channel instanceof Ci.nsIHttpChannel) {
switch (this.httpActivity.channel.responseStatus) {
case HTTP_MOVED_PERMANENTLY:
case HTTP_FOUND:
case HTTP_SEE_OTHER:
case HTTP_TEMPORARY_REDIRECT:
this.httpActivity.response.bodyDiscarded = true;
break;
}
}
// Asynchronously wait for the data coming from the request.
this.setAsyncListener(this.sink.inputStream, this);
},
/**
* Handle the onStopRequest by storing the response header is stored on the
* httpActivity object. The sink output stream is also closed.
*
* For more documentation about nsIRequestObserver go to:
* https://developer.mozilla.org/En/NsIRequestObserver
*
* @param nsIRequest aRequest
* The request we are observing.
* @param nsISupports aContext
* @param nsresult aStatusCode
*/
onStopRequest: function RL_onStopRequest(aRequest, aContext, aStatusCode)
{
// Retrieve the response headers, as they are, from the server.
let response = null;
for each (let item in HUDService.openResponseHeaders) {
if (item.channel === this.httpActivity.channel) {
response = item;
break;
}
}
if (response) {
this.httpActivity.response.header = response.headers;
delete HUDService.openResponseHeaders[response.id];
}
else {
this.setResponseHeader(aRequest);
}
this.sink.outputStream.close();
},
/**
* Clean up the response listener once the response input stream is closed.
* This is called from onStopRequest() or from onInputStreamReady() when the
* stream is closed.
*
* @returns void
*/
onStreamClose: function RL_onStreamClose()
{
if (!this.httpActivity) {
return;
}
// Remove our listener from the request input stream.
this.setAsyncListener(this.sink.inputStream, null);
if (!this.httpActivity.response.bodyDiscarded &&
HUDService.saveRequestAndResponseBodies) {
this.httpActivity.response.body = this.receivedData;
}
if (HUDService.lastFinishedRequestCallback) {
HUDService.lastFinishedRequestCallback(this.httpActivity);
}
// Call update on all panels.
this.httpActivity.panels.forEach(function(weakRef) {
let panel = weakRef.get();
if (panel) {
panel.update();
}
});
this.httpActivity.response.isDone = true;
this.httpActivity = null;
this.receivedData = "";
this.request = null;
this.sink = null;
this.inputStream = null;
},
/**
* The nsIInputStreamCallback for when the request input stream is ready -
* either it has more data or it is closed.
*
* @param nsIAsyncInputStream aStream
* The sink input stream from which data is coming.
*
* @returns void
*/
onInputStreamReady: function RL_onInputStreamReady(aStream)
{
if (!(aStream instanceof Ci.nsIAsyncInputStream) || !this.httpActivity) {
return;
}
let available = -1;
try {
// This may throw if the stream is closed normally or due to an error.
available = aStream.available();
}
catch (ex) { }
if (available != -1) {
if (available != 0) {
// Note that passing 0 as the offset here is wrong, but the
// onDataAvailable() method does not use the offset, so it does not
// matter.
this.onDataAvailable(this.request, null, aStream, 0, available);
}
this.setAsyncListener(aStream, this);
}
else {
this.onStreamClose();
}
},
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIStreamListener,
Ci.nsIInputStreamCallback,
Ci.nsIRequestObserver,
Ci.nsISupports,
])
}
///////////////////////////////////////////////////////////////////////////
//// Helper for creating the network panel.
/**
* Creates a DOMNode and sets all the attributes of aAttributes on the created
* element.
*
* @param nsIDOMDocument aDocument
* Document to create the new DOMNode.
* @param string aTag
* Name of the tag for the DOMNode.
* @param object aAttributes
* Attributes set on the created DOMNode.
*
* @returns nsIDOMNode
*/
function createElement(aDocument, aTag, aAttributes)
{
let node = aDocument.createElement(aTag);
if (aAttributes) {
for (let attr in aAttributes) {
node.setAttribute(attr, aAttributes[attr]);
}
}
return node;
}
/**
* Creates a new DOMNode and appends it to aParent.
*
* @param nsIDOMNode aParent
* A parent node to append the created element.
* @param string aTag
* Name of the tag for the DOMNode.
* @param object aAttributes
* Attributes set on the created DOMNode.
*
* @returns nsIDOMNode
*/
function createAndAppendElement(aParent, aTag, aAttributes)
{
let node = createElement(aParent.ownerDocument, aTag, aAttributes);
aParent.appendChild(node);
return node;
}
///////////////////////////////////////////////////////////////////////////
//// NetworkPanel
/**
* Creates a new NetworkPanel.
*
* @param nsIDOMNode aParent
* Parent node to append the created panel to.
* @param object aHttpActivity
* HttpActivity to display in the panel.
*/
function NetworkPanel(aParent, aHttpActivity)
{
let doc = aParent.ownerDocument;
this.httpActivity = aHttpActivity;
// Create the underlaying panel
this.panel = createElement(doc, "panel", {
label: l10n.getStr("NetworkPanel.label"),
titlebar: "normal",
noautofocus: "true",
noautohide: "true",
close: "true"
});
// Create the iframe that displays the NetworkPanel XHTML.
this.iframe = createAndAppendElement(this.panel, "iframe", {
src: "chrome://browser/content/NetworkPanel.xhtml",
type: "content",
flex: "1"
});
let self = this;
// Destroy the panel when it's closed.
this.panel.addEventListener("popuphidden", function onPopupHide() {
self.panel.removeEventListener("popuphidden", onPopupHide, false);
self.panel.parentNode.removeChild(self.panel);
self.panel = null;
self.iframe = null;
self.document = null;
self.httpActivity = null;
if (self.linkNode) {
self.linkNode._panelOpen = false;
self.linkNode = null;
}
}, false);
// Set the document object and update the content once the panel is loaded.
this.panel.addEventListener("load", function onLoad() {
self.panel.removeEventListener("load", onLoad, true);
self.document = self.iframe.contentWindow.document;
self.update();
}, true);
// Create the footer.
let footer = createElement(doc, "hbox", { align: "end" });
createAndAppendElement(footer, "spacer", { flex: 1 });
createAndAppendElement(footer, "resizer", { dir: "bottomend" });
this.panel.appendChild(footer);
aParent.appendChild(this.panel);
}
NetworkPanel.prototype =
{
/**
* Callback is called once the NetworkPanel is processed completly. Used by
* unit tests.
*/
isDoneCallback: null,
/**
* The current state of the output.
*/
_state: 0,
/**
* State variables.
*/
_INIT: 0,
_DISPLAYED_REQUEST_HEADER: 1,
_DISPLAYED_REQUEST_BODY: 2,
_DISPLAYED_RESPONSE_HEADER: 3,
_TRANSITION_CLOSED: 4,
_fromDataRegExp: /Content-Type\:\s*application\/x-www-form-urlencoded/,
/**
* Small helper function that is nearly equal to HUDService.getFormatStr
* except that it prefixes aName with "NetworkPanel.".
*
* @param string aName
* The name of an i10n string to format. This string is prefixed with
* "NetworkPanel." before calling the HUDService.getFormatStr function.
* @param array aArray
* Values used as placeholder for the i10n string.
* @returns string
* The i10n formated string.
*/
_format: function NP_format(aName, aArray)
{
return l10n.getFormatStr("NetworkPanel." + aName, aArray);
},
/**
* Returns the content type of the response body. This is based on the
* response.header["Content-Type"] info. If this value is not available, then
* the content type is tried to be estimated by the url file ending.
*
* @returns string or null
* Content type or null if no content type could be figured out.
*/
get _contentType()
{
let response = this.httpActivity.response;
let contentTypeValue = null;
if (response.header && response.header["Content-Type"]) {
let types = response.header["Content-Type"].split(/,|;/);
for (let i = 0; i < types.length; i++) {
let type = NetworkHelper.mimeCategoryMap[types[i]];
if (type) {
return types[i];
}
}
}
// Try to get the content type from the request file extension.
let uri = NetUtil.newURI(this.httpActivity.url);
let mimeType = null;
if ((uri instanceof Ci.nsIURL) && uri.fileExtension) {
try {
mimeType = mimeService.getTypeFromExtension(uri.fileExtension);
} catch(e) {
// Added to prevent failures on OS X 64. No Flash?
Cu.reportError(e);
// Return empty string to pass unittests.
return "";
}
}
return mimeType;
},
/**
*
* @returns boolean
* True if the response is an image, false otherwise.
*/
get _responseIsImage()
{
return NetworkHelper.mimeCategoryMap[this._contentType] == "image";
},
/**
*
* @returns boolean
* True if the response body contains text, false otherwise.
*/
get _isResponseBodyTextData()
{
let contentType = this._contentType;
if (!contentType)
return false;
if (contentType.indexOf("text/") == 0) {
return true;
}
switch (NetworkHelper.mimeCategoryMap[contentType]) {
case "txt":
case "js":
case "json":
case "css":
case "html":
case "svg":
case "xml":
return true;
default:
return false;
}
},
/**
*
* @returns boolean
* Returns true if the server responded that the request is already
* in the browser's cache, false otherwise.
*/
get _isResponseCached()
{
return this.httpActivity.response.status.indexOf("304") != -1;
},
/**
*
* @returns boolean
* Returns true if the posted body contains form data.
*/
get _isRequestBodyFormData()
{
let requestBody = this.httpActivity.request.body;
return this._fromDataRegExp.test(requestBody);
},
/**
* Appends the node with id=aId by the text aValue.
*
* @param string aId
* @param string aValue
* @returns void
*/
_appendTextNode: function NP_appendTextNode(aId, aValue)
{
let textNode = this.document.createTextNode(aValue);
this.document.getElementById(aId).appendChild(textNode);
},
/**
* Generates some HTML to display the key-value pair of the aList data. The
* generated HTML is added to node with id=aParentId.
*
* @param string aParentId
* Id of the parent node to append the list to.
* @oaram object aList
* Object that holds the key-value information to display in aParentId.
* @param boolean aIgnoreCookie
* If true, the key-value named "Cookie" is not added to the list.
* @returns void
*/
_appendList: function NP_appendList(aParentId, aList, aIgnoreCookie)
{
let parent = this.document.getElementById(aParentId);
let doc = this.document;
let sortedList = {};
Object.keys(aList).sort().forEach(function(aKey) {
sortedList[aKey] = aList[aKey];
});
for (let key in sortedList) {
if (aIgnoreCookie && key == "Cookie") {
continue;
}
/**
* The following code creates the HTML:
* <tr>
* <th scope="row" class="property-name">${line}:</th>
* <td class="property-value">${aList[line]}</td>
* </tr>
* and adds it to parent.
*/
let row = doc.createElement("tr");
let textNode = doc.createTextNode(key + ":");
let th = doc.createElement("th");
th.setAttribute("scope", "row");
th.setAttribute("class", "property-name");
th.appendChild(textNode);
row.appendChild(th);
textNode = doc.createTextNode(sortedList[key]);
let td = doc.createElement("td");
td.setAttribute("class", "property-value");
td.appendChild(textNode);
row.appendChild(td);
parent.appendChild(row);
}
},
/**
* Displays the node with id=aId.
*
* @param string aId
* @returns void
*/
_displayNode: function NP_displayNode(aId)
{
this.document.getElementById(aId).style.display = "block";
},
/**
* Sets the request URL, request method, the timing information when the
* request started and the request header content on the NetworkPanel.
* If the request header contains cookie data, a list of sent cookies is
* generated and a special sent cookie section is displayed + the cookie list
* added to it.
*
* @returns void
*/
_displayRequestHeader: function NP_displayRequestHeader()
{
let timing = this.httpActivity.timing;
let request = this.httpActivity.request;
this._appendTextNode("headUrl", this.httpActivity.url);
this._appendTextNode("headMethod", this.httpActivity.method);
this._appendTextNode("requestHeadersInfo",
l10n.timestampString(timing.REQUEST_HEADER/1000));
this._appendList("requestHeadersContent", request.header, true);
if ("Cookie" in request.header) {
this._displayNode("requestCookie");
let cookies = request.header.Cookie.split(";");
let cookieList = {};
let cookieListSorted = {};
cookies.forEach(function(cookie) {
let name, value;
[name, value] = cookie.trim().split("=");
cookieList[name] = value;
});
this._appendList("requestCookieContent", cookieList);
}
},
/**
* Displays the request body section of the NetworkPanel and set the request
* body content on the NetworkPanel.
*
* @returns void
*/
_displayRequestBody: function NP_displayRequestBody() {
this._displayNode("requestBody");
this._appendTextNode("requestBodyContent", this.httpActivity.request.body);
},
/*
* Displays the `sent form data` section. Parses the request header for the
* submitted form data displays it inside of the `sent form data` section.
*
* @returns void
*/
_displayRequestForm: function NP_processRequestForm() {
let requestBodyLines = this.httpActivity.request.body.split("\n");
let formData = requestBodyLines[requestBodyLines.length - 1].
replace(/\+/g, " ").split("&");
function unescapeText(aText)
{
try {
return decodeURIComponent(aText);
}
catch (ex) {
return decodeURIComponent(unescape(aText));
}
}
let formDataObj = {};
for (let i = 0; i < formData.length; i++) {
let data = formData[i];
let idx = data.indexOf("=");
let key = data.substring(0, idx);
let value = data.substring(idx + 1);
formDataObj[unescapeText(key)] = unescapeText(value);
}
this._appendList("requestFormDataContent", formDataObj);
this._displayNode("requestFormData");
},
/**
* Displays the response section of the NetworkPanel, sets the response status,
* the duration between the start of the request and the receiving of the
* response header as well as the response header content on the the NetworkPanel.
*
* @returns void
*/
_displayResponseHeader: function NP_displayResponseHeader()
{
let timing = this.httpActivity.timing;
let response = this.httpActivity.response;
this._appendTextNode("headStatus", response.status);
let deltaDuration =
Math.round((timing.RESPONSE_HEADER - timing.REQUEST_HEADER) / 1000);
this._appendTextNode("responseHeadersInfo",
this._format("durationMS", [deltaDuration]));
this._displayNode("responseContainer");
this._appendList("responseHeadersContent", response.header);
},
/**
* Displays the respones image section, sets the source of the image displayed
* in the image response section to the request URL and the duration between
* the receiving of the response header and the end of the request. Once the
* image is loaded, the size of the requested image is set.
*
* @returns void
*/
_displayResponseImage: function NP_displayResponseImage()
{
let self = this;
let timing = this.httpActivity.timing;
let response = this.httpActivity.response;
let cached = "";
if (this._isResponseCached) {
cached = "Cached";
}
let imageNode = this.document.getElementById("responseImage" + cached +"Node");
imageNode.setAttribute("src", this.httpActivity.url);
// This function is called to set the imageInfo.
function setImageInfo() {
let deltaDuration =
Math.round((timing.RESPONSE_COMPLETE - timing.RESPONSE_HEADER) / 1000);
self._appendTextNode("responseImage" + cached + "Info",
self._format("imageSizeDeltaDurationMS", [
imageNode.width, imageNode.height, deltaDuration
]
));
}
// Check if the image is already loaded.
if (imageNode.width != 0) {
setImageInfo();
}
else {
// Image is not loaded yet therefore add a load event.
imageNode.addEventListener("load", function imageNodeLoad() {
imageNode.removeEventListener("load", imageNodeLoad, false);
setImageInfo();
}, false);
}
this._displayNode("responseImage" + cached);
},
/**
* Displays the response body section, sets the the duration between
* the receiving of the response header and the end of the request as well as
* the content of the response body on the NetworkPanel.
*
* @param [optional] string aCachedContent
* Cached content for this request. If this argument is set, the
* responseBodyCached section is displayed.
* @returns void
*/
_displayResponseBody: function NP_displayResponseBody(aCachedContent)
{
let timing = this.httpActivity.timing;
let response = this.httpActivity.response;
let cached = "";
if (aCachedContent) {
cached = "Cached";
}
let deltaDuration =
Math.round((timing.RESPONSE_COMPLETE - timing.RESPONSE_HEADER) / 1000);
this._appendTextNode("responseBody" + cached + "Info",
this._format("durationMS", [deltaDuration]));
this._displayNode("responseBody" + cached);
this._appendTextNode("responseBody" + cached + "Content",
aCachedContent || response.body);
},
/**
* Displays the `Unknown Content-Type hint` and sets the duration between the
* receiving of the response header on the NetworkPanel.
*
* @returns void
*/
_displayResponseBodyUnknownType: function NP_displayResponseBodyUnknownType()
{
let timing = this.httpActivity.timing;
this._displayNode("responseBodyUnknownType");
let deltaDuration =
Math.round((timing.RESPONSE_COMPLETE - timing.RESPONSE_HEADER) / 1000);
this._appendTextNode("responseBodyUnknownTypeInfo",
this._format("durationMS", [deltaDuration]));
this._appendTextNode("responseBodyUnknownTypeContent",
this._format("responseBodyUnableToDisplay.content", [this._contentType]));
},
/**
* Displays the `no response body` section and sets the the duration between
* the receiving of the response header and the end of the request.
*
* @returns void
*/
_displayNoResponseBody: function NP_displayNoResponseBody()
{
let timing = this.httpActivity.timing;
this._displayNode("responseNoBody");
let deltaDuration =
Math.round((timing.RESPONSE_COMPLETE - timing.RESPONSE_HEADER) / 1000);
this._appendTextNode("responseNoBodyInfo",
this._format("durationMS", [deltaDuration]));
},
/*
* Calls the isDoneCallback function if one is specified.
*/
_callIsDone: function() {
if (this.isDoneCallback) {
this.isDoneCallback();
}
},
/**
* Updates the content of the NetworkPanel's iframe.
*
* @returns void
*/
update: function NP_update()
{
/**
* After the iframe's contentWindow is ready, the document object is set.
* If the document object isn't set yet, then the page is loaded and nothing
* can be updated.
*/
if (!this.document) {
return;
}
let timing = this.httpActivity.timing;
let request = this.httpActivity.request;
let response = this.httpActivity.response;
switch (this._state) {
case this._INIT:
this._displayRequestHeader();
this._state = this._DISPLAYED_REQUEST_HEADER;
// FALL THROUGH
case this._DISPLAYED_REQUEST_HEADER:
// Process the request body if there is one.
if (!request.bodyDiscarded && request.body) {
// Check if we send some form data. If so, display the form data special.
if (this._isRequestBodyFormData) {
this._displayRequestForm();
}
else {
this._displayRequestBody();
}
this._state = this._DISPLAYED_REQUEST_BODY;
}
// FALL THROUGH
case this._DISPLAYED_REQUEST_BODY:
// There is always a response header. Therefore we can skip here if
// we don't have a response header yet and don't have to try updating
// anything else in the NetworkPanel.
if (!response.header) {
break
}
this._displayResponseHeader();
this._state = this._DISPLAYED_RESPONSE_HEADER;
// FALL THROUGH
case this._DISPLAYED_RESPONSE_HEADER:
// Check if the transition is done.
if (timing.TRANSACTION_CLOSE && response.isDone) {
if (response.bodyDiscarded) {
this._callIsDone();
}
else if (this._responseIsImage) {
this._displayResponseImage();
this._callIsDone();
}
else if (!this._isResponseBodyTextData) {
this._displayResponseBodyUnknownType();
this._callIsDone();
}
else if (response.body) {
this._displayResponseBody();
this._callIsDone();
}
else if (this._isResponseCached) {
let self = this;
NetworkHelper.loadFromCache(this.httpActivity.url,
this.httpActivity.charset,
function(aContent) {
// If some content could be loaded from the cache, then display
// the body.
if (aContent) {
self._displayResponseBody(aContent);
self._callIsDone();
}
// Otherwise, show the "There is no response body" hint.
else {
self._displayNoResponseBody();
self._callIsDone();
}
});
}
else {
this._displayNoResponseBody();
this._callIsDone();
}
this._state = this._TRANSITION_CLOSED;
}
break;
}
}
}
///////////////////////////////////////////////////////////////////////////
//// Private utility functions for the HUD service
/**
* Ensures that the number of message nodes of type aCategory don't exceed that
* category's line limit by removing old messages as needed.
*
* @param aHUDId aHUDId
* The HeadsUpDisplay ID.
* @param integer aCategory
* The category of message nodes to limit.
* @return number
* The current user-selected log limit.
*/
function pruneConsoleOutputIfNecessary(aHUDId, aCategory)
{
// Get the log limit, either from the pref or from the constant.
let logLimit;
try {
let prefName = CATEGORY_CLASS_FRAGMENTS[aCategory];
logLimit = Services.prefs.getIntPref("devtools.hud.loglimit." + prefName);
} catch (e) {
logLimit = DEFAULT_LOG_LIMIT;
}
let hudRef = HUDService.getHudReferenceById(aHUDId);
let outputNode = hudRef.outputNode;
let scrollBox = outputNode.scrollBoxObject.element;
let oldScrollHeight = scrollBox.scrollHeight;
let scrolledToBottom = ConsoleUtils.isOutputScrolledToBottom(outputNode);
// Prune the nodes.
let messageNodes = outputNode.querySelectorAll(".webconsole-msg-" +
CATEGORY_CLASS_FRAGMENTS[aCategory]);
let removeNodes = messageNodes.length - logLimit;
for (let i = 0; i < removeNodes; i++) {
let node = messageNodes[i];
if (node._evalCacheId && !node._panelOpen) {
hudRef.jsterm.clearObjectCache(node._evalCacheId);
}
if (node.classList.contains("webconsole-msg-cssparser")) {
let desc = messageNodes[i].childNodes[2].textContent;
let location = "";
if (node.childNodes[4]) {
location = node.childNodes[4].getAttribute("title");
}
delete hudRef.cssNodes[desc + location];
}
else if (node.classList.contains("webconsole-msg-inspector")) {
hudRef.pruneConsoleDirNode(node);
continue;
}
node.parentNode.removeChild(node);
}
if (!scrolledToBottom && removeNodes > 0 &&
oldScrollHeight != scrollBox.scrollHeight) {
scrollBox.scrollTop -= oldScrollHeight - scrollBox.scrollHeight;
}
return logLimit;
}
///////////////////////////////////////////////////////////////////////////
//// The HUD service
function HUD_SERVICE()
{
// TODO: provide mixins for FENNEC: bug 568621
if (appName() == "FIREFOX") {
var mixins = new FirefoxApplicationHooks();
}
else {
throw new Error("Unsupported Application");
}
this.mixins = mixins;
// These methods access the "this" object, but they're registered as
// event listeners. So we hammer in the "this" binding.
this.onTabClose = this.onTabClose.bind(this);
this.onWindowUnload = this.onWindowUnload.bind(this);
// Remembers the last console height, in pixels.
this.lastConsoleHeight = Services.prefs.getIntPref("devtools.hud.height");
// Network response bodies are piped through a buffer of the given size (in
// bytes).
this.responsePipeSegmentSize =
Services.prefs.getIntPref("network.buffer.cache.size");
/**
* Collection of HUDIds that map to the tabs/windows/contexts
* that a HeadsUpDisplay can be activated for.
*/
this.activatedContexts = [];
/**
* Collection of outer window IDs mapping to HUD IDs.
*/
this.windowIds = {};
/**
* Each HeadsUpDisplay has a set of filter preferences
*/
this.filterPrefs = {};
/**
* Keeps a reference for each HeadsUpDisplay that is created
*/
this.hudReferences = {};
/**
* Requests that haven't finished yet.
*/
this.openRequests = {};
/**
* Response headers for requests that haven't finished yet.
*/
this.openResponseHeaders = {};
};
HUD_SERVICE.prototype =
{
/**
* getter for UI commands to be used by the frontend
*
* @returns object
*/
get consoleUI() {
return HeadsUpDisplayUICommands;
},
/**
* The sequencer is a generator (after initialization) that returns unique
* integers
*/
sequencer: null,
/**
* Whether to save the bodies of network requests and responses. Disabled by
* default to save memory.
*/
saveRequestAndResponseBodies: false,
/**
* Tell the HUDService that a HeadsUpDisplay can be activated
* for the window or context that has 'aContextDOMId' node id
*
* @param string aContextDOMId
* @return void
*/
registerActiveContext: function HS_registerActiveContext(aContextDOMId)
{
this.activatedContexts.push(aContextDOMId);
},
/**
* Firefox-specific current tab getter
*
* @returns nsIDOMWindow
*/
currentContext: function HS_currentContext() {
return this.mixins.getCurrentContext();
},
/**
* Tell the HUDService that a HeadsUpDisplay should be deactivated
*
* @param string aContextDOMId
* @return void
*/
unregisterActiveContext: function HS_deregisterActiveContext(aContextDOMId)
{
var domId = aContextDOMId.split("_")[1];
var idx = this.activatedContexts.indexOf(domId);
if (idx > -1) {
this.activatedContexts.splice(idx, 1);
}
},
/**
* Tells callers that a HeadsUpDisplay can be activated for the context
*
* @param string aContextDOMId
* @return boolean
*/
canActivateContext: function HS_canActivateContext(aContextDOMId)
{
var domId = aContextDOMId.split("_")[1];
for (var idx in this.activatedContexts) {
if (this.activatedContexts[idx] == domId){
return true;
}
}
return false;
},
/**
* Activate a HeadsUpDisplay for the given tab context.
*
* @param Element aContext the tab element.
* @param boolean aAnimated animate opening the Web Console?
* @returns void
*/
activateHUDForContext: function HS_activateHUDForContext(aContext, aAnimated)
{
this.wakeup();
let window = aContext.linkedBrowser.contentWindow;
let chromeDocument = aContext.ownerDocument;
let nBox = chromeDocument.defaultView.getNotificationBox(window);
this.registerActiveContext(nBox.id);
this.windowInitializer(window);
let hudId = "hud_" + nBox.id;
let hudRef = this.hudReferences[hudId];
if (!aAnimated || hudRef.consolePanel) {
this.disableAnimation(hudId);
}
// Create a processing instruction for GCLIs CSS stylesheet, but only if
// we don't have one for this document. Also record the context we're
// adding this for so we know when to remove it.
let procInstr = aContext.ownerDocument.gcliCssProcInstr;
if (!procInstr) {
procInstr = aContext.ownerDocument.createProcessingInstruction(
"xml-stylesheet",
"href='chrome://browser/skin/devtools/gcli.css' type='text/css'");
procInstr.contexts = [];
let root = aContext.ownerDocument.getElementsByTagName('window')[0];
root.parentNode.insertBefore(procInstr, root);
aContext.ownerDocument.gcliCssProcInstr = procInstr;
}
if (procInstr.contexts.indexOf(hudId) == -1) {
procInstr.contexts.push(hudId);
}
HeadsUpDisplayUICommands.refreshCommand();
},
/**
* Deactivate a HeadsUpDisplay for the given tab context.
*
* @param nsIDOMWindow aContext
* @param aAnimated animate closing the web console?
* @returns void
*/
deactivateHUDForContext: function HS_deactivateHUDForContext(aContext, aAnimated)
{
let browser = aContext.linkedBrowser;
let window = browser.contentWindow;
let chromeDocument = aContext.ownerDocument;
let nBox = chromeDocument.defaultView.getNotificationBox(window);
let hudId = "hud_" + nBox.id;
let displayNode = chromeDocument.getElementById(hudId);
let hudFound = (hudId in this.hudReferences) && displayNode;
if (hudFound) {
if (!aAnimated) {
this.storeHeight(hudId);
}
let hud = this.hudReferences[hudId];
browser.webProgress.removeProgressListener(hud.progressListener);
delete hud.progressListener;
this.unregisterDisplay(hudId);
window.focus();
HeadsUpDisplayUICommands.refreshCommand();
}
// Remove this context from the list of contexts that need the GCLI CSS
// processing instruction and then remove the processing instruction if it
// isn't needed any more.
let procInstr = aContext.ownerDocument.gcliCssProcInstr;
if (procInstr) {
procInstr.contexts = procInstr.contexts.filter(function(id) {
return id !== hudId;
});
if (procInstr.contexts.length == 0 && procInstr.parentNode) {
procInstr.parentNode.removeChild(procInstr);
delete aContext.ownerDocument.gcliCssProcInstr;
}
}
if (hudFound) {
let id = WebConsoleUtils.supportsString(hudId);
Services.obs.notifyObservers(id, "web-console-destroyed", null);
}
},
/**
* get a unique ID from the sequence generator
*
* @returns integer
*/
sequenceId: function HS_sequencerId()
{
if (!this.sequencer) {
this.sequencer = this.createSequencer(-1);
}
return this.sequencer.next();
},
/**
* get the default filter prefs
*
* @param string aHUDId
* @returns JS Object
*/
getDefaultFilterPrefs: function HS_getDefaultFilterPrefs(aHUDId) {
return this.filterPrefs[aHUDId];
},
/**
* get the current filter prefs
*
* @param string aHUDId
* @returns JS Object
*/
getFilterPrefs: function HS_getFilterPrefs(aHUDId) {
return this.filterPrefs[aHUDId];
},
/**
* get the filter state for a specific toggle button on a heads up display
*
* @param string aHUDId
* @param string aToggleType
* @returns boolean
*/
getFilterState: function HS_getFilterState(aHUDId, aToggleType)
{
if (!aHUDId) {
return false;
}
try {
var bool = this.filterPrefs[aHUDId][aToggleType];
return bool;
}
catch (ex) {
return false;
}
},
/**
* set the filter state for a specific toggle button on a heads up display
*
* @param string aHUDId
* @param string aToggleType
* @param boolean aState
* @returns void
*/
setFilterState: function HS_setFilterState(aHUDId, aToggleType, aState)
{
this.filterPrefs[aHUDId][aToggleType] = aState;
this.adjustVisibilityForMessageType(aHUDId, aToggleType, aState);
Services.prefs.setBoolPref(PREFS_PREFIX + aToggleType, aState);
},
/**
* Splits the given console messages into groups based on their timestamps.
*
* @param nsIDOMNode aOutputNode
* The output node to alter.
* @returns void
*/
regroupOutput: function HS_regroupOutput(aOutputNode)
{
// Go through the nodes and adjust the placement of "webconsole-new-group"
// classes.
let nodes = aOutputNode.querySelectorAll(".hud-msg-node" +
":not(.hud-filtered-by-string):not(.hud-filtered-by-type)");
let lastTimestamp;
for (let i = 0; i < nodes.length; i++) {
let thisTimestamp = nodes[i].timestamp;
if (lastTimestamp != null &&
thisTimestamp >= lastTimestamp + NEW_GROUP_DELAY) {
nodes[i].classList.add("webconsole-new-group");
}
else {
nodes[i].classList.remove("webconsole-new-group");
}
lastTimestamp = thisTimestamp;
}
},
/**
* Turns the display of log nodes on and off appropriately to reflect the
* adjustment of the message type filter named by @aPrefKey.
*
* @param string aHUDId
* The ID of the HUD to alter.
* @param string aPrefKey
* The preference key for the message type being filtered: one of the
* values in the MESSAGE_PREFERENCE_KEYS table.
* @param boolean aState
* True if the filter named by @aMessageType is being turned on; false
* otherwise.
* @returns void
*/
adjustVisibilityForMessageType:
function HS_adjustVisibilityForMessageType(aHUDId, aPrefKey, aState)
{
let outputNode = this.getHudReferenceById(aHUDId).outputNode;
let doc = outputNode.ownerDocument;
// Look for message nodes ("hud-msg-node") with the given preference key
// ("hud-msg-error", "hud-msg-cssparser", etc.) and add or remove the
// "hud-filtered-by-type" class, which turns on or off the display.
let xpath = ".//*[contains(@class, 'hud-msg-node') and " +
"contains(concat(@class, ' '), 'hud-" + aPrefKey + " ')]";
let result = doc.evaluate(xpath, outputNode, null,
Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
for (let i = 0; i < result.snapshotLength; i++) {
let node = result.snapshotItem(i);
if (aState) {
node.classList.remove("hud-filtered-by-type");
}
else {
node.classList.add("hud-filtered-by-type");
}
}
this.regroupOutput(outputNode);
},
/**
* Check that the passed string matches the filter arguments.
*
* @param String aString
* to search for filter words in.
* @param String aFilter
* is a string containing all of the words to filter on.
* @returns boolean
*/
stringMatchesFilters: function stringMatchesFilters(aString, aFilter)
{
if (!aFilter || !aString) {
return true;
}
let searchStr = aString.toLowerCase();
let filterStrings = aFilter.toLowerCase().split(/\s+/);
return !filterStrings.some(function (f) {
return searchStr.indexOf(f) == -1;
});
},
/**
* Turns the display of log nodes on and off appropriately to reflect the
* adjustment of the search string.
*
* @param string aHUDId
* The ID of the HUD to alter.
* @param string aSearchString
* The new search string.
* @returns void
*/
adjustVisibilityOnSearchStringChange:
function HS_adjustVisibilityOnSearchStringChange(aHUDId, aSearchString)
{
let outputNode = this.getHudReferenceById(aHUDId).outputNode;
let nodes = outputNode.querySelectorAll(".hud-msg-node");
for (let i = 0; i < nodes.length; ++i) {
let node = nodes[i];
// hide nodes that match the strings
let text = node.clipboardText;
// if the text matches the words in aSearchString...
if (this.stringMatchesFilters(text, aSearchString)) {
node.classList.remove("hud-filtered-by-string");
}
else {
node.classList.add("hud-filtered-by-string");
}
}
this.regroupOutput(outputNode);
},
/**
* Register a reference of each HeadsUpDisplay that is created
*/
registerHUDReference:
function HS_registerHUDReference(aHUD)
{
this.hudReferences[aHUD.hudId] = aHUD;
},
/**
* Register a new Heads Up Display
*
* @returns void
*/
registerDisplay: function HS_registerDisplay(aHUDId)
{
// register a display DOM node Id with the service.
if (!aHUDId){
throw new Error(ERRORS.MISSING_ARGS);
}
this.filterPrefs[aHUDId] = {
network: Services.prefs.getBoolPref(PREFS_PREFIX + "network"),
networkinfo: Services.prefs.getBoolPref(PREFS_PREFIX + "networkinfo"),
csserror: Services.prefs.getBoolPref(PREFS_PREFIX + "csserror"),
cssparser: Services.prefs.getBoolPref(PREFS_PREFIX + "cssparser"),
exception: Services.prefs.getBoolPref(PREFS_PREFIX + "exception"),
jswarn: Services.prefs.getBoolPref(PREFS_PREFIX + "jswarn"),
error: Services.prefs.getBoolPref(PREFS_PREFIX + "error"),
info: Services.prefs.getBoolPref(PREFS_PREFIX + "info"),
warn: Services.prefs.getBoolPref(PREFS_PREFIX + "warn"),
log: Services.prefs.getBoolPref(PREFS_PREFIX + "log"),
};
},
/**
* When a display is being destroyed, unregister it first
*
* @param string aHUDId
* The ID of a HUD.
* @returns void
*/
unregisterDisplay: function HS_unregisterDisplay(aHUDId)
{
let hud = this.getHudReferenceById(aHUDId);
// Remove children from the output. If the output is not cleared, there can
// be leaks as some nodes has node.onclick = function; set and GC can't
// remove the nodes then.
if (hud.jsterm) {
hud.jsterm.clearOutput();
}
if (hud.gcliterm) {
hud.gcliterm.clearOutput();
}
hud.destroy();
// Make sure that the console panel does not try to call
// deactivateHUDForContext() again.
hud.consoleWindowUnregisterOnHide = false;
// Remove the HUDBox and the consolePanel if the Web Console is inside a
// floating panel.
if (hud.consolePanel && hud.consolePanel.parentNode) {
hud.consolePanel.parentNode.removeChild(hud.consolePanel);
hud.consolePanel.removeAttribute("hudId");
hud.consolePanel = null;
}
hud.HUDBox.parentNode.removeChild(hud.HUDBox);
if (hud.splitter.parentNode) {
hud.splitter.parentNode.removeChild(hud.splitter);
}
if (hud.jsterm) {
hud.jsterm.autocompletePopup.destroy();
}
delete this.hudReferences[aHUDId];
for (let windowID in this.windowIds) {
if (this.windowIds[windowID] == aHUDId) {
delete this.windowIds[windowID];
}
}
this.unregisterActiveContext(aHUDId);
let popupset = hud.chromeDocument.getElementById("mainPopupSet");
let panels = popupset.querySelectorAll("panel[hudId=" + aHUDId + "]");
for (let i = 0; i < panels.length; i++) {
panels[i].hidePopup();
}
if (Object.keys(this.hudReferences).length == 0) {
let autocompletePopup = hud.chromeDocument.
getElementById("webConsole_autocompletePopup");
if (autocompletePopup) {
autocompletePopup.parentNode.removeChild(autocompletePopup);
}
this.suspend();
}
},
/**
* "Wake up" the Web Console activity. This is called when the first Web
* Console is open. This method initializes the various observers we have.
*
* @returns void
*/
wakeup: function HS_wakeup()
{
if (Object.keys(this.hudReferences).length > 0) {
return;
}
// begin observing HTTP traffic
this.startHTTPObservation();
HUDWindowObserver.init();
},
/**
* Suspend Web Console activity. This is called when all Web Consoles are
* closed.
*
* @returns void
*/
suspend: function HS_suspend()
{
activityDistributor.removeObserver(this.httpObserver);
delete this.httpObserver;
Services.obs.removeObserver(this.httpResponseExaminer,
"http-on-examine-response");
this.openRequests = {};
this.openResponseHeaders = {};
delete this.defaultFilterPrefs;
delete this.lastFinishedRequestCallback;
HUDWindowObserver.uninit();
},
/**
* Shutdown all HeadsUpDisplays on xpcom-shutdown
*
* @returns void
*/
shutdown: function HS_shutdown()
{
for (let hudId in this.hudReferences) {
this.deactivateHUDForContext(this.hudReferences[hudId].tab, false);
}
},
/**
* Returns the HeadsUpDisplay object associated to a content window.
*
* @param nsIDOMWindow aContentWindow
* @returns object
*/
getHudByWindow: function HS_getHudByWindow(aContentWindow)
{
let hudId = this.getHudIdByWindow(aContentWindow);
return hudId ? this.hudReferences[hudId] : null;
},
/**
* Returns the hudId that is corresponding to the hud activated for the
* passed aContentWindow. If there is no matching hudId null is returned.
*
* @param nsIDOMWindow aContentWindow
* @returns string or null
*/
getHudIdByWindow: function HS_getHudIdByWindow(aContentWindow)
{
let windowId = WebConsoleUtils.getOuterWindowId(aContentWindow);
return this.getHudIdByWindowId(windowId);
},
/**
* Returns the hudReference for a given id.
*
* @param string aId
* @returns Object
*/
getHudReferenceById: function HS_getHudReferenceById(aId)
{
return aId in this.hudReferences ? this.hudReferences[aId] : null;
},
/**
* Returns the hudId that is corresponding to the given outer window ID.
*
* @param number aWindowId
* the outer window ID
* @returns string the hudId
*/
getHudIdByWindowId: function HS_getHudIdByWindowId(aWindowId)
{
return this.windowIds[aWindowId];
},
/**
* Get the current filter string for the HeadsUpDisplay
*
* @param string aHUDId
* @returns string
*/
getFilterStringByHUDId: function HS_getFilterStringbyHUDId(aHUDId) {
return this.getHudReferenceById(aHUDId).filterBox.value;
},
/**
* Update the filter text in the internal tracking object for all
* filter strings
*
* @param nsIDOMNode aTextBoxNode
* @returns void
*/
updateFilterText: function HS_updateFiltertext(aTextBoxNode)
{
var hudId = aTextBoxNode.getAttribute("hudId");
this.adjustVisibilityOnSearchStringChange(hudId, aTextBoxNode.value);
},
/**
* Inform user that the Web Console API has been replaced by a script
* in a content page.
*
* @param string aHUDId
* The ID of the Web Console to which to send the message.
* @return void
*/
logWarningAboutReplacedAPI:
function HS_logWarningAboutReplacedAPI(aHUDId)
{
let hud = this.hudReferences[aHUDId];
let chromeDocument = hud.HUDBox.ownerDocument;
let message = stringBundle.GetStringFromName("ConsoleAPIDisabled");
let node = ConsoleUtils.createMessageNode(chromeDocument, CATEGORY_JS,
SEVERITY_WARNING, message,
aHUDId);
ConsoleUtils.outputMessageNode(node, aHUDId);
},
/**
* Register a Gecko app's specialized ApplicationHooks object
*
* @returns void or throws "UNSUPPORTED APPLICATION" error
*/
registerApplicationHooks:
function HS_registerApplications(aAppName, aHooksObject)
{
switch(aAppName) {
case "FIREFOX":
this.applicationHooks = aHooksObject;
return;
default:
throw new Error("MOZ APPLICATION UNSUPPORTED");
}
},
/**
* Registry of ApplicationHooks used by specified Gecko Apps
*
* @returns Specific Gecko 'ApplicationHooks' Object/Mixin
*/
applicationHooks: null,
/**
* Assign a function to this property to listen for finished httpRequests.
* Used by unit tests.
*/
lastFinishedRequestCallback: null,
/**
* Opens a NetworkPanel.
*
* @param nsIDOMNode aNode
* DOMNode to display the panel next to.
* @param object aHttpActivity
* httpActivity object. The data of this object is displayed in the
* NetworkPanel.
* @returns NetworkPanel
*/
openNetworkPanel: function HS_openNetworkPanel(aNode, aHttpActivity)
{
let doc = aNode.ownerDocument;
let parent = doc.getElementById("mainPopupSet");
let netPanel = new NetworkPanel(parent, aHttpActivity);
netPanel.linkNode = aNode;
let panel = netPanel.panel;
panel.openPopup(aNode, "after_pointer", 0, 0, false, false);
panel.sizeTo(450, 500);
panel.setAttribute("hudId", aHttpActivity.hudId);
aHttpActivity.panels.push(Cu.getWeakReference(netPanel));
return netPanel;
},
/**
* Begin observing HTTP traffic that we care about,
* namely traffic that originates inside any context that a Heads Up Display
* is active for.
*/
startHTTPObservation: function HS_httpObserverFactory()
{
// creates an observer for http traffic
var self = this;
var httpObserver = {
observeActivity :
function HS_SHO_observeActivity(aChannel,
aActivityType,
aActivitySubtype,
aTimestamp,
aExtraSizeData,
aExtraStringData)
{
if (aActivityType ==
activityDistributor.ACTIVITY_TYPE_HTTP_TRANSACTION ||
aActivityType ==
activityDistributor.ACTIVITY_TYPE_SOCKET_TRANSPORT) {
aChannel = aChannel.QueryInterface(Ci.nsIHttpChannel);
let transCodes = this.httpTransactionCodes;
let hudId;
if (aActivitySubtype ==
activityDistributor.ACTIVITY_SUBTYPE_REQUEST_HEADER ) {
// Try to get the source window of the request.
let win = NetworkHelper.getWindowForRequest(aChannel);
if (!win) {
return;
}
// Try to get the hudId that is associated to the window.
hudId = self.getHudIdByWindow(win.top);
if (!hudId) {
return;
}
// The httpActivity object will hold all information concerning
// this request and later response.
let httpActivity = {
id: self.sequenceId(),
hudId: hudId,
url: aChannel.URI.spec,
method: aChannel.requestMethod,
channel: aChannel,
charset: win.document.characterSet,
panels: [],
request: {
header: { }
},
response: {
header: null
},
timing: {
"REQUEST_HEADER": aTimestamp
}
};
// Add a new output entry.
let loggedNode = self.logNetActivity(httpActivity);
// In some cases loggedNode can be undefined (e.g. if an image was
// requested). Don't continue in such a case.
if (!loggedNode) {
return;
}
aChannel.QueryInterface(Ci.nsITraceableChannel);
// The response will be written into the outputStream of this pipe.
// This allows us to buffer the data we are receiving and read it
// asynchronously.
// Both ends of the pipe must be blocking.
let sink = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
// The streams need to be blocking because this is required by the
// stream tee.
sink.init(false, false, HUDService.responsePipeSegmentSize,
PR_UINT32_MAX, null);
// Add listener for the response body.
let newListener = new ResponseListener(httpActivity);
// Remember the input stream, so it isn't released by GC.
newListener.inputStream = sink.inputStream;
newListener.sink = sink;
let tee = Cc["@mozilla.org/network/stream-listener-tee;1"].
createInstance(Ci.nsIStreamListenerTee);
let originalListener = aChannel.setNewListener(tee);
tee.init(originalListener, sink.outputStream, newListener);
// Copy the request header data.
aChannel.visitRequestHeaders({
visitHeader: function(aName, aValue) {
httpActivity.request.header[aName] = aValue;
}
});
// Store the loggedNode and the httpActivity object for later reuse.
let linkNode = loggedNode.querySelector(".webconsole-msg-link");
httpActivity.messageObject = {
messageNode: loggedNode,
linkNode: linkNode
};
self.openRequests[httpActivity.id] = httpActivity;
// Make the network span clickable.
linkNode.setAttribute("aria-haspopup", "true");
linkNode.addEventListener("mousedown", function(aEvent) {
this._startX = aEvent.clientX;
this._startY = aEvent.clientY;
}, false);
linkNode.addEventListener("click", function(aEvent) {
if (aEvent.detail != 1 || aEvent.button != 0 ||
(this._startX != aEvent.clientX &&
this._startY != aEvent.clientY)) {
return;
}
if (!this._panelOpen) {
self.openNetworkPanel(this, httpActivity);
this._panelOpen = true;
}
}, false);
}
else {
// Iterate over all currently ongoing requests. If aChannel can't
// be found within them, then exit this function.
let httpActivity = null;
for each (let item in self.openRequests) {
if (item.channel !== aChannel) {
continue;
}
httpActivity = item;
break;
}
if (!httpActivity) {
return;
}
hudId = httpActivity.hudId;
let msgObject = httpActivity.messageObject;
let updatePanel = false;
let data;
// Store the time information for this activity subtype.
httpActivity.timing[transCodes[aActivitySubtype]] = aTimestamp;
switch (aActivitySubtype) {
case activityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT: {
if (!self.saveRequestAndResponseBodies) {
httpActivity.request.bodyDiscarded = true;
break;
}
let gBrowser = msgObject.messageNode.ownerDocument.
defaultView.gBrowser;
let HUD = HUDService.hudReferences[hudId];
let browser = gBrowser.
getBrowserForDocument(HUD.contentDocument);
let sentBody = NetworkHelper.
readPostTextFromRequest(aChannel, browser);
if (!sentBody) {
// If the request URL is the same as the current page url, then
// we can try to get the posted text from the page directly.
// This check is necessary as otherwise the
// NetworkHelper.readPostTextFromPage
// function is called for image requests as well but these
// are not web pages and as such don't store the posted text
// in the cache of the webpage.
if (httpActivity.url == browser.contentWindow.location.href) {
sentBody = NetworkHelper.readPostTextFromPage(browser);
}
if (!sentBody) {
sentBody = "";
}
}
httpActivity.request.body = sentBody;
break;
}
case activityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER: {
// aExtraStringData contains the response header. The first line
// contains the response status (e.g. HTTP/1.1 200 OK).
//
// Note: The response header is not saved here. Calling the
// aChannel.visitResponseHeaders at this point sometimes
// causes an NS_ERROR_NOT_AVAILABLE exception. Therefore,
// the response header and response body is stored on the
// httpActivity object within the RL_onStopRequest function.
httpActivity.response.status =
aExtraStringData.split(/\r\n|\n|\r/)[0];
// Add the response status.
let linkNode = msgObject.linkNode;
let statusNode = linkNode.
querySelector(".webconsole-msg-status");
let statusText = "[" + httpActivity.response.status + "]";
statusNode.setAttribute("value", statusText);
let clipboardTextPieces =
[ httpActivity.method, httpActivity.url, statusText ];
msgObject.messageNode.clipboardText =
clipboardTextPieces.join(" ");
let status = parseInt(httpActivity.response.status.
replace(/^HTTP\/\d\.\d (\d+).+$/, "$1"));
if (status >= MIN_HTTP_ERROR_CODE &&
status < MAX_HTTP_ERROR_CODE) {
ConsoleUtils.setMessageType(msgObject.messageNode,
CATEGORY_NETWORK,
SEVERITY_ERROR);
}
break;
}
case activityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE: {
let timing = httpActivity.timing;
let requestDuration =
Math.round((timing.RESPONSE_COMPLETE -
timing.REQUEST_HEADER) / 1000);
// Add the request duration.
let linkNode = msgObject.linkNode;
let statusNode = linkNode.
querySelector(".webconsole-msg-status");
let statusText = httpActivity.response.status;
let timeText = l10n.getFormatStr("NetworkPanel.durationMS",
[ requestDuration ]);
let fullStatusText = "[" + statusText + " " + timeText + "]";
statusNode.setAttribute("value", fullStatusText);
let clipboardTextPieces =
[ httpActivity.method, httpActivity.url, fullStatusText ];
msgObject.messageNode.clipboardText =
clipboardTextPieces.join(" ");
delete httpActivity.messageObject;
delete self.openRequests[httpActivity.id];
updatePanel = true;
break;
}
}
if (updatePanel) {
httpActivity.panels.forEach(function(weakRef) {
let panel = weakRef.get();
if (panel) {
panel.update();
}
});
}
}
}
},
httpTransactionCodes: {
0x5001: "REQUEST_HEADER",
0x5002: "REQUEST_BODY_SENT",
0x5003: "RESPONSE_START",
0x5004: "RESPONSE_HEADER",
0x5005: "RESPONSE_COMPLETE",
0x5006: "TRANSACTION_CLOSE",
0x804b0003: "STATUS_RESOLVING",
0x804b000b: "STATUS_RESOLVED",
0x804b0007: "STATUS_CONNECTING_TO",
0x804b0004: "STATUS_CONNECTED_TO",
0x804b0005: "STATUS_SENDING_TO",
0x804b000a: "STATUS_WAITING_FOR",
0x804b0006: "STATUS_RECEIVING_FROM"
}
};
this.httpObserver = httpObserver;
activityDistributor.addObserver(httpObserver);
// This is used to find the correct HTTP response headers.
Services.obs.addObserver(this.httpResponseExaminer,
"http-on-examine-response", false);
},
/**
* Observe notifications for the http-on-examine-response topic, coming from
* the nsIObserver service.
*
* @param string aTopic
* @param nsIHttpChannel aSubject
* @returns void
*/
httpResponseExaminer: function HS_httpResponseExaminer(aSubject, aTopic)
{
if (aTopic != "http-on-examine-response" ||
!(aSubject instanceof Ci.nsIHttpChannel)) {
return;
}
let channel = aSubject.QueryInterface(Ci.nsIHttpChannel);
let win = NetworkHelper.getWindowForRequest(channel);
if (!win) {
return;
}
let hudId = HUDService.getHudIdByWindow(win);
if (!hudId) {
return;
}
let response = {
id: HUDService.sequenceId(),
hudId: hudId,
channel: channel,
headers: {},
};
try {
channel.visitResponseHeaders({
visitHeader: function(aName, aValue) {
response.headers[aName] = aValue;
}
});
}
catch (ex) {
delete response.headers;
}
if (response.headers) {
HUDService.openResponseHeaders[response.id] = response;
}
},
/**
* Logs network activity.
*
* @param object aActivityObject
* The activity to log.
* @returns void
*/
logNetActivity: function HS_logNetActivity(aActivityObject)
{
let hudId = aActivityObject.hudId;
let outputNode = this.hudReferences[hudId].outputNode;
let chromeDocument = outputNode.ownerDocument;
let msgNode = chromeDocument.createElementNS(XUL_NS, "hbox");
let methodNode = chromeDocument.createElementNS(XUL_NS, "label");
methodNode.setAttribute("value", aActivityObject.method);
methodNode.classList.add("webconsole-msg-body-piece");
msgNode.appendChild(methodNode);
let linkNode = chromeDocument.createElementNS(XUL_NS, "hbox");
linkNode.setAttribute("flex", "1");
linkNode.classList.add("webconsole-msg-body-piece");
linkNode.classList.add("webconsole-msg-link");
msgNode.appendChild(linkNode);
let urlNode = chromeDocument.createElementNS(XUL_NS, "label");
urlNode.setAttribute("crop", "center");
urlNode.setAttribute("flex", "1");
urlNode.setAttribute("title", aActivityObject.url);
urlNode.setAttribute("value", aActivityObject.url);
urlNode.classList.add("hud-clickable");
urlNode.classList.add("webconsole-msg-body-piece");
urlNode.classList.add("webconsole-msg-url");
linkNode.appendChild(urlNode);
let statusNode = chromeDocument.createElementNS(XUL_NS, "label");
statusNode.setAttribute("value", "");
statusNode.classList.add("hud-clickable");
statusNode.classList.add("webconsole-msg-body-piece");
statusNode.classList.add("webconsole-msg-status");
linkNode.appendChild(statusNode);
let clipboardText = aActivityObject.method + " " + aActivityObject.url;
let messageNode = ConsoleUtils.createMessageNode(chromeDocument,
CATEGORY_NETWORK,
SEVERITY_LOG,
msgNode,
hudId,
null,
null,
clipboardText);
ConsoleUtils.outputMessageNode(messageNode, aActivityObject.hudId);
return messageNode;
},
/**
* Initialize the JSTerm object to create a JS Workspace by attaching the UI
* into the given parent node, using the mixin.
*
* @param nsIDOMWindow aContext the context used for evaluating user input
* @param nsIDOMNode aParentNode where to attach the JSTerm
* @param object aConsole
* Console object used within the JSTerm instance to report errors
* and log data (by calling console.error(), console.log(), etc).
*/
initializeJSTerm: function HS_initializeJSTerm(aContext, aParentNode, aConsole)
{
// create Initial JS Workspace:
var context = Cu.getWeakReference(aContext);
// Attach the UI into the target parent node using the mixin.
var firefoxMixin = new JSTermFirefoxMixin(context, aParentNode);
var jsTerm = new JSTerm(context, aParentNode, firefoxMixin, aConsole);
// TODO: injection of additional functionality needs re-thinking/api
// see bug 559748
},
/**
* Creates a generator that always returns a unique number for use in the
* indexes
*
* @returns Generator
*/
createSequencer: function HS_createSequencer(aInt)
{
function sequencer(aInt)
{
while(1) {
aInt++;
yield aInt;
}
}
return sequencer(aInt);
},
// See jsapi.h (JSErrorReport flags):
// http://mxr.mozilla.org/mozilla-central/source/js/src/jsapi.h#3429
scriptErrorFlags: {
0: "error", // JSREPORT_ERROR
1: "warn", // JSREPORT_WARNING
2: "exception", // JSREPORT_EXCEPTION
4: "error", // JSREPORT_STRICT | JSREPORT_ERROR
5: "warn", // JSREPORT_STRICT | JSREPORT_WARNING
8: "error", // JSREPORT_STRICT_MODE_ERROR
13: "warn", // JSREPORT_STRICT_MODE_ERROR | JSREPORT_WARNING | JSREPORT_ERROR
},
/**
* replacement strings (L10N)
*/
scriptMsgLogLevel: {
0: "typeError", // JSREPORT_ERROR
1: "typeWarning", // JSREPORT_WARNING
2: "typeException", // JSREPORT_EXCEPTION
4: "typeError", // JSREPORT_STRICT | JSREPORT_ERROR
5: "typeStrict", // JSREPORT_STRICT | JSREPORT_WARNING
8: "typeError", // JSREPORT_STRICT_MODE_ERROR
13: "typeWarning", // JSREPORT_STRICT_MODE_ERROR | JSREPORT_WARNING | JSREPORT_ERROR
},
/**
* onTabClose event handler function
*
* @param aEvent
* @returns void
*/
onTabClose: function HS_onTabClose(aEvent)
{
this.deactivateHUDForContext(aEvent.target, false);
},
/**
* Called whenever a browser window closes. Cleans up any consoles still
* around.
*
* @param nsIDOMEvent aEvent
* The dispatched event.
* @returns void
*/
onWindowUnload: function HS_onWindowUnload(aEvent)
{
let window = aEvent.target.defaultView;
window.removeEventListener("unload", this.onWindowUnload, false);
let gBrowser = window.gBrowser;
let tabContainer = gBrowser.tabContainer;
tabContainer.removeEventListener("TabClose", this.onTabClose, false);
let tab = tabContainer.firstChild;
while (tab != null) {
this.deactivateHUDForContext(tab, false);
tab = tab.nextSibling;
}
if (window.webConsoleCommandController) {
window.controllers.removeController(window.webConsoleCommandController);
window.webConsoleCommandController = null;
}
},
/**
* windowInitializer - checks what Gecko app is running and inits the HUD
*
* @param nsIDOMWindow aContentWindow
* @returns void
*/
windowInitializer: function HS_WindowInitalizer(aContentWindow)
{
var xulWindow = aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.chromeEventHandler.ownerDocument.defaultView;
let xulWindow = WebConsoleUtils.unwrap(xulWindow);
let docElem = xulWindow.document.documentElement;
if (!docElem || docElem.getAttribute("windowtype") != "navigator:browser" ||
!xulWindow.gBrowser) {
// Do not do anything unless we have a browser window.
// This may be a view-source window or other type of non-browser window.
return;
}
let gBrowser = xulWindow.gBrowser;
let _browser = gBrowser.
getBrowserForDocument(aContentWindow.top.document);
// ignore newly created documents that don't belong to a tab's browser
if (!_browser) {
return;
}
let nBox = gBrowser.getNotificationBox(_browser);
let nBoxId = nBox.getAttribute("id");
let hudId = "hud_" + nBoxId;
let windowUI = nBox.ownerDocument.getElementById("console_window_" + hudId);
if (windowUI) {
// The Web Console popup is already open, no need to continue.
if (aContentWindow == aContentWindow.top) {
let hud = this.hudReferences[hudId];
hud.reattachConsole(aContentWindow);
}
return;
}
if (!this.canActivateContext(hudId)) {
return;
}
xulWindow.addEventListener("unload", this.onWindowUnload, false);
gBrowser.tabContainer.addEventListener("TabClose", this.onTabClose, false);
this.registerDisplay(hudId);
let hudNode;
let childNodes = nBox.childNodes;
for (let i = 0; i < childNodes.length; i++) {
let id = childNodes[i].getAttribute("id");
// `id` is a string with the format "hud_<number>".
if (id.split("_")[0] == "hud") {
hudNode = childNodes[i];
break;
}
}
let hud;
// If there is no HUD for this tab create a new one.
if (!hudNode) {
// get nBox object and call new HUD
let config = { parentNode: nBox,
contentWindow: aContentWindow.top
};
hud = new HeadsUpDisplay(config);
HUDService.registerHUDReference(hud);
let windowId = WebConsoleUtils.getOuterWindowId(aContentWindow.top);
this.windowIds[windowId] = hudId;
hud.progressListener = new ConsoleProgressListener(hudId);
_browser.webProgress.addProgressListener(hud.progressListener,
Ci.nsIWebProgress.NOTIFY_STATE_ALL);
}
else {
hud = this.hudReferences[hudId];
if (aContentWindow == aContentWindow.top) {
// TODO: name change?? doesn't actually re-attach the console
hud.reattachConsole(aContentWindow);
}
}
// Need to detect that the console component has been paved over.
let consoleObject = WebConsoleUtils.unwrap(aContentWindow).console;
if (!("__mozillaConsole__" in consoleObject))
this.logWarningAboutReplacedAPI(hudId);
// register the controller to handle "select all" properly
this.createController(xulWindow);
},
/**
* Adds the command controller to the XUL window if it's not already present.
*
* @param nsIDOMWindow aWindow
* The browser XUL window.
* @returns void
*/
createController: function HUD_createController(aWindow)
{
if (aWindow.webConsoleCommandController == null) {
aWindow.webConsoleCommandController = new CommandController(aWindow);
aWindow.controllers.insertControllerAt(0,
aWindow.webConsoleCommandController);
}
},
/**
* Animates the Console appropriately.
*
* @param string aHUDId The ID of the console.
* @param string aDirection Whether to animate the console appearing
* (ANIMATE_IN) or disappearing (ANIMATE_OUT).
* @param function aCallback An optional callback, which will be called with
* the "transitionend" event passed as a parameter once the animation
* finishes.
*/
animate: function HS_animate(aHUDId, aDirection, aCallback)
{
let hudBox = this.getHudReferenceById(aHUDId).HUDBox;
if (!hudBox.classList.contains("animated")) {
if (aCallback) {
aCallback();
}
return;
}
switch (aDirection) {
case ANIMATE_OUT:
hudBox.style.height = 0;
break;
case ANIMATE_IN:
this.resetHeight(aHUDId);
break;
}
if (aCallback) {
hudBox.addEventListener("transitionend", aCallback, false);
}
},
/**
* Disables all animation for a console, for unit testing. After this call,
* the console will instantly take on a reasonable height, and the close
* animation will not occur.
*
* @param string aHUDId The ID of the console.
*/
disableAnimation: function HS_disableAnimation(aHUDId)
{
let hudBox = HUDService.hudReferences[aHUDId].HUDBox;
if (hudBox.classList.contains("animated")) {
hudBox.classList.remove("animated");
this.resetHeight(aHUDId);
}
},
/**
* Reset the height of the Web Console.
*
* @param string aHUDId The ID of the Web Console.
*/
resetHeight: function HS_resetHeight(aHUDId)
{
let HUD = this.hudReferences[aHUDId];
let innerHeight = HUD.contentWindow.innerHeight;
let chromeWindow = HUD.chromeDocument.defaultView;
if (!HUD.consolePanel) {
let splitterStyle = chromeWindow.getComputedStyle(HUD.splitter, null);
innerHeight += parseInt(splitterStyle.height) +
parseInt(splitterStyle.borderTopWidth) +
parseInt(splitterStyle.borderBottomWidth);
}
let boxStyle = chromeWindow.getComputedStyle(HUD.HUDBox, null);
innerHeight += parseInt(boxStyle.height) +
parseInt(boxStyle.borderTopWidth) +
parseInt(boxStyle.borderBottomWidth);
let height = this.lastConsoleHeight > 0 ? this.lastConsoleHeight :
Math.ceil(innerHeight * DEFAULT_CONSOLE_HEIGHT);
if ((innerHeight - height) < MINIMUM_PAGE_HEIGHT) {
height = innerHeight - MINIMUM_PAGE_HEIGHT;
}
else if (height < MINIMUM_CONSOLE_HEIGHT) {
height = MINIMUM_CONSOLE_HEIGHT;
}
HUD.HUDBox.style.height = height + "px";
},
/**
* Remember the height of the given Web Console, such that it can later be
* reused when other Web Consoles are open.
*
* @param string aHUDId The ID of the Web Console.
*/
storeHeight: function HS_storeHeight(aHUDId)
{
let hudBox = this.hudReferences[aHUDId].HUDBox;
let window = hudBox.ownerDocument.defaultView;
let style = window.getComputedStyle(hudBox, null);
let height = parseInt(style.height);
height += parseInt(style.borderTopWidth);
height += parseInt(style.borderBottomWidth);
this.lastConsoleHeight = height;
let pref = Services.prefs.getIntPref("devtools.hud.height");
if (pref > -1) {
Services.prefs.setIntPref("devtools.hud.height", height);
}
},
/**
* Copies the selected items to the system clipboard.
*
* @param nsIDOMNode aOutputNode
* The output node.
* @returns void
*/
copySelectedItems: function HS_copySelectedItems(aOutputNode)
{
// Gather up the selected items and concatenate their clipboard text.
let strings = [];
let newGroup = false;
let children = aOutputNode.children;
for (let i = 0; i < children.length; i++) {
let item = children[i];
if (!item.selected) {
continue;
}
// Add dashes between groups so that group boundaries show up in the
// copied output.
if (i > 0 && item.classList.contains("webconsole-new-group")) {
newGroup = true;
}
// Ensure the selected item hasn't been filtered by type or string.
if (!item.classList.contains("hud-filtered-by-type") &&
!item.classList.contains("hud-filtered-by-string")) {
let timestampString = l10n.timestampString(item.timestamp);
if (newGroup) {
strings.push("--");
newGroup = false;
}
strings.push("[" + timestampString + "] " + item.clipboardText);
}
}
clipboardHelper.copyString(strings.join("\n"));
}
};
//////////////////////////////////////////////////////////////////////////
// HeadsUpDisplay
//////////////////////////////////////////////////////////////////////////
/*
* HeadsUpDisplay is an interactive console initialized *per tab* that
* displays console log data as well as provides an interactive terminal to
* manipulate the current tab's document content.
* */
function HeadsUpDisplay(aConfig)
{
// sample config: { parentNode: aDOMNode,
// // or
// parentNodeId: "myHUDParent123",
//
// placement: "appendChild"
// // or
// placement: "insertBefore",
// placementChildNodeIndex: 0,
// }
this.HUDBox = null;
if (aConfig.parentNode) {
// TODO: need to replace these DOM calls with internal functions
// that operate on each application's node structure
// better yet, we keep these functions in a "bridgeModule" or the HUDService
// to keep a registry of nodeGetters for each application
// see bug 568647
this.parentNode = aConfig.parentNode;
this.notificationBox = aConfig.parentNode;
this.chromeDocument = aConfig.parentNode.ownerDocument;
this.contentWindow = aConfig.contentWindow;
this.uriSpec = aConfig.contentWindow.location.href;
this.hudId = "hud_" + aConfig.parentNode.getAttribute("id");
}
else {
// parentNodeId is the node's id where we attach the HUD
// TODO: is the "navigator:browser" below used in all Gecko Apps?
// see bug 568647
let windowEnum = Services.wm.getEnumerator("navigator:browser");
let parentNode;
let contentDocument;
let contentWindow;
let chromeDocument;
// TODO: the following part is still very Firefox specific
// see bug 568647
while (windowEnum.hasMoreElements()) {
let window = windowEnum.getNext();
try {
let gBrowser = window.gBrowser;
let _browsers = gBrowser.browsers;
let browserLen = _browsers.length;
for (var i = 0; i < browserLen; i++) {
var _notificationBox = gBrowser.getNotificationBox(_browsers[i]);
this.notificationBox = _notificationBox;
if (_notificationBox.getAttribute("id") == aConfig.parentNodeId) {
this.parentNodeId = _notificationBox.getAttribute("id");
this.hudId = "hud_" + this.parentNodeId;
parentNode = _notificationBox;
this.contentDocument =
_notificationBox.childNodes[0].contentDocument;
this.contentWindow =
_notificationBox.childNodes[0].contentWindow;
this.uriSpec = aConfig.contentWindow.location.href;
this.chromeDocument =
_notificationBox.ownerDocument;
break;
}
}
}
catch (ex) {
Cu.reportError(ex);
}
if (parentNode) {
break;
}
}
if (!parentNode) {
throw new Error(this.ERRORS.PARENTNODE_NOT_FOUND);
}
this.parentNode = parentNode;
this.notificationBox = parentNode;
}
// create textNode Factory:
this.textFactory = NodeFactory("text", "xul", this.chromeDocument);
this.chromeWindow = this.chromeDocument.defaultView;
this.browser = this.tab.linkedBrowser;
this.messageManager = this.browser.messageManager;
// Track callback functions registered for specific async requests sent to the
// content process.
this.asyncRequests = {};
// create a panel dynamically and attach to the parentNode
this.createHUD();
this.HUDBox.lastTimestamp = 0;
// create the JSTerm input element
try {
this.createConsoleInput(this.contentWindow, this.consoleWrap, this.outputNode);
if (this.jsterm) {
this.jsterm.inputNode.focus();
}
if (this.gcliterm) {
this.gcliterm.inputNode.focus();
}
}
catch (ex) {
Cu.reportError(ex);
}
// A cache for tracking repeated CSS Nodes.
this.cssNodes = {};
this._setupMessageManager();
}
HeadsUpDisplay.prototype = {
/**
* Message names that the HUD listens for. These messages come from the remote
* Web Console content script.
*
* @private
* @type array
*/
_messageListeners: ["JSTerm:EvalObject", "WebConsole:ConsoleAPI",
"WebConsole:CachedMessages", "WebConsole:PageError"],
consolePanel: null,
/**
* The nesting depth of the currently active console group.
*/
groupDepth: 0,
get mainPopupSet()
{
return this.chromeDocument.getElementById("mainPopupSet");
},
/**
* Get the tab associated to the HeadsUpDisplay object.
*/
get tab()
{
// TODO: we should only keep a reference to the tab object and use
// getters to determine the rest of objects we need - the chrome window,
// document, etc. We should simplify the entire code to use only a single
// tab object ref. See bug 656231.
let tab = null;
let id = this.notificationBox.id;
Array.some(this.chromeDocument.defaultView.gBrowser.tabs, function(aTab) {
if (aTab.linkedPanel == id) {
tab = aTab;
return true;
}
});
return tab;
},
/**
* Create a panel to open the web console if it should float above
* the content in its own window.
*/
createOwnWindowPanel: function HUD_createOwnWindowPanel()
{
if (this.uiInOwnWindow) {
return this.consolePanel;
}
let width = 0;
try {
width = Services.prefs.getIntPref("devtools.webconsole.width");
}
catch (ex) {}
if (width < 1) {
width = this.HUDBox.clientWidth || this.contentWindow.innerWidth;
}
let height = this.HUDBox.clientHeight;
let top = 0;
try {
top = Services.prefs.getIntPref("devtools.webconsole.top");
}
catch (ex) {}
let left = 0;
try {
left = Services.prefs.getIntPref("devtools.webconsole.left");
}
catch (ex) {}
let panel = this.chromeDocument.createElementNS(XUL_NS, "panel");
let config = { id: "console_window_" + this.hudId,
label: this.getPanelTitle(),
titlebar: "normal",
noautohide: "true",
norestorefocus: "true",
close: "true",
flex: "1",
hudId: this.hudId,
width: width,
position: "overlap",
top: top,
left: left,
};
for (let attr in config) {
panel.setAttribute(attr, config[attr]);
}
panel.classList.add("web-console-panel");
let onPopupShown = (function HUD_onPopupShown() {
panel.removeEventListener("popupshown", onPopupShown, false);
// Make sure that the HUDBox size updates when the panel is resized.
let height = panel.clientHeight;
this.HUDBox.style.height = "auto";
this.HUDBox.setAttribute("flex", "1");
panel.setAttribute("height", height);
// Scroll the outputNode back to the last location.
if (lastIndex > -1 && lastIndex < this.outputNode.getRowCount()) {
this.outputNode.ensureIndexIsVisible(lastIndex);
}
if (this.jsterm) {
this.jsterm.inputNode.focus();
}
if (this.gcliterm) {
this.gcliterm.inputNode.focus();
}
}).bind(this);
panel.addEventListener("popupshown", onPopupShown,false);
let onPopupHidden = (function HUD_onPopupHidden(aEvent) {
if (aEvent.target != panel) {
return;
}
panel.removeEventListener("popuphidden", onPopupHidden, false);
let width = 0;
try {
width = Services.prefs.getIntPref("devtools.webconsole.width");
}
catch (ex) { }
if (width > 0) {
Services.prefs.setIntPref("devtools.webconsole.width", panel.clientWidth);
}
// Are we destroying the HUD or repositioning it?
if (this.consoleWindowUnregisterOnHide) {
HUDService.deactivateHUDForContext(this.tab, false);
} else {
this.consoleWindowUnregisterOnHide = true;
}
}).bind(this);
panel.addEventListener("popuphidden", onPopupHidden, false);
let lastIndex = -1;
if (this.outputNode.getIndexOfFirstVisibleRow) {
lastIndex = this.outputNode.getIndexOfFirstVisibleRow() +
this.outputNode.getNumberOfVisibleRows() - 1;
}
if (this.splitter.parentNode) {
this.splitter.parentNode.removeChild(this.splitter);
}
panel.appendChild(this.HUDBox);
let space = this.chromeDocument.createElement("spacer");
space.setAttribute("flex", "1");
let bottomBox = this.chromeDocument.createElement("hbox");
space.setAttribute("flex", "1");
let resizer = this.chromeDocument.createElement("resizer");
resizer.setAttribute("dir", "bottomend");
resizer.setAttribute("element", config.id);
bottomBox.appendChild(space);
bottomBox.appendChild(resizer);
panel.appendChild(bottomBox);
this.mainPopupSet.appendChild(panel);
Services.prefs.setCharPref("devtools.webconsole.position", "window");
panel.openPopup(null, "overlay", left, top, false, false);
this.consolePanel = panel;
this.consoleWindowUnregisterOnHide = true;
return panel;
},
/**
* Retrieve the Web Console panel title.
*
* @return string
* The Web Console panel title.
*/
getPanelTitle: function HUD_getPanelTitle()
{
return l10n.getFormatStr("webConsoleWindowTitleAndURL", [this.uriSpec]);
},
positions: {
above: 0, // the childNode index
below: 2,
window: null
},
consoleWindowUnregisterOnHide: true,
/**
* Re-position the console
*/
positionConsole: function HUD_positionConsole(aPosition)
{
if (!(aPosition in this.positions)) {
throw new Error("Incorrect argument: " + aPosition +
". Cannot position Web Console");
}
if (aPosition == "window") {
let closeButton = this.consoleFilterToolbar.
querySelector(".webconsole-close-button");
closeButton.setAttribute("hidden", "true");
this.createOwnWindowPanel();
this.positionMenuitems.window.setAttribute("checked", true);
if (this.positionMenuitems.last) {
this.positionMenuitems.last.setAttribute("checked", false);
}
this.positionMenuitems.last = this.positionMenuitems[aPosition];
this.uiInOwnWindow = true;
return;
}
let height = this.HUDBox.clientHeight;
// get the node position index
let nodeIdx = this.positions[aPosition];
let nBox = this.notificationBox;
let node = nBox.childNodes[nodeIdx];
// check to see if console is already positioned in aPosition
if (node == this.HUDBox) {
return;
}
let lastIndex = -1;
if (this.outputNode.getIndexOfFirstVisibleRow) {
lastIndex = this.outputNode.getIndexOfFirstVisibleRow() +
this.outputNode.getNumberOfVisibleRows() - 1;
}
// remove the console and splitter and reposition
if (this.splitter.parentNode) {
this.splitter.parentNode.removeChild(this.splitter);
}
if (aPosition == "below") {
nBox.appendChild(this.splitter);
nBox.appendChild(this.HUDBox);
}
else {
nBox.insertBefore(this.splitter, node);
nBox.insertBefore(this.HUDBox, this.splitter);
}
this.positionMenuitems[aPosition].setAttribute("checked", true);
if (this.positionMenuitems.last) {
this.positionMenuitems.last.setAttribute("checked", false);
}
this.positionMenuitems.last = this.positionMenuitems[aPosition];
Services.prefs.setCharPref("devtools.webconsole.position", aPosition);
if (lastIndex > -1 && lastIndex < this.outputNode.getRowCount()) {
this.outputNode.ensureIndexIsVisible(lastIndex);
}
let closeButton = this.consoleFilterToolbar.
getElementsByClassName("webconsole-close-button")[0];
closeButton.removeAttribute("hidden");
this.uiInOwnWindow = false;
if (this.consolePanel) {
// must destroy the consolePanel
this.consoleWindowUnregisterOnHide = false;
this.consolePanel.hidePopup();
this.consolePanel.parentNode.removeChild(this.consolePanel);
this.consolePanel = null; // remove this as we're not in panel anymore
this.HUDBox.removeAttribute("flex");
this.HUDBox.removeAttribute("height");
this.HUDBox.style.height = height + "px";
}
if (this.jsterm) {
this.jsterm.inputNode.focus();
}
if (this.gcliterm) {
this.gcliterm.inputNode.focus();
}
},
/**
* The JSTerm object that contains the console's inputNode
*
*/
jsterm: null,
/**
* The GcliTerm object that contains the console's GCLI
*/
gcliterm: null,
/**
* creates and attaches the console input node
*
* @param nsIDOMWindow aWindow
* @returns void
*/
createConsoleInput:
function HUD_createConsoleInput(aWindow, aParentNode, aExistingConsole)
{
let usegcli = false;
try {
// usegcli = Services.prefs.getBoolPref("devtools.gcli.enable");
}
catch (ex) {}
if (appName() == "FIREFOX") {
if (!usegcli) {
let context = Cu.getWeakReference(aWindow);
let mixin = new JSTermFirefoxMixin(context, aParentNode,
aExistingConsole);
this.jsterm = new JSTerm(context, aParentNode, mixin, this.console);
}
else {
this.gcliterm = new GcliTerm(aWindow, this.hudId, this.chromeDocument,
this.console, this.hintNode, this.consoleWrap);
aParentNode.appendChild(this.gcliterm.element);
}
}
else {
throw new Error("Unsupported Gecko Application");
}
},
/**
* 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 HUD__displayCachedConsoleMessages(aRemoteMessages)
{
if (!aRemoteMessages.length) {
return;
}
// Turn off scrolling for the moment.
ConsoleUtils.scroll = false;
this.outputNode.hidden = true;
aRemoteMessages.forEach(function(aMessage) {
switch (aMessage._type) {
case "PageError":
this.reportPageError(aMessage);
break;
case "ConsoleAPI":
this.logConsoleAPIMessage(aMessage);
break;
}
}, this);
this.outputNode.hidden = false;
ConsoleUtils.scroll = true;
// Scroll to bottom.
let numChildren = this.outputNode.childNodes.length;
if (numChildren && this.outputNode.clientHeight) {
// We also check the clientHeight to force a reflow, otherwise
// ensureIndexIsVisible() does not work after outputNode.hidden = false.
this.outputNode.ensureIndexIsVisible(numChildren - 1);
}
},
/**
* Re-attaches a console when the contentWindow is recreated
*
* @param nsIDOMWindow aContentWindow
* @returns void
*/
reattachConsole: function HUD_reattachConsole(aContentWindow)
{
this.contentWindow = aContentWindow;
this.contentDocument = this.contentWindow.document;
this.uriSpec = this.contentWindow.location.href;
if (this.consolePanel) {
this.consolePanel.label = this.getPanelTitle();
}
if (this.jsterm) {
this.jsterm.context = Cu.getWeakReference(this.contentWindow);
this.jsterm.console = this.console;
this.jsterm.createSandbox();
}
else if (this.gcliterm) {
this.gcliterm.reattachConsole(this.contentWindow, this.console);
}
else {
this.createConsoleInput(this.contentWindow, this.consoleWrap, this.outputNode);
}
},
/**
* Shortcut to make XUL nodes
*
* @param string aTag
* @returns nsIDOMNode
*/
makeXULNode:
function HUD_makeXULNode(aTag)
{
return this.chromeDocument.createElement(aTag);
},
/**
* Build the UI of each HeadsUpDisplay
*
* @returns nsIDOMNode
*/
makeHUDNodes: function HUD_makeHUDNodes()
{
let self = this;
this.splitter = this.makeXULNode("splitter");
this.splitter.setAttribute("class", "hud-splitter");
this.HUDBox = this.makeXULNode("vbox");
this.HUDBox.setAttribute("id", this.hudId);
this.HUDBox.setAttribute("class", "hud-box animated");
this.HUDBox.style.height = 0;
let outerWrap = this.makeXULNode("vbox");
outerWrap.setAttribute("class", "hud-outer-wrapper");
outerWrap.setAttribute("flex", "1");
let consoleCommandSet = this.makeXULNode("commandset");
outerWrap.appendChild(consoleCommandSet);
let consoleWrap = this.makeXULNode("vbox");
this.consoleWrap = consoleWrap;
consoleWrap.setAttribute("class", "hud-console-wrapper");
consoleWrap.setAttribute("flex", "1");
this.filterSpacer = this.makeXULNode("spacer");
this.filterSpacer.setAttribute("flex", "1");
this.filterBox = this.makeXULNode("textbox");
this.filterBox.setAttribute("class", "compact hud-filter-box");
this.filterBox.setAttribute("hudId", this.hudId);
this.filterBox.setAttribute("placeholder", l10n.getStr("stringFilter"));
this.filterBox.setAttribute("type", "search");
this.setFilterTextBoxEvents();
this.createConsoleMenu(this.consoleWrap);
this.filterPrefs = HUDService.getDefaultFilterPrefs(this.hudId);
let consoleFilterToolbar = this.makeFilterToolbar();
consoleFilterToolbar.setAttribute("id", "viewGroup");
this.consoleFilterToolbar = consoleFilterToolbar;
this.hintNode = this.makeXULNode("vbox");
this.hintNode.setAttribute("class", "gcliterm-hint-node");
let hintParentNode = this.makeXULNode("vbox");
hintParentNode.setAttribute("flex", "0");
hintParentNode.setAttribute("class", "gcliterm-hint-parent");
hintParentNode.setAttribute("pack", "end");
hintParentNode.appendChild(this.hintNode);
hintParentNode.hidden = true;
let hbox = this.makeXULNode("hbox");
hbox.setAttribute("flex", "1");
hbox.setAttribute("class", "gcliterm-display");
this.outputNode = this.makeXULNode("richlistbox");
this.outputNode.setAttribute("class", "hud-output-node");
this.outputNode.setAttribute("flex", "1");
this.outputNode.setAttribute("orient", "vertical");
this.outputNode.setAttribute("context", this.hudId + "-output-contextmenu");
this.outputNode.setAttribute("style", "direction: ltr;");
this.outputNode.setAttribute("seltype", "multiple");
hbox.appendChild(hintParentNode);
hbox.appendChild(this.outputNode);
consoleWrap.appendChild(consoleFilterToolbar);
consoleWrap.appendChild(hbox);
outerWrap.appendChild(consoleWrap);
this.HUDBox.lastTimestamp = 0;
this.jsTermParentNode = outerWrap;
this.HUDBox.appendChild(outerWrap);
return this.HUDBox;
},
/**
* sets the click events for all binary toggle filter buttons
*
* @returns void
*/
setFilterTextBoxEvents: function HUD_setFilterTextBoxEvents()
{
var filterBox = this.filterBox;
function onChange()
{
// To improve responsiveness, we let the user finish typing before we
// perform the search.
if (this.timer == null) {
let timerClass = Cc["@mozilla.org/timer;1"];
this.timer = timerClass.createInstance(Ci.nsITimer);
} else {
this.timer.cancel();
}
let timerEvent = {
notify: function setFilterTextBoxEvents_timerEvent_notify() {
HUDService.updateFilterText(filterBox);
}
};
this.timer.initWithCallback(timerEvent, SEARCH_DELAY,
Ci.nsITimer.TYPE_ONE_SHOT);
}
filterBox.addEventListener("command", onChange, false);
filterBox.addEventListener("input", onChange, false);
},
/**
* Make the filter toolbar where we can toggle logging filters
*
* @returns nsIDOMNode
*/
makeFilterToolbar: function HUD_makeFilterToolbar()
{
const BUTTONS = [
{
name: "PageNet",
category: "net",
severities: [
{ name: "ConsoleErrors", prefKey: "network" },
{ name: "ConsoleLog", prefKey: "networkinfo" }
]
},
{
name: "PageCSS",
category: "css",
severities: [
{ name: "ConsoleErrors", prefKey: "csserror" },
{ name: "ConsoleWarnings", prefKey: "cssparser" }
]
},
{
name: "PageJS",
category: "js",
severities: [
{ name: "ConsoleErrors", prefKey: "exception" },
{ name: "ConsoleWarnings", prefKey: "jswarn" }
]
},
{
name: "PageLogging",
category: "logging",
severities: [
{ name: "ConsoleErrors", prefKey: "error" },
{ name: "ConsoleWarnings", prefKey: "warn" },
{ name: "ConsoleInfo", prefKey: "info" },
{ name: "ConsoleLog", prefKey: "log" }
]
}
];
let toolbar = this.makeXULNode("toolbar");
toolbar.setAttribute("class", "hud-console-filter-toolbar");
toolbar.setAttribute("mode", "full");
#ifdef XP_MACOSX
this.makeCloseButton(toolbar);
#endif
for (let i = 0; i < BUTTONS.length; i++) {
this.makeFilterButton(toolbar, BUTTONS[i]);
}
toolbar.appendChild(this.filterSpacer);
let positionUI = this.createPositionUI();
toolbar.appendChild(positionUI);
toolbar.appendChild(this.filterBox);
this.makeClearConsoleButton(toolbar);
#ifndef XP_MACOSX
this.makeCloseButton(toolbar);
#endif
return toolbar;
},
/**
* Creates the UI for re-positioning the console
*
* @return nsIDOMNode
* The toolbarbutton which holds the menu that allows the user to
* change the console position.
*/
createPositionUI: function HUD_createPositionUI()
{
this._positionConsoleAbove = (function HUD_positionAbove() {
this.positionConsole("above");
}).bind(this);
this._positionConsoleBelow = (function HUD_positionBelow() {
this.positionConsole("below");
}).bind(this);
this._positionConsoleWindow = (function HUD_positionWindow() {
this.positionConsole("window");
}).bind(this);
let button = this.makeXULNode("toolbarbutton");
button.setAttribute("type", "menu");
button.setAttribute("label", l10n.getStr("webConsolePosition"));
button.setAttribute("tooltip", l10n.getStr("webConsolePositionTooltip"));
let menuPopup = this.makeXULNode("menupopup");
button.appendChild(menuPopup);
let itemAbove = this.makeXULNode("menuitem");
itemAbove.setAttribute("label", l10n.getStr("webConsolePositionAbove"));
itemAbove.setAttribute("type", "checkbox");
itemAbove.setAttribute("autocheck", "false");
itemAbove.addEventListener("command", this._positionConsoleAbove, false);
menuPopup.appendChild(itemAbove);
let itemBelow = this.makeXULNode("menuitem");
itemBelow.setAttribute("label", l10n.getStr("webConsolePositionBelow"));
itemBelow.setAttribute("type", "checkbox");
itemBelow.setAttribute("autocheck", "false");
itemBelow.addEventListener("command", this._positionConsoleBelow, false);
menuPopup.appendChild(itemBelow);
let itemWindow = this.makeXULNode("menuitem");
itemWindow.setAttribute("label", l10n.getStr("webConsolePositionWindow"));
itemWindow.setAttribute("type", "checkbox");
itemWindow.setAttribute("autocheck", "false");
itemWindow.addEventListener("command", this._positionConsoleWindow, false);
menuPopup.appendChild(itemWindow);
this.positionMenuitems = {
last: null,
above: itemAbove,
below: itemBelow,
window: itemWindow,
};
return button;
},
/**
* Creates the context menu on the console.
*
* @param nsIDOMNode aOutputNode
* The console output DOM node.
* @returns void
*/
createConsoleMenu: function HUD_createConsoleMenu(aConsoleWrapper) {
let menuPopup = this.makeXULNode("menupopup");
let id = this.hudId + "-output-contextmenu";
menuPopup.setAttribute("id", id);
menuPopup.addEventListener("popupshowing", function() {
saveBodiesItem.setAttribute("checked",
HUDService.saveRequestAndResponseBodies);
}, true);
let saveBodiesItem = this.makeXULNode("menuitem");
saveBodiesItem.setAttribute("label", l10n.getStr("saveBodies.label"));
saveBodiesItem.setAttribute("accesskey",
l10n.getStr("saveBodies.accesskey"));
saveBodiesItem.setAttribute("type", "checkbox");
saveBodiesItem.setAttribute("buttonType", "saveBodies");
saveBodiesItem.setAttribute("oncommand", "HUDConsoleUI.command(this);");
menuPopup.appendChild(saveBodiesItem);
menuPopup.appendChild(this.makeXULNode("menuseparator"));
let copyItem = this.makeXULNode("menuitem");
copyItem.setAttribute("label", l10n.getStr("copyCmd.label"));
copyItem.setAttribute("accesskey", l10n.getStr("copyCmd.accesskey"));
copyItem.setAttribute("key", "key_copy");
copyItem.setAttribute("command", "cmd_copy");
copyItem.setAttribute("buttonType", "copy");
menuPopup.appendChild(copyItem);
let selectAllItem = this.makeXULNode("menuitem");
selectAllItem.setAttribute("label", l10n.getStr("selectAllCmd.label"));
selectAllItem.setAttribute("accesskey",
l10n.getStr("selectAllCmd.accesskey"));
selectAllItem.setAttribute("hudId", this.hudId);
selectAllItem.setAttribute("buttonType", "selectAll");
selectAllItem.setAttribute("oncommand", "HUDConsoleUI.command(this);");
menuPopup.appendChild(selectAllItem);
aConsoleWrapper.appendChild(menuPopup);
aConsoleWrapper.setAttribute("context", id);
},
/**
* Creates one of the filter buttons on the toolbar.
*
* @param nsIDOMNode aParent
* The node to which the filter button should be appended.
* @param object aDescriptor
* A descriptor that contains info about the button. Contains "name",
* "category", and "prefKey" properties, and optionally a "severities"
* property.
* @return void
*/
makeFilterButton: function HUD_makeFilterButton(aParent, aDescriptor)
{
let toolbarButton = this.makeXULNode("toolbarbutton");
aParent.appendChild(toolbarButton);
let toggleFilter = HeadsUpDisplayUICommands.toggleFilter;
toolbarButton.addEventListener("click", toggleFilter, false);
let name = aDescriptor.name;
toolbarButton.setAttribute("type", "menu-button");
toolbarButton.setAttribute("label", l10n.getStr("btn" + name));
toolbarButton.setAttribute("tooltip", l10n.getStr("tip" + name));
toolbarButton.setAttribute("category", aDescriptor.category);
toolbarButton.setAttribute("hudId", this.hudId);
toolbarButton.classList.add("webconsole-filter-button");
let menuPopup = this.makeXULNode("menupopup");
toolbarButton.appendChild(menuPopup);
let someChecked = false;
for (let i = 0; i < aDescriptor.severities.length; i++) {
let severity = aDescriptor.severities[i];
let menuItem = this.makeXULNode("menuitem");
menuItem.setAttribute("label", l10n.getStr("btn" + severity.name));
menuItem.setAttribute("type", "checkbox");
menuItem.setAttribute("autocheck", "false");
menuItem.setAttribute("hudId", this.hudId);
let prefKey = severity.prefKey;
menuItem.setAttribute("prefKey", prefKey);
let checked = this.filterPrefs[prefKey];
menuItem.setAttribute("checked", checked);
if (checked) {
someChecked = true;
}
menuItem.addEventListener("command", toggleFilter, false);
menuPopup.appendChild(menuItem);
}
toolbarButton.setAttribute("checked", someChecked);
},
/**
* Creates the close button on the toolbar.
*
* @param nsIDOMNode aParent
* The toolbar to attach the close button to.
* @return void
*/
makeCloseButton: function HUD_makeCloseButton(aToolbar)
{
this.closeButtonOnCommand = (function HUD_closeButton_onCommand() {
HUDService.animate(this.hudId, ANIMATE_OUT, (function() {
HUDService.deactivateHUDForContext(this.tab, true);
}).bind(this));
}).bind(this);
this.closeButton = this.makeXULNode("toolbarbutton");
this.closeButton.classList.add("webconsole-close-button");
this.closeButton.addEventListener("command",
this.closeButtonOnCommand, false);
aToolbar.appendChild(this.closeButton);
},
/**
* Creates the "Clear Console" button.
*
* @param nsIDOMNode aParent
* The toolbar to attach the "Clear Console" button to.
* @param string aHUDId
* The ID of the console.
* @return void
*/
makeClearConsoleButton: function HUD_makeClearConsoleButton(aToolbar)
{
let hudId = this.hudId;
function HUD_clearButton_onCommand() {
let hud = HUDService.getHudReferenceById(hudId);
if (hud.jsterm) {
hud.jsterm.clearOutput(true);
}
if (hud.gcliterm) {
hud.gcliterm.clearOutput();
}
}
let clearButton = this.makeXULNode("toolbarbutton");
clearButton.setAttribute("label", l10n.getStr("btnClear"));
clearButton.classList.add("webconsole-clear-console-button");
clearButton.addEventListener("command", HUD_clearButton_onCommand, false);
aToolbar.appendChild(clearButton);
},
/**
* Destroy the property inspector message node. This performs the necessary
* cleanup for the tree widget and removes it from the DOM.
*
* @param nsIDOMNode aMessageNode
* The message node that contains the property inspector from a
* console.dir call.
*/
pruneConsoleDirNode: function HUD_pruneConsoleDirNode(aMessageNode)
{
aMessageNode.parentNode.removeChild(aMessageNode);
let tree = aMessageNode.querySelector("tree");
tree.parentNode.removeChild(tree);
aMessageNode.propertyTreeView = null;
if (tree.view) {
tree.view.data = null;
}
tree.view = null;
},
/**
* Create the Web Console UI.
*
* @return nsIDOMNode
* The Web Console container element (HUDBox).
*/
createHUD: function HUD_createHUD()
{
if (!this.HUDBox) {
this.makeHUDNodes();
let positionPref = Services.prefs.getCharPref("devtools.webconsole.position");
this.positionConsole(positionPref);
}
return this.HUDBox;
},
uiInOwnWindow: false,
get console() { return this.contentWindow.wrappedJSObject.console; },
/**
* Logs a message to the Web Console that originates from the remote Web
* Console instance.
*
* @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.
*/
logConsoleAPIMessage: function HUD_logConsoleAPIMessage(aMessage)
{
let body = null;
let clipboardText = null;
let sourceURL = null;
let sourceLine = 0;
let level = aMessage.apiMessage.level;
let args = aMessage.apiMessage.arguments;
let argsToString = aMessage.argumentsToString;
switch (level) {
case "log":
case "info":
case "warn":
case "error":
case "debug":
body = argsToString.join(" ");
sourceURL = aMessage.apiMessage.filename;
sourceLine = aMessage.apiMessage.lineNumber;
break;
case "trace":
let filename = WebConsoleUtils.abbreviateSourceURL(args[0].filename);
let functionName = args[0].functionName ||
l10n.getStr("stacktrace.anonymousFunction");
let lineNumber = args[0].lineNumber;
body = l10n.getFormatStr("stacktrace.outputMessage",
[filename, functionName, lineNumber]);
sourceURL = args[0].filename;
sourceLine = args[0].lineNumber;
clipboardText = "";
args.forEach(function(aFrame) {
clipboardText += aFrame.filename + " :: " +
aFrame.functionName + " :: " +
aFrame.lineNumber + "\n";
});
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;
this.groupDepth++;
break;
case "groupEnd":
if (this.groupDepth > 0) {
this.groupDepth--;
}
return;
case "time":
if (!args) {
return;
}
if (args.error) {
Cu.reportError(l10n.getStr(args.error));
return;
}
body = l10n.getFormatStr("timerStarted", [args.name]);
clipboardText = body;
sourceURL = aMessage.apiMessage.filename;
sourceLine = aMessage.apiMessage.lineNumber;
break;
case "timeEnd":
if (!args) {
return;
}
body = l10n.getFormatStr("timeEnd", [args.name, args.duration]);
clipboardText = body;
sourceURL = aMessage.apiMessage.filename;
sourceLine = aMessage.apiMessage.lineNumber;
break;
default:
Cu.reportError("Unknown Console API log level: " + level);
return;
}
let node = ConsoleUtils.createMessageNode(this.chromeDocument,
CATEGORY_WEBDEV,
LEVELS[level],
body,
this.hudId,
sourceURL,
sourceLine,
clipboardText,
level,
aMessage.timeStamp);
// Make the node bring up the property panel, to allow the user to inspect
// the stack trace.
if (level == "trace") {
node._stacktrace = args;
this.makeOutputMessageLink(node, function _traceNodeClickCallback() {
if (node._panelOpen) {
return;
}
let options = {
anchor: node,
data: { object: node._stacktrace },
};
let propPanel = this.jsterm.openPropertyPanelAsync(options);
propPanel.panel.setAttribute("hudId", this.hudId);
}.bind(this));
}
ConsoleUtils.outputMessageNode(node, this.hudId);
if (level == "dir") {
// 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.
let tree = node.querySelector("tree");
tree.view = node.propertyTreeView;
// Make sure the cached evaluated object will be purged when the node is
// removed.
node._evalCacheId = aMessage.objectsCacheId;
}
},
/**
* Reports an error in the page source, either JavaScript or CSS.
*
* @param nsIScriptError aScriptError
* The error message to report.
*/
reportPageError: function HUD_reportPageError(aScriptError)
{
if (!aScriptError.outerWindowID) {
return;
}
let category;
switch (aScriptError.category) {
// We ignore chrome-originating errors as we only care about content.
case "XPConnect JavaScript":
case "component javascript":
case "chrome javascript":
case "chrome registration":
case "XBL":
case "XBL Prototype Handler":
case "XBL Content Sink":
case "xbl javascript":
return;
case "CSS Parser":
case "CSS Loader":
category = CATEGORY_CSS;
break;
default:
category = CATEGORY_JS;
break;
}
// Warnings and legacy strict errors become warnings; other types become
// errors.
let severity = SEVERITY_ERROR;
if ((aScriptError.flags & aScriptError.warningFlag) ||
(aScriptError.flags & aScriptError.strictFlag)) {
severity = SEVERITY_WARNING;
}
let node = ConsoleUtils.createMessageNode(this.chromeDocument,
category,
severity,
aScriptError.errorMessage,
this.hudId,
aScriptError.sourceName,
aScriptError.lineNumber,
null,
null,
aScriptError.timeStamp);
ConsoleUtils.outputMessageNode(node, this.hudId);
},
ERRORS: {
HUD_BOX_DOES_NOT_EXIST: "Heads Up Display does not exist",
TAB_ID_REQUIRED: "Tab DOM ID is required",
PARENTNODE_NOT_FOUND: "parentNode element not found"
},
/**
* Setup the message manager used to communicate with the Web Console content
* script. This method loads the content script, adds the message listeners
* and initializes the connection to the content script.
*
* @private
*/
_setupMessageManager: function HUD__setupMessageManager()
{
this.messageManager.loadFrameScript(CONTENT_SCRIPT_URL, true);
this._messageListeners.forEach(function(aName) {
this.messageManager.addMessageListener(aName, this);
}, this);
let message = {
hudId: this.hudId,
features: ["ConsoleAPI", "JSTerm", "PageError"],
cachedMessages: ["ConsoleAPI", "PageError"],
};
this.sendMessageToContent("WebConsole:Init", message);
},
/**
* Handler for all of the messages coming from the Web Console content script.
*
* @private
* @param object aMessage
* A MessageManager object that holds the remote message.
*/
receiveMessage: function HUD_receiveMessage(aMessage)
{
if (!aMessage.json || aMessage.json.hudId != this.hudId) {
Cu.reportError("JSTerm: received message " + aMessage.name +
" from wrong hudId.");
return;
}
switch (aMessage.name) {
case "JSTerm:EvalObject":
this._receiveMessageWithCallback(aMessage.json);
break;
case "WebConsole:ConsoleAPI":
this.logConsoleAPIMessage(aMessage.json);
break;
case "WebConsole:PageError":
this.reportPageError(aMessage.json.pageError);
break;
case "WebConsole:CachedMessages":
this._displayCachedConsoleMessages(aMessage.json.messages);
this._onInitComplete();
break;
}
},
/**
* Callback method for when the Web Console initialization is complete. For
* now this method sends the web-console-created notification using the
* nsIObserverService.
*
* @private
*/
_onInitComplete: function HUD__onInitComplete()
{
let id = WebConsoleUtils.supportsString(this.hudId);
Services.obs.notifyObservers(id, "web-console-created", null);
},
/**
* Handler for messages that have an associated callback function. The
* this.sendMessageToContent() allows one to provide a function to be invoked
* when the content script replies to the message previously sent. This is the
* method that invokes the callback.
*
* @see this.sendMessageToContent
* @private
* @param object aResponse
* Message object received from the content script.
*/
_receiveMessageWithCallback:
function HUD__receiveMessageWithCallback(aResponse)
{
if (aResponse.id in this.asyncRequests) {
let request = this.asyncRequests[aResponse.id];
request.callback(aResponse, request.message);
delete this.asyncRequests[aResponse.id];
}
else {
Cu.reportError("receiveMessageWithCallback response for stale request " +
"ID " + aResponse.id);
}
},
/**
* Send a message to the content script.
*
* @param string aName
* The name of the message you want to send.
*
* @param object aMessage
* The message object you want to send. This object needs to have no
* cyclic references and it needs to be JSON-stringifiable.
*
* @param function [aCallback]
* Optional function you want to have called when the content script
* replies to your message. Your callback receives two arguments:
* (1) the response object from the content script and (2) the message
* you sent to the content script (which is aMessage here).
*/
sendMessageToContent:
function HUD_sendMessageToContent(aName, aMessage, aCallback)
{
aMessage.hudId = this.hudId;
if (!("id" in aMessage)) {
aMessage.id = "HUDChrome-" + HUDService.sequenceId();
}
if (aCallback) {
this.asyncRequests[aMessage.id] = {
name: aName,
message: aMessage,
callback: aCallback,
};
}
this.messageManager.sendAsyncMessage(aName, aMessage);
},
/**
* Make a link given an output element.
*
* @param nsIDOMNode aNode
* The message element you want to make a link for.
* @param function aCallback
* The function you want invoked when the user clicks on the message
* element.
*/
makeOutputMessageLink: function HUD_makeOutputMessageLink(aNode, aCallback)
{
let linkNode;
if (aNode.category === CATEGORY_NETWORK) {
linkNode = aNode.querySelector(".webconsole-msg-link");
}
else {
linkNode = aNode.querySelector(".webconsole-msg-body");
linkNode.classList.add("hud-clickable");
}
linkNode.setAttribute("aria-haspopup", "true");
aNode.addEventListener("mousedown", function(aEvent) {
this._startX = aEvent.clientX;
this._startY = aEvent.clientY;
}, false);
aNode.addEventListener("click", function(aEvent) {
if (aEvent.detail != 1 || aEvent.button != 0 ||
(this._startX != aEvent.clientX &&
this._startY != aEvent.clientY)) {
return;
}
aCallback(this, aEvent);
}, false);
},
/**
* Destroy the HUD object. Call this method to avoid memory leaks when the Web
* Console is closed.
*/
destroy: function HUD_destroy()
{
this.sendMessageToContent("WebConsole:Destroy", {hudId: this.hudId});
this._messageListeners.forEach(function(aName) {
this.messageManager.removeMessageListener(aName, this);
}, this);
if (this.jsterm) {
this.jsterm.destroy();
}
if (this.gcliterm) {
this.gcliterm.destroy();
}
delete this.asyncRequests;
delete this.messageManager;
delete this.browser;
this.positionMenuitems.above.removeEventListener("command",
this._positionConsoleAbove, false);
this.positionMenuitems.below.removeEventListener("command",
this._positionConsoleBelow, false);
this.positionMenuitems.window.removeEventListener("command",
this._positionConsoleWindow, false);
this.closeButton.removeEventListener("command",
this.closeButtonOnCommand, false);
},
};
/**
* Creates a DOM Node factory for XUL nodes - as well as textNodes
* @param aFactoryType "xul" or "text"
* @param ignored This parameter is currently ignored, and will be removed
* See bug 594304
* @param aDocument The document, the factory is to generate nodes from
* @return DOM Node Factory function
*/
function NodeFactory(aFactoryType, ignored, aDocument)
{
// aDocument is presumed to be a XULDocument
if (aFactoryType == "text") {
return function factory(aText)
{
return aDocument.createTextNode(aText);
}
}
else if (aFactoryType == "xul") {
return function factory(aTag)
{
return aDocument.createElement(aTag);
}
}
else {
throw new Error('NodeFactory: Unknown factory type: ' + aFactoryType);
}
}
//////////////////////////////////////////////////////////////////////////
// JS Completer
//////////////////////////////////////////////////////////////////////////
const STATE_NORMAL = 0;
const STATE_QUOTE = 2;
const STATE_DQUOTE = 3;
const OPEN_BODY = '{[('.split('');
const CLOSE_BODY = '}])'.split('');
const OPEN_CLOSE_BODY = {
'{': '}',
'[': ']',
'(': ')'
};
/**
* Analyses a given string to find the last statement that is interesting for
* later completion.
*
* @param string aStr
* A string to analyse.
*
* @returns object
* If there was an error in the string detected, then a object like
*
* { err: "ErrorMesssage" }
*
* is returned, otherwise a object like
*
* {
* state: STATE_NORMAL|STATE_QUOTE|STATE_DQUOTE,
* startPos: index of where the last statement begins
* }
*/
function findCompletionBeginning(aStr)
{
let bodyStack = [];
let state = STATE_NORMAL;
let start = 0;
let c;
for (let i = 0; i < aStr.length; i++) {
c = aStr[i];
switch (state) {
// Normal JS state.
case STATE_NORMAL:
if (c == '"') {
state = STATE_DQUOTE;
}
else if (c == '\'') {
state = STATE_QUOTE;
}
else if (c == ';') {
start = i + 1;
}
else if (c == ' ') {
start = i + 1;
}
else if (OPEN_BODY.indexOf(c) != -1) {
bodyStack.push({
token: c,
start: start
});
start = i + 1;
}
else if (CLOSE_BODY.indexOf(c) != -1) {
var last = bodyStack.pop();
if (!last || OPEN_CLOSE_BODY[last.token] != c) {
return {
err: "syntax error"
};
}
if (c == '}') {
start = i + 1;
}
else {
start = last.start;
}
}
break;
// Double quote state > " <
case STATE_DQUOTE:
if (c == '\\') {
i ++;
}
else if (c == '\n') {
return {
err: "unterminated string literal"
};
}
else if (c == '"') {
state = STATE_NORMAL;
}
break;
// Single quoate state > ' <
case STATE_QUOTE:
if (c == '\\') {
i ++;
}
else if (c == '\n') {
return {
err: "unterminated string literal"
};
return;
}
else if (c == '\'') {
state = STATE_NORMAL;
}
break;
}
}
return {
state: state,
startPos: start
};
}
/**
* Provides a list of properties, that are possible matches based on the passed
* scope and inputValue.
*
* @param object aScope
* Scope to use for the completion.
*
* @param string aInputValue
* Value that should be completed.
*
* @returns null or object
* If no completion valued could be computed, null is returned,
* otherwise a object with the following form is returned:
* {
* matches: [ string, string, string ],
* matchProp: Last part of the inputValue that was used to find
* the matches-strings.
* }
*/
function JSPropertyProvider(aScope, aInputValue)
{
let obj = WebConsoleUtils.unwrap(aScope);
// Analyse the aInputValue and find the beginning of the last part that
// should be completed.
let beginning = findCompletionBeginning(aInputValue);
// There was an error analysing the string.
if (beginning.err) {
return null;
}
// If the current state is not STATE_NORMAL, then we are inside of an string
// which means that no completion is possible.
if (beginning.state != STATE_NORMAL) {
return null;
}
let completionPart = aInputValue.substring(beginning.startPos);
// Don't complete on just an empty string.
if (completionPart.trim() == "") {
return null;
}
let properties = completionPart.split('.');
let matchProp;
if (properties.length > 1) {
matchProp = properties.pop().trimLeft();
for (let i = 0; i < properties.length; i++) {
let prop = properties[i].trim();
// If obj is undefined or null, then there is no chance to run completion
// on it. Exit here.
if (typeof obj === "undefined" || obj === null) {
return null;
}
// Check if prop is a getter function on obj. Functions can change other
// stuff so we can't execute them to get the next object. Stop here.
if (WebConsoleUtils.isNonNativeGetter(obj, prop)) {
return null;
}
try {
obj = obj[prop];
}
catch (ex) {
return null;
}
}
}
else {
matchProp = properties[0].trimLeft();
}
// If obj is undefined or null, then there is no chance to run
// completion on it. Exit here.
if (typeof obj === "undefined" || obj === null) {
return null;
}
// Skip Iterators and Generators.
if (WebConsoleUtils.isIteratorOrGenerator(obj)) {
return null;
}
let matches = [];
for (let prop in obj) {
if (prop.indexOf(matchProp) == 0) {
matches.push(prop);
}
}
return {
matchProp: matchProp,
matches: matches.sort(),
};
}
//////////////////////////////////////////////////////////////////////////
// JSTerm
//////////////////////////////////////////////////////////////////////////
/**
* JSTermHelper
*
* Defines a set of functions ("helper functions") that are available from the
* WebConsole but not from the webpage.
* A list of helper functions used by Firebug can be found here:
* http://getfirebug.com/wiki/index.php/Command_Line_API
*/
function JSTermHelper(aJSTerm)
{
/**
* Returns the result of document.getElementById(aId).
*
* @param string aId
* A string that is passed to window.document.getElementById.
* @returns nsIDOMNode or null
*/
aJSTerm.sandbox.$ = function JSTH_$(aId)
{
try {
return aJSTerm._window.document.getElementById(aId);
}
catch (ex) {
aJSTerm.console.error(ex.message);
}
};
/**
* Returns the result of document.querySelectorAll(aSelector).
*
* @param string aSelector
* A string that is passed to window.document.querySelectorAll.
* @returns array of nsIDOMNode
*/
aJSTerm.sandbox.$$ = function JSTH_$$(aSelector)
{
try {
return aJSTerm._window.document.querySelectorAll(aSelector);
}
catch (ex) {
aJSTerm.console.error(ex.message);
}
};
/**
* Runs a 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.
* @returns array of nsIDOMNode
*/
aJSTerm.sandbox.$x = function JSTH_$x(aXPath, aContext)
{
let nodes = [];
let doc = aJSTerm._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) {
aJSTerm.console.error(ex.message);
}
return nodes;
};
/**
* Returns the currently selected object in the highlighter.
*
* @returns nsIDOMNode or null
*/
Object.defineProperty(aJSTerm.sandbox, "$0", {
get: function() {
let mw = HUDService.currentContext();
try {
return mw.InspectorUI.selection;
}
catch (ex) {
aJSTerm.console.error(ex.message);
}
},
enumerable: true,
configurable: false
});
/**
* Clears the output of the JSTerm.
*/
aJSTerm.sandbox.clear = function JSTH_clear()
{
aJSTerm.helperEvaluated = true;
aJSTerm.clearOutput(true);
};
/**
* Returns the result of Object.keys(aObject).
*
* @param object aObject
* Object to return the property names from.
* @returns array of string
*/
aJSTerm.sandbox.keys = function JSTH_keys(aObject)
{
try {
return Object.keys(WebConsoleUtils.unwrap(aObject));
}
catch (ex) {
aJSTerm.console.error(ex.message);
}
};
/**
* Returns the values of all properties on aObject.
*
* @param object aObject
* Object to display the values from.
* @returns array of string
*/
aJSTerm.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) {
aJSTerm.console.error(ex.message);
}
return arrValues;
};
/**
* Opens a help window in MDC
*/
aJSTerm.sandbox.help = function JSTH_help()
{
aJSTerm.helperEvaluated = true;
aJSTerm._window.open(
"https://developer.mozilla.org/AppLinks/WebConsoleHelp?locale=" +
aJSTerm._window.navigator.language, "help", "");
};
/**
* Inspects the passed aObject. This is done by opening the PropertyPanel.
*
* @param object aObject
* Object to inspect.
* @returns void
*/
aJSTerm.sandbox.inspect = function JSTH_inspect(aObject)
{
aJSTerm.helperEvaluated = true;
let propPanel = aJSTerm.openPropertyPanel(null,
WebConsoleUtils.unwrap(aObject));
propPanel.panel.setAttribute("hudId", aJSTerm.hudId);
};
aJSTerm.sandbox.inspectrules = function JSTH_inspectrules(aNode)
{
aJSTerm.helperEvaluated = true;
let doc = aJSTerm.inputNode.ownerDocument;
let win = doc.defaultView;
let panel = createElement(doc, "panel", {
label: "CSS Rules",
titlebar: "normal",
noautofocus: "true",
noautohide: "true",
close: "true",
width: 350,
height: (win.screen.height / 2)
});
let iframe = createAndAppendElement(panel, "iframe", {
src: "chrome://browser/content/devtools/cssruleview.xul",
flex: "1",
});
panel.addEventListener("load", function onLoad() {
panel.removeEventListener("load", onLoad, true);
let doc = iframe.contentDocument;
let view = new CssRuleView(doc);
doc.documentElement.appendChild(view.element);
view.highlight(aNode);
}, true);
let parent = doc.getElementById("mainPopupSet");
parent.appendChild(panel);
panel.addEventListener("popuphidden", function onHide() {
panel.removeEventListener("popuphidden", onHide);
parent.removeChild(panel);
});
let footer = createElement(doc, "hbox", { align: "end" });
createAndAppendElement(footer, "spacer", { flex: 1});
createAndAppendElement(footer, "resizer", { dir: "bottomend" });
panel.appendChild(footer);
let anchor = win.gBrowser.selectedBrowser;
panel.openPopup(anchor, "end_before", 0, 0, false, false);
}
/**
* Prints aObject to the output.
*
* @param object aObject
* Object to print to the output.
* @returns void
*/
aJSTerm.sandbox.pprint = function JSTH_pprint(aObject)
{
aJSTerm.helperEvaluated = true;
if (aObject === null || aObject === undefined || aObject === true || aObject === false) {
aJSTerm.console.error(l10n.getStr("helperFuncUnsupportedTypeError"));
return;
}
else if (typeof aObject === TYPEOF_FUNCTION) {
aJSTerm.writeOutput(aObject + "\n", CATEGORY_OUTPUT, SEVERITY_LOG);
return;
}
let output = [];
let pairs = namesAndValuesOf(WebConsoleUtils.unwrap(aObject));
pairs.forEach(function(pair) {
output.push(" " + pair.display);
});
aJSTerm.writeOutput(output.join("\n"), CATEGORY_OUTPUT, SEVERITY_LOG);
};
/**
* Print a string to the output, as-is.
*
* @param string aString
* A string you want to output.
* @returns void
*/
aJSTerm.sandbox.print = function JSTH_print(aString)
{
aJSTerm.helperEvaluated = true;
aJSTerm.writeOutput("" + aString, CATEGORY_OUTPUT, SEVERITY_LOG);
};
}
/**
* JSTerm
*
* JavaScript Terminal: creates input nodes for console code interpretation
* and 'JS Workspaces'
*/
/**
* Create a JSTerminal or attach a JSTerm input node to an existing output node,
* given by the parent node.
*
* @param object aContext
* Usually nsIDOMWindow, but doesn't have to be
* @param nsIDOMNode aParentNode where to attach the JSTerm
* @param object aMixin
* Gecko-app (or Jetpack) specific utility object
* @param object aConsole
* Console object to use within the JSTerm.
*/
function JSTerm(aContext, aParentNode, aMixin, aConsole)
{
// set the context, attach the UI by appending to aParentNode
this.application = appName();
this.context = aContext;
this.parentNode = aParentNode;
this.mixins = aMixin;
this.console = aConsole;
this.document = aParentNode.ownerDocument
this.setTimeout = aParentNode.ownerDocument.defaultView.setTimeout;
let node = aParentNode;
while (!node.hasAttribute("id")) {
node = node.parentNode;
}
this.hudId = node.getAttribute("id");
this.history = [];
this.historyIndex = 0;
this.historyPlaceHolder = 0; // this.history.length;
this.log = LogFactory("*** JSTerm:");
this.autocompletePopup = new AutocompletePopup(aParentNode.ownerDocument);
this.autocompletePopup.onSelect = this.onAutocompleteSelect.bind(this);
this.autocompletePopup.onClick = this.acceptProposedCompletion.bind(this);
this.init();
}
JSTerm.prototype = {
lastInputValue: "",
propertyProvider: JSPropertyProvider,
COMPLETE_FORWARD: 0,
COMPLETE_BACKWARD: 1,
COMPLETE_HINT_ONLY: 2,
init: function JST_init()
{
this.createSandbox();
this.inputNode = this.mixins.inputNode;
this.outputNode = this.mixins.outputNode;
this.completeNode = this.mixins.completeNode;
this._keyPress = this.keyPress.bind(this);
this._inputEventHandler = this.inputEventHandler.bind(this);
this.inputNode.addEventListener("keypress",
this._keyPress, false);
this.inputNode.addEventListener("input",
this._inputEventHandler, false);
this.inputNode.addEventListener("keyup",
this._inputEventHandler, false);
},
get codeInputString()
{
return this.inputNode.value;
},
generateUI: function JST_generateUI()
{
this.mixins.generateUI();
},
attachUI: function JST_attachUI()
{
this.mixins.attachUI();
},
createSandbox: function JST_setupSandbox()
{
// create a JS Sandbox out of this.context
this.sandbox = new Cu.Sandbox(this._window,
{ sandboxPrototype: this._window, wantXrays: false });
this.sandbox.console = this.console;
JSTermHelper(this);
},
get _window()
{
return this.context.get().QueryInterface(Ci.nsIDOMWindow);
},
/**
* Evaluates a string in the sandbox.
*
* @param string aString
* String to evaluate in the sandbox.
* @returns something
* The result of the evaluation.
*/
evalInSandbox: function JST_evalInSandbox(aString)
{
// 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;
},
execute: function JST_execute(aExecuteString)
{
// attempt to execute the content of the inputNode
aExecuteString = aExecuteString || this.inputNode.value;
if (!aExecuteString) {
this.writeOutput("no value to execute", CATEGORY_OUTPUT, SEVERITY_LOG);
return;
}
this.writeOutput(aExecuteString, CATEGORY_INPUT, SEVERITY_LOG);
try {
this.helperEvaluated = false;
let result = this.evalInSandbox(aExecuteString);
// Hide undefined results coming from helpers.
let shouldShow = !(result === undefined && this.helperEvaluated);
if (shouldShow) {
let inspectable = WebConsoleUtils.isObjectInspectable(result);
let resultString = WebConsoleUtils.formatResult(result);
if (inspectable) {
this.writeOutputJS(aExecuteString, result, resultString);
}
else {
this.writeOutput(resultString, CATEGORY_OUTPUT, SEVERITY_LOG);
}
}
}
catch (ex) {
this.writeOutput("" + ex, CATEGORY_OUTPUT, SEVERITY_ERROR);
}
this.history.push(aExecuteString);
this.historyIndex++;
this.historyPlaceHolder = this.history.length;
this.setInputValue("");
this.clearCompletion();
},
/**
* Opens a new PropertyPanel. The panel has two buttons: "Update" reexecutes
* the passed aEvalString and places the result inside of the tree. The other
* button closes the panel.
*
* @param string aEvalString
* String that was used to eval the aOutputObject. Used as title
* and to update the tree content.
* @param object aOutputObject
* Object to display/inspect inside of the tree.
* @param nsIDOMNode aAnchor
* A node to popup the panel next to (using "after_pointer").
* @returns object the created and opened propertyPanel.
*/
openPropertyPanel: function JST_openPropertyPanel(aEvalString, aOutputObject,
aAnchor)
{
let self = this;
let propPanel;
// The property panel has one button:
// `Update`: reexecutes the string executed on the command line. The
// result will be inspected by this panel.
let buttons = [];
// If there is a evalString passed to this function, then add a `Update`
// button to the panel so that the evalString can be reexecuted to update
// the content of the panel.
if (aEvalString !== null) {
buttons.push({
label: l10n.getStr("update.button"),
accesskey: l10n.getStr("update.accesskey"),
oncommand: function () {
try {
var result = self.evalInSandbox(aEvalString);
if (result !== undefined) {
// TODO: This updates the value of the tree.
// However, the states of opened nodes is not saved.
// See bug 586246.
propPanel.treeView.data = result;
}
}
catch (ex) {
self.console.error(ex);
}
}
});
}
let doc = self.document;
let parent = doc.getElementById("mainPopupSet");
let title = (aEvalString
? l10n.getFormatStr("jsPropertyInspectTitle", [aEvalString])
: l10n.getStr("jsPropertyTitle"));
propPanel = new PropertyPanel(parent, doc, title, aOutputObject, buttons);
propPanel.linkNode = aAnchor;
let panel = propPanel.panel;
panel.openPopup(aAnchor, "after_pointer", 0, 0, false, false);
panel.sizeTo(350, 450);
return propPanel;
},
/**
* Opens a new property panel that allows the inspection of the given object.
* The object information can be retrieved both async and sync, depending on
* the given options.
*
* @param object aOptions
* Property panel options:
* - title:
* Panel title.
* - anchor:
* The DOM element you want the panel to be anchored to.
* - updateButtonCallback:
* An optional function you want invoked when the user clicks the
* Update button. If this function is not provided the Update button is
* not shown.
* - data:
* An object that represents the object you want to inspect. Please see
* the PropertyPanelAsync documentation - this object is passed to the
* PropertyPanelAsync constructor
* @param object aResponse
* The response object to display/inspect inside of the tree.
* @param nsIDOMNode aAnchor
* A node to popup the panel next to (using "after_pointer").
* @return object
* The new instance of PropertyPanelAsync.
*/
openPropertyPanelAsync: function JST_openPropertyPanelAsync(aOptions)
{
// The property panel has one button:
// `Update`: reexecutes the string executed on the command line. The
// result will be inspected by this panel.
let buttons = [];
if (aOptions.updateButtonCallback) {
buttons.push({
label: l10n.getStr("update.button"),
accesskey: l10n.getStr("update.accesskey"),
oncommand: aOptions.updateButtonCallback,
});
}
let parent = this.document.getElementById("mainPopupSet");
let title = aOptions.title ?
l10n.getFormatStr("jsPropertyInspectTitle", [aOptions.title]) :
l10n.getStr("jsPropertyTitle");
let propPanel = new PropertyPanelAsync(parent, title, aOptions.data, buttons);
propPanel.panel.openPopup(aOptions.anchor, "after_pointer", 0, 0, false, false);
propPanel.panel.sizeTo(350, 450);
if (aOptions.anchor) {
propPanel.panel.addEventListener("popuphiding", function onPopupHide() {
propPanel.panel.removeEventListener("popuphiding", onPopupHide, false);
aOptions.anchor._panelOpen = false;
}, false);
aOptions.anchor._panelOpen = true;
}
return propPanel;
},
/**
* Writes a JS object to the JSTerm outputNode. If the user clicks on the
* written object, openPropertyPanel is called to open up a panel to inspect
* the object.
*
* @param string aEvalString
* String that was evaluated to get the aOutputObject.
* @param object aResultObject
* The evaluation result object.
* @param object aOutputString
* The output string to be written to the outputNode.
*/
writeOutputJS: function JST_writeOutputJS(aEvalString, aOutputObject, aOutputString)
{
let node = ConsoleUtils.createMessageNode(this.document,
CATEGORY_OUTPUT,
SEVERITY_LOG,
aOutputString,
this.hudId);
let linkNode = node.querySelector(".webconsole-msg-body");
linkNode.classList.add("hud-clickable");
linkNode.setAttribute("aria-haspopup", "true");
// Make the object bring up the property panel.
node.addEventListener("mousedown", function(aEvent) {
this._startX = aEvent.clientX;
this._startY = aEvent.clientY;
}, false);
let self = this;
node.addEventListener("click", function(aEvent) {
if (aEvent.detail != 1 || aEvent.button != 0 ||
(this._startX != aEvent.clientX &&
this._startY != aEvent.clientY)) {
return;
}
if (!this._panelOpen) {
let propPanel = self.openPropertyPanel(aEvalString, aOutputObject, this);
propPanel.panel.setAttribute("hudId", self.hudId);
this._panelOpen = true;
}
}, false);
ConsoleUtils.outputMessageNode(node, this.hudId);
},
/**
* Writes a message to the HUD that originates from the interactive
* JavaScript console.
*
* @param string aOutputMessage
* The message to display.
* @param number aCategory
* The category of message: one of the CATEGORY_ constants.
* @param number aSeverity
* The severity of message: one of the SEVERITY_ constants.
* @returns void
*/
writeOutput: function JST_writeOutput(aOutputMessage, aCategory, aSeverity)
{
let node = ConsoleUtils.createMessageNode(this.document,
aCategory, aSeverity,
aOutputMessage, this.hudId);
ConsoleUtils.outputMessageNode(node, this.hudId);
},
/**
* Clear the Web Console output.
*
* @param boolean aClearStorage
* True if you want to clear the console messages storage associated to
* this Web Console.
*/
clearOutput: function JST_clearOutput(aClearStorage)
{
let hud = HUDService.getHudReferenceById(this.hudId);
hud.cssNodes = {};
let outputNode = hud.outputNode;
let node;
while ((node = outputNode.firstChild)) {
if (node._evalCacheId && !node._panelOpen) {
this.clearObjectCache(node._evalCacheId);
}
if (node.classList &&
node.classList.contains("webconsole-msg-inspector")) {
hud.pruneConsoleDirNode(node);
}
else {
outputNode.removeChild(node);
}
}
hud.HUDBox.lastTimestamp = 0;
hud.groupDepth = 0;
if (aClearStorage) {
hud.sendMessageToContent("ConsoleAPI:ClearCache", {});
}
},
/**
* Updates the size of the input field (command line) to fit its contents.
*
* @returns void
*/
resizeInput: function JST_resizeInput()
{
let inputNode = this.inputNode;
// Reset the height so that scrollHeight will reflect the natural height of
// the contents of the input field.
inputNode.style.height = "auto";
// Now resize the input field to fit its contents.
let scrollHeight = inputNode.inputField.scrollHeight;
if (scrollHeight > 0) {
inputNode.style.height = scrollHeight + "px";
}
},
/**
* Sets the value of the input field (command line), and resizes the field to
* fit its contents. This method is preferred over setting "inputNode.value"
* directly, because it correctly resizes the field.
*
* @param string aNewValue
* The new value to set.
* @returns void
*/
setInputValue: function JST_setInputValue(aNewValue)
{
this.inputNode.value = aNewValue;
this.lastInputValue = aNewValue;
this.completeNode.value = "";
this.resizeInput();
},
/**
* The inputNode "input" and "keyup" event handler.
*
* @param nsIDOMEvent aEvent
*/
inputEventHandler: function JSTF_inputEventHandler(aEvent)
{
if (this.lastInputValue != this.inputNode.value) {
this.resizeInput();
this.complete(this.COMPLETE_HINT_ONLY);
this.lastInputValue = this.inputNode.value;
}
},
/**
* The inputNode "keypress" event handler.
*
* @param nsIDOMEvent aEvent
*/
keyPress: function JSTF_keyPress(aEvent)
{
if (aEvent.ctrlKey) {
switch (aEvent.charCode) {
case 97:
// control-a
this.inputNode.setSelectionRange(0, 0);
aEvent.preventDefault();
break;
case 101:
// control-e
this.inputNode.setSelectionRange(this.inputNode.value.length,
this.inputNode.value.length);
aEvent.preventDefault();
break;
default:
break;
}
return;
}
else if (aEvent.shiftKey &&
aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
// shift return
// TODO: expand the inputNode height by one line
return;
}
let inputUpdated = false;
switch(aEvent.keyCode) {
case Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE:
if (this.autocompletePopup.isOpen) {
this.clearCompletion();
aEvent.preventDefault();
}
break;
case Ci.nsIDOMKeyEvent.DOM_VK_RETURN:
if (this.autocompletePopup.isOpen && this.autocompletePopup.selectedIndex > -1) {
this.acceptProposedCompletion();
}
else {
this.execute();
}
aEvent.preventDefault();
break;
case Ci.nsIDOMKeyEvent.DOM_VK_UP:
if (this.autocompletePopup.isOpen) {
inputUpdated = this.complete(this.COMPLETE_BACKWARD);
}
else if (this.canCaretGoPrevious()) {
inputUpdated = this.historyPeruse(HISTORY_BACK);
}
if (inputUpdated) {
aEvent.preventDefault();
}
break;
case Ci.nsIDOMKeyEvent.DOM_VK_DOWN:
if (this.autocompletePopup.isOpen) {
inputUpdated = this.complete(this.COMPLETE_FORWARD);
}
else if (this.canCaretGoNext()) {
inputUpdated = this.historyPeruse(HISTORY_FORWARD);
}
if (inputUpdated) {
aEvent.preventDefault();
}
break;
case Ci.nsIDOMKeyEvent.DOM_VK_TAB:
// Generate a completion and accept the first proposed value.
if (this.complete(this.COMPLETE_HINT_ONLY) &&
this.lastCompletion &&
this.acceptProposedCompletion()) {
aEvent.preventDefault();
}
else {
this.updateCompleteNode(l10n.getStr("Autocomplete.blank"));
aEvent.preventDefault();
}
break;
default:
break;
}
},
/**
* Go up/down the history stack of input values.
*
* @param number aDirection
* History navigation direction: HISTORY_BACK or HISTORY_FORWARD.
*
* @returns boolean
* True if the input value changed, false otherwise.
*/
historyPeruse: function JST_historyPeruse(aDirection)
{
if (!this.history.length) {
return false;
}
// Up Arrow key
if (aDirection == HISTORY_BACK) {
if (this.historyPlaceHolder <= 0) {
return false;
}
let inputVal = this.history[--this.historyPlaceHolder];
if (inputVal){
this.setInputValue(inputVal);
}
}
// Down Arrow key
else if (aDirection == HISTORY_FORWARD) {
if (this.historyPlaceHolder == this.history.length - 1) {
this.historyPlaceHolder ++;
this.setInputValue("");
}
else if (this.historyPlaceHolder >= (this.history.length)) {
return false;
}
else {
let inputVal = this.history[++this.historyPlaceHolder];
if (inputVal){
this.setInputValue(inputVal);
}
}
}
else {
throw new Error("Invalid argument 0");
}
return true;
},
refocus: function JSTF_refocus()
{
this.inputNode.blur();
this.inputNode.focus();
},
/**
* Check if the caret is at a location that allows selecting the previous item
* in history when the user presses the Up arrow key.
*
* @return boolean
* True if the caret is at a location that allows selecting the
* previous item in history when the user presses the Up arrow key,
* otherwise false.
*/
canCaretGoPrevious: function JST_canCaretGoPrevious()
{
let node = this.inputNode;
if (node.selectionStart != node.selectionEnd) {
return false;
}
let multiline = /[\r\n]/.test(node.value);
return node.selectionStart == 0 ? true :
node.selectionStart == node.value.length && !multiline;
},
/**
* Check if the caret is at a location that allows selecting the next item in
* history when the user presses the Down arrow key.
*
* @return boolean
* True if the caret is at a location that allows selecting the next
* item in history when the user presses the Down arrow key, otherwise
* false.
*/
canCaretGoNext: function JST_canCaretGoNext()
{
let node = this.inputNode;
if (node.selectionStart != node.selectionEnd) {
return false;
}
let multiline = /[\r\n]/.test(node.value);
return node.selectionStart == node.value.length ? true :
node.selectionStart == 0 && !multiline;
},
history: null,
// Stores the data for the last completion.
lastCompletion: null,
/**
* Completes the current typed text in the inputNode. Completion is performed
* only if the selection/cursor is at the end of the string. If no completion
* is found, the current inputNode value and cursor/selection stay.
*
* @param int type possible values are
* - this.COMPLETE_FORWARD: If there is more than one possible completion
* and the input value stayed the same compared to the last time this
* function was called, then the next completion of all possible
* completions is used. If the value changed, then the first possible
* completion is used and the selection is set from the current
* cursor position to the end of the completed text.
* If there is only one possible completion, then this completion
* value is used and the cursor is put at the end of the completion.
* - this.COMPLETE_BACKWARD: Same as this.COMPLETE_FORWARD but if the
* value stayed the same as the last time the function was called,
* then the previous completion of all possible completions is used.
* - this.COMPLETE_HINT_ONLY: If there is more than one possible
* completion and the input value stayed the same compared to the
* last time this function was called, then the same completion is
* used again. If there is only one possible completion, then
* the inputNode.value is set to this value and the selection is set
* from the current cursor position to the end of the completed text.
*
* @returns boolean true if there existed a completion for the current input,
* or false otherwise.
*/
complete: function JSTF_complete(type)
{
let inputNode = this.inputNode;
let inputValue = inputNode.value;
// If the inputNode has no value, then don't try to complete on it.
if (!inputValue) {
this.clearCompletion();
return false;
}
// Only complete if the selection is empty and at the end of the input.
if (inputNode.selectionStart == inputNode.selectionEnd &&
inputNode.selectionEnd != inputValue.length) {
this.clearCompletion();
return false;
}
let popup = this.autocompletePopup;
if (!this.lastCompletion || this.lastCompletion.value != inputValue) {
let properties = this.propertyProvider(this.sandbox.window, inputValue);
if (!properties || !properties.matches.length) {
this.clearCompletion();
return false;
}
let items = properties.matches.map(function(aMatch) {
return {label: aMatch};
});
popup.setItems(items);
this.lastCompletion = {value: inputValue,
matchProp: properties.matchProp};
if (items.length > 1 && !popup.isOpen) {
popup.openPopup(this.inputNode);
}
else if (items.length < 2 && popup.isOpen) {
popup.hidePopup();
}
if (items.length == 1) {
popup.selectedIndex = 0;
}
this.onAutocompleteSelect();
}
let accepted = false;
if (type != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) {
this.acceptProposedCompletion();
accepted = true;
}
else if (type == this.COMPLETE_BACKWARD) {
this.autocompletePopup.selectPreviousItem();
}
else if (type == this.COMPLETE_FORWARD) {
this.autocompletePopup.selectNextItem();
}
return accepted || popup.itemCount > 0;
},
onAutocompleteSelect: function JSTF_onAutocompleteSelect()
{
let currentItem = this.autocompletePopup.selectedItem;
if (currentItem && this.lastCompletion) {
let suffix = currentItem.label.substring(this.lastCompletion.
matchProp.length);
this.updateCompleteNode(suffix);
}
else {
this.updateCompleteNode("");
}
},
/**
* Clear the current completion information and close the autocomplete popup,
* if needed.
*/
clearCompletion: function JSTF_clearCompletion()
{
this.autocompletePopup.clearItems();
this.lastCompletion = null;
this.updateCompleteNode("");
if (this.autocompletePopup.isOpen) {
this.autocompletePopup.hidePopup();
}
},
/**
* Accept the proposed input completion.
*
* @return boolean
* True if there was a selected completion item and the input value
* was updated, false otherwise.
*/
acceptProposedCompletion: function JSTF_acceptProposedCompletion()
{
let updated = false;
let currentItem = this.autocompletePopup.selectedItem;
if (currentItem && this.lastCompletion) {
let suffix = currentItem.label.substring(this.lastCompletion.
matchProp.length);
this.setInputValue(this.inputNode.value + suffix);
updated = true;
}
this.clearCompletion();
return updated;
},
/**
* Update the node that displays the currently selected autocomplete proposal.
*
* @param string aSuffix
* The proposed suffix for the inputNode value.
*/
updateCompleteNode: function JSTF_updateCompleteNode(aSuffix)
{
// completion prefix = input, with non-control chars replaced by spaces
let prefix = aSuffix ? this.inputNode.value.replace(/[\S]/g, " ") : "";
this.completeNode.value = prefix + aSuffix;
},
/**
* Clear the object cache from the Web Console content instance.
*
* @param string aCacheId
* The cache ID you want to clear. Multiple objects are cached into one
* group which is given an ID.
*/
clearObjectCache: function JST_clearObjectCache(aCacheId)
{
let hud = HUDService.getHudReferenceById(this.hudId);
hud.sendMessageToContent("JSTerm:ClearObjectCache", {cacheId: aCacheId});
},
/**
* The remote object provider allows you to retrieve a given object from
* a specific cache and have your callback invoked when the desired object is
* received from the Web Console content instance.
*
* @param string aCacheId
* Retrieve the desired object from this cache ID.
* @param string aObjectId
* The ID of the object you want.
* @param string aResultCacheId
* The ID of the cache where you want any object references to be
* stored into.
* @param function aCallback
* The function you want invoked when the desired object is retrieved.
*/
remoteObjectProvider:
function JST_remoteObjectProvider(aCacheId, aObjectId, aResultCacheId,
aCallback) {
let message = {
cacheId: aCacheId,
objectId: aObjectId,
resultCacheId: aResultCacheId,
};
let hud = HUDService.getHudReferenceById(this.hudId);
hud.sendMessageToContent("JSTerm:GetEvalObject", message, aCallback);
},
/**
* Destroy the JSTerm object. Call this method to avoid memory leaks.
*/
destroy: function JST_destroy()
{
this.inputNode.removeEventListener("keypress", this._keyPress, false);
this.inputNode.removeEventListener("input", this._inputEventHandler, false);
this.inputNode.removeEventListener("keyup", this._inputEventHandler, false);
},
};
/**
* Generates and attaches the JS Terminal part of the Web Console, which
* essentially consists of the interactive JavaScript input facility.
*
* @param nsWeakPtr<nsIDOMWindow> aContext
* A weak pointer to the DOM window that contains the Web Console.
* @param nsIDOMNode aParentNode
* The Web Console wrapper node.
* @param nsIDOMNode aExistingConsole
* The Web Console output node.
* @return void
*/
function
JSTermFirefoxMixin(aContext,
aParentNode,
aExistingConsole)
{
// aExisting Console is the existing outputNode to use in favor of
// creating a new outputNode - this is so we can just attach the inputNode to
// a normal HeadsUpDisplay console output, and re-use code.
this.context = aContext;
this.parentNode = aParentNode;
this.existingConsoleNode = aExistingConsole;
this.setTimeout = aParentNode.ownerDocument.defaultView.setTimeout;
if (aParentNode.ownerDocument) {
this.xulElementFactory =
NodeFactory("xul", "xul", aParentNode.ownerDocument);
this.textFactory = NodeFactory("text", "xul", aParentNode.ownerDocument);
this.generateUI();
this.attachUI();
}
else {
throw new Error("aParentNode should be a DOM node with an ownerDocument property ");
}
}
JSTermFirefoxMixin.prototype = {
/**
* Generates and attaches the UI for an entire JS Workspace or
* just the input node used under the console output
*
* @returns void
*/
generateUI: function JSTF_generateUI()
{
this.completeNode = this.xulElementFactory("textbox");
this.completeNode.setAttribute("class", "jsterm-complete-node");
this.completeNode.setAttribute("multiline", "true");
this.completeNode.setAttribute("rows", "1");
this.completeNode.setAttribute("tabindex", "-1");
this.inputNode = this.xulElementFactory("textbox");
this.inputNode.setAttribute("class", "jsterm-input-node");
this.inputNode.setAttribute("multiline", "true");
this.inputNode.setAttribute("rows", "1");
let inputStack = this.xulElementFactory("stack");
inputStack.setAttribute("class", "jsterm-stack-node");
inputStack.setAttribute("flex", "1");
inputStack.appendChild(this.completeNode);
inputStack.appendChild(this.inputNode);
if (this.existingConsoleNode == undefined) {
throw new Error("This can't happen");
}
this.outputNode = this.existingConsoleNode;
this.term = this.xulElementFactory("hbox");
this.term.setAttribute("class", "jsterm-input-container");
this.term.setAttribute("style", "direction: ltr;");
this.term.appendChild(inputStack);
},
get inputValue()
{
return this.inputNode.value;
},
attachUI: function JSTF_attachUI()
{
this.parentNode.appendChild(this.term);
}
};
/**
* Firefox-specific Application Hooks.
* Each Gecko-based application will need an object like this in
* order to use the Heads Up Display
*/
function FirefoxApplicationHooks()
{ }
FirefoxApplicationHooks.prototype = {
/**
* gets the current contentWindow (Firefox-specific)
*
* @returns nsIDOMWindow
*/
getCurrentContext: function FAH_getCurrentContext()
{
return Services.wm.getMostRecentWindow("navigator:browser");
},
};
//////////////////////////////////////////////////////////////////////////////
// Utility functions used by multiple callers
//////////////////////////////////////////////////////////////////////////////
/**
* ConsoleUtils: a collection of globally used functions
*
*/
ConsoleUtils = {
/**
* Flag to turn on and off scrolling.
*/
scroll: true,
/**
* Scrolls a node so that it's visible in its containing XUL "scrollbox"
* element.
*
* @param nsIDOMNode aNode
* The node to make visible.
* @returns void
*/
scrollToVisible: function ConsoleUtils_scrollToVisible(aNode) {
if (!this.scroll) {
return;
}
// Find the enclosing richlistbox node.
let richListBoxNode = aNode.parentNode;
while (richListBoxNode.tagName != "richlistbox") {
richListBoxNode = richListBoxNode.parentNode;
}
// Use the scroll box object interface to ensure the element is visible.
let boxObject = richListBoxNode.scrollBoxObject;
let nsIScrollBoxObject = boxObject.QueryInterface(Ci.nsIScrollBoxObject);
nsIScrollBoxObject.ensureElementIsVisible(aNode);
},
/**
* Given a category and message body, creates a DOM node to represent an
* incoming message. The timestamp is automatically added.
*
* @param nsIDOMDocument aDocument
* The document in which to create the node.
* @param number aCategory
* The category of the message: one of the CATEGORY_* constants.
* @param number aSeverity
* The severity of the message: one of the SEVERITY_* constants;
* @param string|nsIDOMNode aBody
* The body of the message, either a simple string or a DOM node.
* @param number aHUDId
* The HeadsUpDisplay ID.
* @param string aSourceURL [optional]
* The URL of the source file that emitted the error.
* @param number aSourceLine [optional]
* The line number on which the error occurred. If zero or omitted,
* there is no line number associated with this message.
* @param string aClipboardText [optional]
* The text that should be copied to the clipboard when this node is
* copied. If omitted, defaults to the body text. If `aBody` is not
* a string, then the clipboard text must be supplied.
* @param number aLevel [optional]
* The level of the console API message.
* @param number aTimeStamp [optional]
* The timestamp to use for this message node. If omitted, the current
* date and time is used.
* @return nsIDOMNode
* The message node: a XUL richlistitem ready to be inserted into
* the Web Console output node.
*/
createMessageNode:
function ConsoleUtils_createMessageNode(aDocument, aCategory, aSeverity,
aBody, aHUDId, aSourceURL,
aSourceLine, aClipboardText, aLevel,
aTimeStamp) {
if (typeof aBody != "string" && aClipboardText == null && aBody.innerText) {
aClipboardText = aBody.innerText;
}
// Make the icon container, which is a vertical box. Its purpose is to
// ensure that the icon stays anchored at the top of the message even for
// long multi-line messages.
let iconContainer = aDocument.createElementNS(XUL_NS, "vbox");
iconContainer.classList.add("webconsole-msg-icon-container");
// Apply the curent group by indenting appropriately.
let hud = HUDService.getHudReferenceById(aHUDId);
iconContainer.style.marginLeft = hud.groupDepth * GROUP_INDENT + "px";
// Make the icon node. It's sprited and the actual region of the image is
// determined by CSS rules.
let iconNode = aDocument.createElementNS(XUL_NS, "image");
iconNode.classList.add("webconsole-msg-icon");
iconContainer.appendChild(iconNode);
// Make the spacer that positions the icon.
let spacer = aDocument.createElementNS(XUL_NS, "spacer");
spacer.setAttribute("flex", "1");
iconContainer.appendChild(spacer);
// Create the message body, which contains the actual text of the message.
let bodyNode = aDocument.createElementNS(XUL_NS, "description");
bodyNode.setAttribute("flex", "1");
bodyNode.classList.add("webconsole-msg-body");
// Store the body text, since it is needed later for the property tree
// case.
let body = aBody;
// If a string was supplied for the body, turn it into a DOM node and an
// associated clipboard string now.
aClipboardText = aClipboardText ||
(aBody + (aSourceURL ? " @ " + aSourceURL : "") +
(aSourceLine ? ":" + aSourceLine : ""));
if (!(aBody instanceof Ci.nsIDOMNode)) {
aBody = aDocument.createTextNode(aLevel == "dir" ?
aBody.resultString : aBody);
}
if (!aBody.nodeType) {
aBody = aDocument.createTextNode(aBody.toString());
}
if (typeof aBody == "string") {
aBody = aDocument.createTextNode(aBody);
}
bodyNode.appendChild(aBody);
let repeatContainer = aDocument.createElementNS(XUL_NS, "hbox");
repeatContainer.setAttribute("align", "start");
let repeatNode = aDocument.createElementNS(XUL_NS, "label");
repeatNode.setAttribute("value", "1");
repeatNode.classList.add("webconsole-msg-repeat");
repeatContainer.appendChild(repeatNode);
// Create the timestamp.
let timestampNode = aDocument.createElementNS(XUL_NS, "label");
timestampNode.classList.add("webconsole-timestamp");
let timestamp = aTimeStamp || Date.now();
let timestampString = l10n.timestampString(timestamp);
timestampNode.setAttribute("value", timestampString);
// Create the source location (e.g. www.example.com:6) that sits on the
// right side of the message, if applicable.
let locationNode;
if (aSourceURL) {
locationNode = this.createLocationNode(aDocument, aSourceURL,
aSourceLine);
}
// Create the containing node and append all its elements to it.
let node = aDocument.createElementNS(XUL_NS, "richlistitem");
node.clipboardText = aClipboardText;
node.classList.add("hud-msg-node");
node.timestamp = timestamp;
ConsoleUtils.setMessageType(node, aCategory, aSeverity);
node.appendChild(timestampNode);
node.appendChild(iconContainer);
// Display the object tree after the message node.
if (aLevel == "dir") {
// Make the body container, which is a vertical box, for grouping the text
// and tree widgets.
let bodyContainer = aDocument.createElement("vbox");
bodyContainer.setAttribute("flex", "1");
bodyContainer.appendChild(bodyNode);
// Create the tree.
let tree = createElement(aDocument, "tree", {
flex: 1,
hidecolumnpicker: "true"
});
let treecols = aDocument.createElement("treecols");
let treecol = createElement(aDocument, "treecol", {
primary: "true",
flex: 1,
hideheader: "true",
ignoreincolumnpicker: "true"
});
treecols.appendChild(treecol);
tree.appendChild(treecols);
tree.appendChild(aDocument.createElement("treechildren"));
bodyContainer.appendChild(tree);
node.appendChild(bodyContainer);
node.classList.add("webconsole-msg-inspector");
// Create the treeView object.
let treeView = node.propertyTreeView = new PropertyTreeViewAsync();
treeView.data = {
rootCacheId: body.cacheId,
panelCacheId: body.cacheId,
remoteObject: body.remoteObject,
remoteObjectProvider: body.remoteObjectProvider,
};
tree.setAttribute("rows", treeView.rowCount);
}
else {
node.appendChild(bodyNode);
}
node.appendChild(repeatContainer);
if (locationNode) {
node.appendChild(locationNode);
}
node.setAttribute("id", "console-msg-" + HUDService.sequenceId());
return node;
},
/**
* Adjusts the category and severity of the given message, clearing the old
* category and severity if present.
*
* @param nsIDOMNode aMessageNode
* The message node to alter.
* @param number aNewCategory
* The new category for the message; one of the CATEGORY_ constants.
* @param number aNewSeverity
* The new severity for the message; one of the SEVERITY_ constants.
* @return void
*/
setMessageType:
function ConsoleUtils_setMessageType(aMessageNode, aNewCategory,
aNewSeverity) {
// Remove the old CSS classes, if applicable.
if ("category" in aMessageNode) {
let oldCategory = aMessageNode.category;
let oldSeverity = aMessageNode.severity;
aMessageNode.classList.remove("webconsole-msg-" +
CATEGORY_CLASS_FRAGMENTS[oldCategory]);
aMessageNode.classList.remove("webconsole-msg-" +
SEVERITY_CLASS_FRAGMENTS[oldSeverity]);
let key = "hud-" + MESSAGE_PREFERENCE_KEYS[oldCategory][oldSeverity];
aMessageNode.classList.remove(key);
}
// Add in the new CSS classes.
aMessageNode.category = aNewCategory;
aMessageNode.severity = aNewSeverity;
aMessageNode.classList.add("webconsole-msg-" +
CATEGORY_CLASS_FRAGMENTS[aNewCategory]);
aMessageNode.classList.add("webconsole-msg-" +
SEVERITY_CLASS_FRAGMENTS[aNewSeverity]);
let key = "hud-" + MESSAGE_PREFERENCE_KEYS[aNewCategory][aNewSeverity];
aMessageNode.classList.add(key);
},
/**
* Creates the XUL label that displays the textual location of an incoming
* message.
*
* @param nsIDOMDocument aDocument
* The document in which to create the node.
* @param string aSourceURL
* The URL of the source file responsible for the error.
* @param number aSourceLine [optional]
* The line number on which the error occurred. If zero or omitted,
* there is no line number associated with this message.
* @return nsIDOMNode
* The new XUL label node, ready to be added to the message node.
*/
createLocationNode:
function ConsoleUtils_createLocationNode(aDocument, aSourceURL,
aSourceLine) {
let locationNode = aDocument.createElementNS(XUL_NS, "label");
// Create the text, which consists of an abbreviated version of the URL
// plus an optional line number.
let text = WebConsoleUtils.abbreviateSourceURL(aSourceURL);
if (aSourceLine) {
text += ":" + aSourceLine;
}
locationNode.setAttribute("value", text);
// Style appropriately.
locationNode.setAttribute("crop", "center");
locationNode.setAttribute("title", aSourceURL);
locationNode.classList.add("webconsole-location");
locationNode.classList.add("text-link");
// Make the location clickable.
locationNode.addEventListener("click", function() {
if (aSourceURL == "Scratchpad") {
let win = Services.wm.getMostRecentWindow("devtools:scratchpad");
if (win) {
win.focus();
}
return;
}
let viewSourceUtils = aDocument.defaultView.gViewSourceUtils;
viewSourceUtils.viewSource(aSourceURL, null, aDocument, aSourceLine);
}, true);
return locationNode;
},
/**
* Applies the user's filters to a newly-created message node via CSS
* classes.
*
* @param nsIDOMNode aNode
* The newly-created message node.
* @param string aHUDId
* The ID of the HUD which this node is to be inserted into.
*/
filterMessageNode: function ConsoleUtils_filterMessageNode(aNode, aHUDId) {
// Filter by the message type.
let prefKey = MESSAGE_PREFERENCE_KEYS[aNode.category][aNode.severity];
if (prefKey && !HUDService.getFilterState(aHUDId, prefKey)) {
// The node is filtered by type.
aNode.classList.add("hud-filtered-by-type");
}
// Filter on the search string.
let search = HUDService.getFilterStringByHUDId(aHUDId);
let text = aNode.clipboardText;
// if string matches the filter text
if (!HUDService.stringMatchesFilters(text, search)) {
aNode.classList.add("hud-filtered-by-string");
}
},
/**
* Merge the attributes of the two nodes that are about to be filtered.
* Increment the number of repeats of aOriginal.
*
* @param nsIDOMNode aOriginal
* The Original Node. The one being merged into.
* @param nsIDOMNode aFiltered
* The node being filtered out because it is repeated.
*/
mergeFilteredMessageNode:
function ConsoleUtils_mergeFilteredMessageNode(aOriginal, aFiltered) {
// childNodes[3].firstChild is the node containing the number of repetitions
// of a node.
let repeatNode = aOriginal.childNodes[3].firstChild;
if (!repeatNode) {
return aOriginal; // no repeat node, return early.
}
let occurrences = parseInt(repeatNode.getAttribute("value")) + 1;
repeatNode.setAttribute("value", occurrences);
},
/**
* Filter the css node from the output node if it is a repeat. CSS messages
* are merged with previous messages if they occurred in the past.
*
* @param nsIDOMNode aNode
* The message node to be filtered or not.
* @param nsIDOMNode aOutput
* The outputNode of the HUD.
* @returns boolean
* true if the message is filtered, false otherwise.
*/
filterRepeatedCSS:
function ConsoleUtils_filterRepeatedCSS(aNode, aOutput, aHUDId) {
let hud = HUDService.getHudReferenceById(aHUDId);
// childNodes[2] is the description node containing the text of the message.
let description = aNode.childNodes[2].textContent;
let location;
// childNodes[4] represents the location (source URL) of the error message.
// The full source URL is stored in the title attribute.
if (aNode.childNodes[4]) {
// browser_webconsole_bug_595934_message_categories.js
location = aNode.childNodes[4].getAttribute("title");
}
else {
location = "";
}
let dupe = hud.cssNodes[description + location];
if (!dupe) {
// no matching nodes
hud.cssNodes[description + location] = aNode;
return false;
}
this.mergeFilteredMessageNode(dupe, aNode);
return true;
},
/**
* Filter the console node from the output node if it is a repeat. Console
* messages are filtered from the output if and only if they match the
* immediately preceding message. The output node's last occurrence should
* have its timestamp updated.
*
* @param nsIDOMNode aNode
* The message node to be filtered or not.
* @param nsIDOMNode aOutput
* The outputNode of the HUD.
* @return boolean
* true if the message is filtered, false otherwise.
*/
filterRepeatedConsole:
function ConsoleUtils_filterRepeatedConsole(aNode, aOutput) {
let lastMessage = aOutput.lastChild;
// childNodes[2] is the description element
if (lastMessage && !aNode.classList.contains("webconsole-msg-inspector") &&
aNode.childNodes[2].textContent ==
lastMessage.childNodes[2].textContent) {
this.mergeFilteredMessageNode(lastMessage, aNode);
return true;
}
return false;
},
/**
* Filters a node appropriately, then sends it to the output, regrouping and
* pruning output as necessary.
*
* @param nsIDOMNode aNode
* The message node to send to the output.
* @param string aHUDId
* The ID of the HUD in which to insert this node.
*/
outputMessageNode: function ConsoleUtils_outputMessageNode(aNode, aHUDId) {
ConsoleUtils.filterMessageNode(aNode, aHUDId);
let outputNode = HUDService.hudReferences[aHUDId].outputNode;
let scrolledToBottom = ConsoleUtils.isOutputScrolledToBottom(outputNode);
let isRepeated = false;
if (aNode.classList.contains("webconsole-msg-cssparser")) {
isRepeated = this.filterRepeatedCSS(aNode, outputNode, aHUDId);
}
if (!isRepeated &&
(aNode.classList.contains("webconsole-msg-console") ||
aNode.classList.contains("webconsole-msg-exception") ||
aNode.classList.contains("webconsole-msg-error"))) {
isRepeated = this.filterRepeatedConsole(aNode, outputNode);
}
if (!isRepeated) {
outputNode.appendChild(aNode);
}
HUDService.regroupOutput(outputNode);
if (pruneConsoleOutputIfNecessary(aHUDId, aNode.category) == 0) {
// We can't very well scroll to make the message node visible if the log
// limit is zero and the node was destroyed in the first place.
return;
}
let isInputOutput = aNode.classList.contains("webconsole-msg-input") ||
aNode.classList.contains("webconsole-msg-output");
let isFiltered = aNode.classList.contains("hud-filtered-by-string") ||
aNode.classList.contains("hud-filtered-by-type");
// Scroll to the new node if it is not filtered, and if the output node is
// scrolled at the bottom or if the new node is a jsterm input/output
// message.
if (!isFiltered && !isRepeated && (scrolledToBottom || isInputOutput)) {
ConsoleUtils.scrollToVisible(aNode);
}
let id = WebConsoleUtils.supportsString(aHUDId);
let nodeID = aNode.getAttribute("id");
Services.obs.notifyObservers(id, "web-console-message-created", nodeID);
},
/**
* Check if the given output node is scrolled to the bottom.
*
* @param nsIDOMNode aOutputNode
* @return boolean
* True if the output node is scrolled to the bottom, or false
* otherwise.
*/
isOutputScrolledToBottom:
function ConsoleUtils_isOutputScrolledToBottom(aOutputNode)
{
let lastNodeHeight = aOutputNode.lastChild ?
aOutputNode.lastChild.clientHeight : 0;
let scrollBox = aOutputNode.scrollBoxObject.element;
return scrollBox.scrollTop + scrollBox.clientHeight >=
scrollBox.scrollHeight - lastNodeHeight / 2;
},
};
//////////////////////////////////////////////////////////////////////////
// HeadsUpDisplayUICommands
//////////////////////////////////////////////////////////////////////////
HeadsUpDisplayUICommands = {
refreshCommand: function UIC_refreshCommand() {
var window = HUDService.currentContext();
let command = window.document.getElementById("Tools:WebConsole");
if (this.getOpenHUD() != null) {
command.setAttribute("checked", true);
} else {
command.removeAttribute("checked");
}
},
toggleHUD: function UIC_toggleHUD() {
var window = HUDService.currentContext();
var gBrowser = window.gBrowser;
var linkedBrowser = gBrowser.selectedTab.linkedBrowser;
var tabId = gBrowser.getNotificationBox(linkedBrowser).getAttribute("id");
var hudId = "hud_" + tabId;
var ownerDocument = gBrowser.selectedTab.ownerDocument;
var hud = ownerDocument.getElementById(hudId);
var hudRef = HUDService.hudReferences[hudId];
if (hudRef && hud) {
if (hudRef.consolePanel) {
hudRef.consolePanel.hidePopup();
}
else {
HUDService.storeHeight(hudId);
HUDService.animate(hudId, ANIMATE_OUT, function() {
// If the user closes the console while the console is animating away,
// then these callbacks will queue up, but all the callbacks after the
// first will have no console to operate on. This test handles this
// case gracefully.
if (ownerDocument.getElementById(hudId)) {
HUDService.deactivateHUDForContext(gBrowser.selectedTab, true);
}
});
}
}
else {
HUDService.activateHUDForContext(gBrowser.selectedTab, true);
HUDService.animate(hudId, ANIMATE_IN);
}
},
/**
* Find the hudId for the active chrome window.
* @return string|null
* The hudId or null if the active chrome window has no open Web
* Console.
*/
getOpenHUD: function UIC_getOpenHUD() {
let chromeWindow = HUDService.currentContext();
let contentWindow = chromeWindow.gBrowser.selectedBrowser.contentWindow;
return HUDService.getHudIdByWindow(contentWindow);
},
/**
* The event handler that is called whenever a user switches a filter on or
* off.
*
* @param nsIDOMEvent aEvent
* The event that triggered the filter change.
* @return boolean
*/
toggleFilter: function UIC_toggleFilter(aEvent) {
let hudId = this.getAttribute("hudId");
switch (this.tagName) {
case "toolbarbutton": {
let originalTarget = aEvent.originalTarget;
let classes = originalTarget.classList;
if (originalTarget.localName !== "toolbarbutton") {
// Oddly enough, the click event is sent to the menu button when
// selecting a menu item with the mouse. Detect this case and bail
// out.
break;
}
if (!classes.contains("toolbarbutton-menubutton-button") &&
originalTarget.getAttribute("type") === "menu-button") {
// This is a filter button with a drop-down. The user clicked the
// drop-down, so do nothing. (The menu will automatically appear
// without our intervention.)
break;
}
let state = this.getAttribute("checked") !== "true";
this.setAttribute("checked", state);
// This is a filter button with a drop-down, and the user clicked the
// main part of the button. Go through all the severities and toggle
// their associated filters.
let menuItems = this.querySelectorAll("menuitem");
for (let i = 0; i < menuItems.length; i++) {
menuItems[i].setAttribute("checked", state);
let prefKey = menuItems[i].getAttribute("prefKey");
HUDService.setFilterState(hudId, prefKey, state);
}
break;
}
case "menuitem": {
let state = this.getAttribute("checked") !== "true";
this.setAttribute("checked", state);
let prefKey = this.getAttribute("prefKey");
HUDService.setFilterState(hudId, prefKey, state);
// Adjust the state of the button appropriately.
let menuPopup = this.parentNode;
let someChecked = false;
let menuItem = menuPopup.firstChild;
while (menuItem) {
if (menuItem.getAttribute("checked") === "true") {
someChecked = true;
break;
}
menuItem = menuItem.nextSibling;
}
let toolbarButton = menuPopup.parentNode;
toolbarButton.setAttribute("checked", someChecked);
break;
}
}
return true;
},
command: function UIC_command(aButton) {
var filter = aButton.getAttribute("buttonType");
var hudId = aButton.getAttribute("hudId");
switch (filter) {
case "copy": {
let outputNode = HUDService.hudReferences[hudId].outputNode;
HUDService.copySelectedItems(outputNode);
break;
}
case "selectAll": {
HUDService.hudReferences[hudId].outputNode.selectAll();
break;
}
case "saveBodies": {
let checked = aButton.getAttribute("checked") === "true";
HUDService.saveRequestAndResponseBodies = checked;
break;
}
}
},
};
/**
* A Console log entry
*
* @param JSObject aConfig, object literal with ConsolEntry properties
* @param integer aId
* @returns void
*/
function ConsoleEntry(aConfig, id)
{
if (!aConfig.logLevel && aConfig.message) {
throw new Error("Missing Arguments when creating a console entry");
}
this.config = aConfig;
this.id = id;
for (var prop in aConfig) {
if (!(typeof aConfig[prop] == "function")){
this[prop] = aConfig[prop];
}
}
if (aConfig.logLevel == "network") {
this.transactions = { };
if (aConfig.activity) {
this.transactions[aConfig.activity.stage] = aConfig.activity;
}
}
}
ConsoleEntry.prototype = {
updateTransaction: function CE_updateTransaction(aActivity) {
this.transactions[aActivity.stage] = aActivity;
}
};
//////////////////////////////////////////////////////////////////////////
// HUDWindowObserver
//////////////////////////////////////////////////////////////////////////
HUDWindowObserver = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
init: function HWO_init()
{
Services.obs.addObserver(this, "content-document-global-created", false);
Services.obs.addObserver(this, "quit-application-granted", false);
},
observe: function HWO_observe(aSubject, aTopic, aData)
{
if (aTopic == "content-document-global-created") {
HUDService.windowInitializer(aSubject);
}
else if (aTopic == "quit-application-granted") {
HUDService.shutdown();
}
},
uninit: function HWO_uninit()
{
Services.obs.removeObserver(this, "content-document-global-created");
Services.obs.removeObserver(this, "quit-application-granted");
},
};
///////////////////////////////////////////////////////////////////////////////
// CommandController
///////////////////////////////////////////////////////////////////////////////
/**
* A controller (an instance of nsIController) that makes editing actions
* behave appropriately in the context of the Web Console.
*/
function CommandController(aWindow) {
this.window = aWindow;
}
CommandController.prototype = {
/**
* Returns the HUD output node that currently has the focus, or null if the
* currently-focused element isn't inside the output node.
*
* @returns nsIDOMNode
* The currently-focused output node.
*/
_getFocusedOutputNode: function CommandController_getFocusedOutputNode()
{
let element = this.window.document.commandDispatcher.focusedElement;
if (element && element.classList.contains("hud-output-node")) {
return element;
}
return null;
},
/**
* Copies the currently-selected entries in the Web Console output to the
* clipboard.
*
* @param nsIDOMNode aOutputNode
* The Web Console output node.
* @returns void
*/
copy: function CommandController_copy(aOutputNode)
{
HUDService.copySelectedItems(aOutputNode);
},
/**
* Selects all the text in the HUD output.
*
* @param nsIDOMNode aOutputNode
* The HUD output node.
* @returns void
*/
selectAll: function CommandController_selectAll(aOutputNode)
{
aOutputNode.selectAll();
},
supportsCommand: function CommandController_supportsCommand(aCommand)
{
return this.isCommandEnabled(aCommand);
},
isCommandEnabled: function CommandController_isCommandEnabled(aCommand)
{
let outputNode = this._getFocusedOutputNode();
if (!outputNode) {
return false;
}
switch (aCommand) {
case "cmd_copy":
// Only enable "copy" if nodes are selected.
return outputNode.selectedCount > 0;
case "cmd_selectAll":
// "Select All" is always enabled.
return true;
}
},
doCommand: function CommandController_doCommand(aCommand)
{
let outputNode = this._getFocusedOutputNode();
switch (aCommand) {
case "cmd_copy":
this.copy(outputNode);
break;
case "cmd_selectAll":
this.selectAll(outputNode);
break;
}
}
};
/**
* A WebProgressListener that listens for location changes, to update HUDService
* state information on page navigation.
*
* @constructor
* @param string aHudId
* The HeadsUpDisplay ID.
*/
function ConsoleProgressListener(aHudId)
{
this.hudId = aHudId;
}
ConsoleProgressListener.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference]),
onStateChange: function CPL_onStateChange(aProgress, aRequest, aState,
aStatus)
{
if (!(aState & Ci.nsIWebProgressListener.STATE_START)) {
return;
}
let uri = null;
if (aRequest instanceof Ci.imgIRequest) {
let imgIRequest = aRequest.QueryInterface(Ci.imgIRequest);
uri = imgIRequest.URI;
}
else if (aRequest instanceof Ci.nsIChannel) {
let nsIChannel = aRequest.QueryInterface(Ci.nsIChannel);
uri = nsIChannel.URI;
}
if (!uri || !uri.schemeIs("file") && !uri.schemeIs("ftp")) {
return;
}
let outputNode = HUDService.hudReferences[this.hudId].outputNode;
let chromeDocument = outputNode.ownerDocument;
let msgNode = chromeDocument.createElementNS(HTML_NS, "html:span");
// Create the clickable URL part of the message.
let linkNode = chromeDocument.createElementNS(HTML_NS, "html:span");
linkNode.appendChild(chromeDocument.createTextNode(uri.spec));
linkNode.classList.add("hud-clickable");
linkNode.classList.add("webconsole-msg-url");
linkNode.addEventListener("mousedown", function(aEvent) {
this._startX = aEvent.clientX;
this._startY = aEvent.clientY;
}, false);
linkNode.addEventListener("click", function(aEvent) {
if (aEvent.detail == 1 && aEvent.button == 0 &&
this._startX == aEvent.clientX && this._startY == aEvent.clientY) {
let viewSourceUtils = chromeDocument.defaultView.gViewSourceUtils;
viewSourceUtils.viewSource(uri.spec, null, chromeDocument);
}
}, false);
msgNode.appendChild(linkNode);
let messageNode = ConsoleUtils.createMessageNode(chromeDocument,
CATEGORY_NETWORK,
SEVERITY_LOG,
msgNode,
this.hudId,
null,
null,
uri.spec);
ConsoleUtils.outputMessageNode(messageNode, this.hudId);
},
onLocationChange: function() {},
onStatusChange: function() {},
onProgressChange: function() {},
onSecurityChange: function() {},
};
///////////////////////////////////////////////////////////////////////////
// appName
///////////////////////////////////////////////////////////////////////////
/**
* Get the app's name so we can properly dispatch app-specific
* methods per API call
* @returns Gecko application name
*/
function appName()
{
let APP_ID = Services.appinfo.QueryInterface(Ci.nsIXULRuntime).ID;
let APP_ID_TABLE = {
"{ec8030f7-c20a-464f-9b0e-13a3a9e97384}": "FIREFOX" ,
"{3550f703-e582-4d05-9a08-453d09bdfdc6}": "THUNDERBIRD",
"{a23983c0-fd0e-11dc-95ff-0800200c9a66}": "FENNEC" ,
"{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}": "SEAMONKEY",
};
let name = APP_ID_TABLE[APP_ID];
if (name){
return name;
}
throw new Error("appName: UNSUPPORTED APPLICATION UUID");
}
XPCOMUtils.defineLazyGetter(this, "HUDService", function () {
try {
return new HUD_SERVICE();
}
catch (ex) {
Cu.reportError(ex);
}
});
///////////////////////////////////////////////////////////////////////////
// GcliTerm
///////////////////////////////////////////////////////////////////////////
/**
* Some commands need customization - this is how we get at them.
*/
let commandExports = undefined;
/**
* GcliTerm
*
* Initialize GCLI by creating a set of startup options from the available
* properties.
*
* @param nsIDOMWindow aContentWindow
* The content window that we're providing as the context to commands
* @param string aHudId
* The HUD to which we should send console messages.
* @param nsIDOMDocument aDocument
* The DOM document from which to create nodes.
* @param object aConsole
* Console object to use within the GcliTerm.
* @param nsIDOMElement aHintNode
* The node to which we add GCLI's hints.
* @constructor
*/
function GcliTerm(aContentWindow, aHudId, aDocument, aConsole, aHintNode, aConsoleWrap)
{
this.context = Cu.getWeakReference(aContentWindow);
this.hudId = aHudId;
this.document = aDocument;
this.console = aConsole;
this.hintNode = aHintNode;
this._window = this.context.get().QueryInterface(Ci.nsIDOMWindow);
this.createUI();
this.createSandbox();
this.gcliConsole = gcli._internal.createDisplay({
contentDocument: aContentWindow.document,
chromeDocument: this.document,
outputDocument: this.document,
chromeWindow: this.document.defaultView,
hintElement: this.hintNode,
inputElement: this.inputNode,
completeElement: this.completeNode,
backgroundElement: this.inputStack,
consoleWrap: aConsoleWrap,
eval: this.evalInSandbox.bind(this),
environment: {
chromeDocument: this.document,
contentDocument: aContentWindow.document
},
tooltipClass: 'gcliterm-tooltip',
// Allow GCLI:Inputter to decide how and when to open a scratchpad window
scratchpad: {
shouldActivate: function Scratchpad_shouldActivate(aEvent) {
return aEvent.shiftKey &&
aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN;
},
activate: function Scratchpad_activate(aValue) {
aValue = aValue.replace(/^\s*{\s*/, '');
ScratchpadManager.openScratchpad({ text: aValue });
return true;
},
linkText: stringBundle.GetStringFromName('scratchpad.linkText')
},
});
this.gcliConsole.onVisibilityChange.add(this.onVisibilityChange, this);
this.gcliConsole.onOutput.add(this.onOutput, this);
}
GcliTerm.prototype = {
/**
* Show or remove the hint column from the display.
*/
onVisibilityChange: function GcliTerm_onVisibilityChange(ev)
{
if (ev.visible) {
this.hintNode.parentNode.hidden = false;
}
else {
let permaHint = false;
try {
permaHint = Services.prefs.getBoolPref("devtools.gcli.permaHint");
}
catch (ex) {}
if (!permaHint) {
this.hintNode.parentNode.hidden = true;
}
}
},
/**
* Destroy the GcliTerm object. Call this method to avoid memory leaks.
*/
destroy: function Gcli_destroy()
{
this.gcliConsole.onVisibilityChange.remove(this.onVisibilityChange, this);
this.gcliConsole.onOutput.remove(this.onOutput, this);
this.gcliConsole.destroy();
delete this.context;
delete this.document;
delete this.console;
delete this.hintNode;
delete this._window;
delete this.sandbox;
delete this.element;
delete this.inputStack;
delete this.completeNode;
delete this.inputNode;
},
/**
* Re-attaches a console when the contentWindow is recreated.
*
* @param nsIDOMWindow aContentWindow
* The content window that we're providing as the context to commands
* @param object aConsole
* Console object to use within the GcliTerm.
*/
reattachConsole: function Gcli_reattachConsole(aContentWindow, aConsole)
{
this.context = Cu.getWeakReference(aContentWindow);
this.console = aConsole;
this.createSandbox();
this.gcliConsole.reattach({
contentDocument: aContentWindow.document,
environment: {
chromeDocument: this.document,
contentDocument: aContentWindow.document
},
});
},
/**
* Generates and attaches the GCLI Terminal part of the Web Console, which
* essentially consists of the interactive JavaScript input facility.
*/
createUI: function Gcli_createUI()
{
this.element = this.document.createElement("vbox");
this.element.setAttribute("class", "gcliterm-input-container");
this.element.setAttribute("flex", "0");
this.inputStack = this.document.createElement("stack");
this.inputStack.setAttribute("class", "gcliterm-stack-node");
this.element.appendChild(this.inputStack);
this.completeNode = this.document.createElementNS(HTML_NS, "div");
this.completeNode.setAttribute("class", "gcliterm-complete-node");
this.completeNode.setAttribute("aria-live", "polite");
this.inputStack.appendChild(this.completeNode);
this.inputNode = this.document.createElement("textbox");
this.inputNode.setAttribute("class", "gcliterm-input-node");
this.inputNode.setAttribute("rows", "1");
this.inputStack.appendChild(this.inputNode);
},
/**
* Called by GCLI/canon when command line output changes.
*/
onOutput: function Gcli_onOutput(aEvent)
{
// When we can update the history of the console, then we should stop
// filtering incomplete reports.
if (!aEvent.output.completed) {
return;
}
this.writeOutput(aEvent.output.typed, CATEGORY_INPUT);
// This is an experiment to see how much people yell when we stop reporting
// undefined replies.
if (aEvent.output.output === undefined) {
return;
}
let output = aEvent.output.output;
let declaredType = aEvent.output.command.returnType || "";
if (declaredType == "object") {
let actualType = typeof output;
if (output === null) {
output = "null";
}
else if (actualType == "string") {
output = "\"" + output + "\"";
}
else if (actualType == "object" || actualType == "function") {
let formatOpts = [ nameObject(output) ];
output = stringBundle.formatStringFromName('gcliterm.instanceLabel',
formatOpts, formatOpts.length);
let linkNode = this.document.createElementNS(HTML_NS, 'html:span');
linkNode.appendChild(this.document.createTextNode(output));
linkNode.classList.add("hud-clickable");
linkNode.setAttribute("aria-haspopup", "true");
// Make the object bring up the property panel.
linkNode.addEventListener("mousedown", function(aEv) {
this._startX = aEv.clientX;
this._startY = aEv.clientY;
}.bind(this), false);
linkNode.addEventListener("click", function(aEv) {
if (aEv.detail != 1 || aEv.button != 0 ||
(this._startX != aEv.clientX && this._startY != aEv.clientY)) {
return;
}
if (!this._panelOpen) {
let propPanel = this.openPropertyPanel(aEvent.output.typed, aEvent.output.output, this);
propPanel.panel.setAttribute("hudId", this.hudId);
this._panelOpen = true;
}
}.bind(this), false);
output = linkNode;
}
// else if (actualType == number/boolean/undefined) do nothing
}
if (declaredType == "html" && typeof output == "string") {
output = this.document.createRange().createContextualFragment(
'<div xmlns="' + HTML_NS + '" xmlns:xul="' + XUL_NS + '">' +
output + '</div>');
}
// See https://github.com/mozilla/domtemplate/blob/master/README.md
// for docs on the template() function
let element = this.document.createRange().createContextualFragment(
'<richlistitem xmlns="' + XUL_NS + '" _clipboardText="${clipboardText}"' +
' _timestamp="${timestamp}" _id="${id}" class="hud-msg-node">' +
' <label class="webconsole-timestamp" _value="${timestampString}"/>' +
' <vbox class="webconsole-msg-icon-container" _style="${iconContainerStyle}">' +
' <image class="webconsole-msg-icon"/>' +
' <spacer flex="1"/>' +
' </vbox>' +
' <hbox flex="1" class="gcliterm-msg-body">${output}</hbox>' +
' <hbox align="start"><label value="1" class="webconsole-msg-repeat"/></hbox>' +
'</richlistitem>').firstChild;
let hud = HUDService.getHudReferenceById(this.hudId);
let timestamp = Date.now();
template(element, {
iconContainerStyle: "margin-left=" + (hud.groupDepth * GROUP_INDENT) + "px",
output: output,
timestamp: timestamp,
timestampString: l10n.timestampString(timestamp),
clipboardText: output.innerText,
id: "console-msg-" + HUDService.sequenceId()
});
ConsoleUtils.setMessageType(element, CATEGORY_OUTPUT, SEVERITY_LOG);
ConsoleUtils.outputMessageNode(element, this.hudId);
},
/**
* Setup the eval sandbox, should be called whenever we are attached.
*/
createSandbox: function Gcli_createSandbox()
{
// create a JS Sandbox out of this.context
this.sandbox = new Cu.Sandbox(this._window, {
sandboxPrototype: this._window,
wantXrays: false
});
this.sandbox.console = this.console;
JSTermHelper(this);
},
/**
* Evaluates a string in the sandbox.
*
* @param string aString
* String to evaluate in the sandbox
* @return The result of the evaluation
*/
evalInSandbox: function Gcli_evalInSandbox(aString)
{
let window = WebConsoleUtils.unwrap(this.sandbox.window);
let temp$ = null;
let temp$$ = null;
// We prefer to execute the page-provided implementations for the $() and
// $$() functions.
if (typeof window.$ == "function") {
temp$ = this.sandbox.$;
delete this.sandbox.$;
}
if (typeof window.$$ == "function") {
temp$$ = this.sandbox.$$;
delete this.sandbox.$$;
}
let result = Cu.evalInSandbox(aString, this.sandbox, "1.8", "Web Console", 1);
if (temp$) {
this.sandbox.$ = temp$;
}
if (temp$$) {
this.sandbox.$$ = temp$$;
}
return result;
},
/**
* Writes a message to the HUD that originates from the interactive
* JavaScript console.
*
* @param string aOutputMessage
* The message to display.
* @param number aCategory
* One of the CATEGORY_ constants.
* @param number aSeverity
* One of the SEVERITY_ constants.
*/
writeOutput: function Gcli_writeOutput(aOutputMessage, aCategory, aSeverity, aOptions)
{
aOptions = aOptions || {};
let node = ConsoleUtils.createMessageNode(
this.document,
aCategory || CATEGORY_OUTPUT,
aSeverity || SEVERITY_LOG,
aOutputMessage,
this.hudId,
aOptions.sourceUrl || undefined,
aOptions.sourceLine || undefined,
aOptions.clipboardText || undefined);
ConsoleUtils.outputMessageNode(node, this.hudId);
},
clearOutput: JSTerm.prototype.clearOutput,
openPropertyPanel: JSTerm.prototype.openPropertyPanel,
formatResult: WebConsoleUtils.formatResult,
getResultType: WebConsoleUtils.getResultType,
formatString: WebConsoleUtils.formatResultString,
};
/**
* A fancy version of toString()
*/
function nameObject(aObj) {
if (aObj.constructor && aObj.constructor.name) {
return aObj.constructor.name;
}
// If that fails, use Objects toString which sometimes gives something
// better than 'Object', and at least defaults to Object if nothing better
return Object.prototype.toString.call(aObj).slice(8, -1);
}