mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
4812 lines
139 KiB
JavaScript
4812 lines
139 KiB
JavaScript
/* -*- 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>
|
|
*
|
|
* 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;
|
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
|
|
var EXPORTED_SYMBOLS = ["HUDService"];
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(this, "scriptError",
|
|
"@mozilla.org/scripterror;1",
|
|
"nsIScriptError");
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(this, "activityDistributor",
|
|
"@mozilla.org/network/http-activity-distributor;1",
|
|
"nsIHttpActivityDistributor");
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(this, "sss",
|
|
"@mozilla.org/content/style-sheet-service;1",
|
|
"nsIStyleSheetService");
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "NetUtil", function () {
|
|
var obj = {};
|
|
Cu.import("resource://gre/modules/NetUtil.jsm", obj);
|
|
return obj.NetUtil;
|
|
});
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "PropertyPanel", function () {
|
|
var obj = {};
|
|
try {
|
|
Cu.import("resource://gre/modules/PropertyPanel.jsm", obj);
|
|
} catch (err) {
|
|
Cu.reportError(err);
|
|
}
|
|
return obj.PropertyPanel;
|
|
});
|
|
|
|
|
|
function LogFactory(aMessagePrefix)
|
|
{
|
|
function log(aMessage) {
|
|
var _msg = aMessagePrefix + " " + aMessage + "\n";
|
|
dump(_msg);
|
|
}
|
|
return log;
|
|
}
|
|
|
|
let log = LogFactory("*** HUDService:");
|
|
|
|
const HUD_STYLESHEET_URI = "chrome://global/skin/webConsole.css";
|
|
const HUD_STRINGS_URI = "chrome://global/locale/headsUpDisplay.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;
|
|
|
|
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",
|
|
};
|
|
|
|
/**
|
|
* 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 original listener for this request.
|
|
*/
|
|
originalListener: null,
|
|
|
|
/**
|
|
* The HttpActivity object associated with this response.
|
|
*/
|
|
httpActivity: null,
|
|
|
|
/**
|
|
* Stores the received data as a string.
|
|
*/
|
|
receivedData: 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) {
|
|
httpActivity.response.header = {};
|
|
if (aRequest instanceof Ci.nsIHttpChannel) {
|
|
aRequest.visitResponseHeaders({
|
|
visitHeader: function(aName, aValue) {
|
|
httpActivity.response.header[aName] = aValue;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* See documention at
|
|
* https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIStreamListener
|
|
*
|
|
* Grabs a copy of the original data and passes it on to the original listener.
|
|
*
|
|
* @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 StorageStream = Components.Constructor("@mozilla.org/storagestream;1",
|
|
"nsIStorageStream",
|
|
"init");
|
|
let BinaryOutputStream = Components.Constructor("@mozilla.org/binaryoutputstream;1",
|
|
"nsIBinaryOutputStream",
|
|
"setOutputStream");
|
|
|
|
storageStream = new StorageStream(8192, aCount, null);
|
|
binaryOutputStream = new BinaryOutputStream(storageStream.getOutputStream(0));
|
|
|
|
let data = NetUtil.readInputStreamToString(aInputStream, aCount);
|
|
this.receivedData += data;
|
|
binaryOutputStream.writeBytes(data, aCount);
|
|
|
|
this.originalListener.onDataAvailable(aRequest, aContext,
|
|
storageStream.newInputStream(0), aOffset, aCount);
|
|
},
|
|
|
|
/**
|
|
* See documentation at
|
|
* https://developer.mozilla.org/En/NsIRequestObserver
|
|
*
|
|
* @param nsIRequest aRequest
|
|
* @param nsISupports aContext
|
|
*/
|
|
onStartRequest: function RL_onStartRequest(aRequest, aContext)
|
|
{
|
|
this.originalListener.onStartRequest(aRequest, aContext);
|
|
},
|
|
|
|
/**
|
|
* See documentation at
|
|
* https://developer.mozilla.org/En/NsIRequestObserver
|
|
*
|
|
* If aRequest is an nsIHttpChannel then the response header is stored on the
|
|
* httpActivity object. Also, the response body is set on the httpActivity
|
|
* object and the HUDService.lastFinishedRequestCallback is called if there
|
|
* is one.
|
|
*
|
|
* @param nsIRequest aRequest
|
|
* @param nsISupports aContext
|
|
* @param nsresult aStatusCode
|
|
*/
|
|
onStopRequest: function RL_onStopRequest(aRequest, aContext, aStatusCode)
|
|
{
|
|
this.originalListener.onStopRequest(aRequest, aContext, aStatusCode);
|
|
|
|
this.setResponseHeader(aRequest);
|
|
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;
|
|
},
|
|
|
|
QueryInterface: XPCOMUtils.generateQI([
|
|
Ci.nsIStreamListener,
|
|
Ci.nsISupports
|
|
])
|
|
}
|
|
|
|
/**
|
|
* Helper object for networking stuff.
|
|
*
|
|
* All of the following functions have been taken from the Firebug source. They
|
|
* have been modified to match the Firefox coding rules.
|
|
*/
|
|
|
|
// FIREBUG CODE BEGIN.
|
|
|
|
/*
|
|
* Software License Agreement (BSD License)
|
|
*
|
|
* Copyright (c) 2007, Parakey Inc.
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use of this software in source and binary forms, with or without modification,
|
|
* are permitted provided that the following conditions are met:
|
|
*
|
|
* * Redistributions of source code must retain the above
|
|
* copyright notice, this list of conditions and the
|
|
* following disclaimer.
|
|
*
|
|
* * Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the
|
|
* following disclaimer in the documentation and/or other
|
|
* materials provided with the distribution.
|
|
*
|
|
* * Neither the name of Parakey Inc. nor the names of its
|
|
* contributors may be used to endorse or promote products
|
|
* derived from this software without specific prior
|
|
* written permission of Parakey Inc.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
|
|
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
|
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
|
|
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
/*
|
|
* Creator:
|
|
* Joe Hewitt
|
|
* Contributors
|
|
* John J. Barton (IBM Almaden)
|
|
* Jan Odvarko (Mozilla Corp.)
|
|
* Max Stepanov (Aptana Inc.)
|
|
* Rob Campbell (Mozilla Corp.)
|
|
* Hans Hillen (Paciello Group, Mozilla)
|
|
* Curtis Bartley (Mozilla Corp.)
|
|
* Mike Collins (IBM Almaden)
|
|
* Kevin Decker
|
|
* Mike Ratcliffe (Comartis AG)
|
|
* Hernan RodrÃguez Colmeiro
|
|
* Austin Andrews
|
|
* Christoph Dorn
|
|
* Steven Roussey (AppCenter Inc, Network54)
|
|
*/
|
|
var NetworkHelper =
|
|
{
|
|
/**
|
|
* Converts aText with a given aCharset to unicode.
|
|
*
|
|
* @param string aText
|
|
* Text to convert.
|
|
* @param string aCharset
|
|
* Charset to convert the text to.
|
|
* @returns string
|
|
* Converted text.
|
|
*/
|
|
convertToUnicode: function NH_convertToUnicode(aText, aCharset)
|
|
{
|
|
let conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
|
|
createInstance(Ci.nsIScriptableUnicodeConverter);
|
|
conv.charset = aCharset || "UTF-8";
|
|
return conv.ConvertToUnicode(aText);
|
|
},
|
|
|
|
/**
|
|
* Reads all available bytes from aStream and converts them to aCharset.
|
|
*
|
|
* @param nsIInputStream aStream
|
|
* @param string aCharset
|
|
* @returns string
|
|
* UTF-16 encoded string based on the content of aStream and aCharset.
|
|
*/
|
|
readAndConvertFromStream: function NH_readAndConvertFromStream(aStream, aCharset)
|
|
{
|
|
let text = null;
|
|
try {
|
|
text = NetUtil.readInputStreamToString(aStream, aStream.available())
|
|
return this.convertToUnicode(text, aCharset);
|
|
}
|
|
catch (err) {
|
|
return text;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Reads the posted text from aRequest.
|
|
*
|
|
* @param nsIHttpChannel aRequest
|
|
* @param nsIDOMNode aBrowser
|
|
* @returns string or null
|
|
* Returns the posted string if it was possible to read from aRequest
|
|
* otherwise null.
|
|
*/
|
|
readPostTextFromRequest: function NH_readPostTextFromRequest(aRequest, aBrowser)
|
|
{
|
|
if (aRequest instanceof Ci.nsIUploadChannel) {
|
|
let iStream = aRequest.uploadStream;
|
|
|
|
let isSeekableStream = false;
|
|
if (iStream instanceof Ci.nsISeekableStream) {
|
|
isSeekableStream = true;
|
|
}
|
|
|
|
let prevOffset;
|
|
if (isSeekableStream) {
|
|
prevOffset = iStream.tell();
|
|
iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
|
|
}
|
|
|
|
// Read data from the stream.
|
|
let charset = aBrowser.contentWindow.document.characterSet;
|
|
let text = this.readAndConvertFromStream(iStream, charset);
|
|
|
|
// Seek locks the file, so seek to the beginning only if necko hasn't
|
|
// read it yet, since necko doesn't seek to 0 before reading (at lest
|
|
// not till 459384 is fixed).
|
|
if (isSeekableStream && prevOffset == 0) {
|
|
iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
|
|
}
|
|
return text;
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Reads the posted text from the page's cache.
|
|
*
|
|
* @param nsIDOMNode aBrowser
|
|
* @returns string or null
|
|
* Returns the posted string if it was possible to read from aBrowser
|
|
* otherwise null.
|
|
*/
|
|
readPostTextFromPage: function NH_readPostTextFromPage(aBrowser)
|
|
{
|
|
let webNav = aBrowser.webNavigation;
|
|
if (webNav instanceof Ci.nsIWebPageDescriptor) {
|
|
let descriptor = webNav.currentDescriptor;
|
|
|
|
if (descriptor instanceof Ci.nsISHEntry && descriptor.postData &&
|
|
descriptor instanceof Ci.nsISeekableStream) {
|
|
descriptor.seek(NS_SEEK_SET, 0);
|
|
|
|
let charset = browser.contentWindow.document.characterSet;
|
|
return this.readAndConvertFromStream(descriptor, charset);
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Gets the nsIDOMWindow that is associated with aRequest.
|
|
*
|
|
* @param nsIHttpChannel aRequest
|
|
* @returns nsIDOMWindow or null
|
|
*/
|
|
getWindowForRequest: function NH_getWindowForRequest(aRequest)
|
|
{
|
|
let loadContext = this.getRequestLoadContext(aRequest);
|
|
if (loadContext) {
|
|
return loadContext.associatedWindow;
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Gets the nsILoadContext that is associated with aRequest.
|
|
*
|
|
* @param nsIHttpChannel aRequest
|
|
* @returns nsILoadContext or null
|
|
*/
|
|
getRequestLoadContext: function NH_getRequestLoadContext(aRequest)
|
|
{
|
|
if (aRequest && aRequest.notificationCallbacks) {
|
|
try {
|
|
return aRequest.notificationCallbacks.getInterface(Ci.nsILoadContext);
|
|
} catch (ex) { }
|
|
}
|
|
|
|
if (aRequest && aRequest.loadGroup
|
|
&& aRequest.loadGroup.notificationCallbacks) {
|
|
try {
|
|
return aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
|
|
} catch (ex) { }
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Loads the content of aUrl from the cache.
|
|
*
|
|
* @param string aUrl
|
|
* URL to load the cached content for.
|
|
* @param string aCharset
|
|
* Assumed charset of the cached content. Used if there is no charset
|
|
* on the channel directly.
|
|
* @param function aCallback
|
|
* Callback that is called with the loaded cached content if available
|
|
* or null if something failed while getting the cached content.
|
|
*/
|
|
loadFromCache: function NH_loadFromCache(aUrl, aCharset, aCallback)
|
|
{
|
|
let channel = NetUtil.newChannel(aUrl);
|
|
|
|
// Ensure that we only read from the cache and not the server.
|
|
channel.loadFlags = Ci.nsIRequest.LOAD_FROM_CACHE |
|
|
Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE |
|
|
Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY;
|
|
|
|
NetUtil.asyncFetch(channel, function (aInputStream, aStatusCode, aRequest) {
|
|
if (!Components.isSuccessCode(aStatusCode)) {
|
|
aCallback(null);
|
|
return;
|
|
}
|
|
|
|
// Try to get the encoding from the channel. If there is none, then use
|
|
// the passed assumed aCharset.
|
|
let aChannel = aRequest.QueryInterface(Ci.nsIChannel);
|
|
let contentCharset = aChannel.contentCharset || aCharset;
|
|
|
|
// Read the content of the stream using contentCharset as encoding.
|
|
aCallback(NetworkHelper.readAndConvertFromStream(aInputStream,
|
|
contentCharset));
|
|
});
|
|
}
|
|
}
|
|
|
|
// FIREBUG CODE END.
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
//// 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);
|
|
for (var 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: HUDService.getStr("NetworkPanel.label"),
|
|
titlebar: "normal",
|
|
noautofocus: "true",
|
|
noautohide: "true",
|
|
close: "true"
|
|
});
|
|
|
|
// Create the browser that displays the NetworkPanel XHTML.
|
|
this.browser = createAndAppendElement(this.panel, "browser", {
|
|
src: "chrome://global/content/NetworkPanel.xhtml",
|
|
disablehistory: "true",
|
|
flex: "1"
|
|
});
|
|
|
|
// 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.browser = null;
|
|
self.document = null;
|
|
self.httpActivity = null;
|
|
}, false);
|
|
|
|
// Set the document object and update the content once the panel is loaded.
|
|
let self = this;
|
|
this.panel.addEventListener("load", function onLoad() {
|
|
self.panel.removeEventListener("load", onLoad, true)
|
|
self.document = self.browser.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 HUDService.getFormatStr("NetworkPanel." + aName, aArray);
|
|
},
|
|
|
|
/**
|
|
*
|
|
* @returns boolean
|
|
* True if the response is an image, false otherwise.
|
|
*/
|
|
get _responseIsImage()
|
|
{
|
|
let response = this.httpActivity.response;
|
|
if (!response || !response.header || !response.header["Content-Type"]) {
|
|
let request = this.httpActivity.request;
|
|
if (request.header["Accept"] &&
|
|
request.header["Accept"].indexOf("image/") != -1) {
|
|
return true;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
return response.header["Content-Type"].indexOf("image/") != -1;
|
|
},
|
|
|
|
/**
|
|
*
|
|
* @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:
|
|
*
|
|
* <span class="property-name">${line}:</span>
|
|
* <span class="property-value">${aList[line]}</span><br>
|
|
*
|
|
* and adds it to parent.
|
|
*/
|
|
let textNode = doc.createTextNode(key + ":");
|
|
let span = doc.createElement("span");
|
|
span.setAttribute("class", "property-name");
|
|
span.appendChild(textNode);
|
|
parent.appendChild(span);
|
|
|
|
textNode = doc.createTextNode(sortedList[key]);
|
|
span = doc.createElement("span");
|
|
span.setAttribute("class", "property-value");
|
|
span.appendChild(textNode);
|
|
parent.appendChild(span);
|
|
|
|
parent.appendChild(doc.createElement("br"));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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",
|
|
ConsoleUtils.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 `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 browser.
|
|
*
|
|
* @returns void
|
|
*/
|
|
update: function NP_update()
|
|
{
|
|
/**
|
|
* After the browser 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.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 (this._responseIsImage) {
|
|
this._displayResponseImage();
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
this.storage = new ConsoleStorage();
|
|
this.defaultFilterPrefs = this.storage.defaultDisplayPrefs;
|
|
this.defaultGlobalConsolePrefs = this.storage.defaultGlobalConsolePrefs;
|
|
|
|
// load stylesheet with StyleSheetService
|
|
var uri = Services.io.newURI(HUD_STYLESHEET_URI, null, null);
|
|
sss.loadAndRegisterSheet(uri, sss.AGENT_SHEET);
|
|
|
|
// begin observing HTTP traffic
|
|
this.startHTTPObservation();
|
|
};
|
|
|
|
HUD_SERVICE.prototype =
|
|
{
|
|
/**
|
|
* L10N shortcut function
|
|
*
|
|
* @param string aName
|
|
* @returns string
|
|
*/
|
|
getStr: function HS_getStr(aName)
|
|
{
|
|
return stringBundle.GetStringFromName(aName);
|
|
},
|
|
|
|
/**
|
|
* L10N shortcut function
|
|
*
|
|
* @param string aName
|
|
* @returns (format) string
|
|
*/
|
|
getFormatStr: function HS_getFormatStr(aName, aArray)
|
|
{
|
|
return stringBundle.formatStringFromName(aName, aArray, aArray.length);
|
|
},
|
|
|
|
/**
|
|
* getter for UI commands to be used by the frontend
|
|
*
|
|
* @returns object
|
|
*/
|
|
get consoleUI() {
|
|
return HeadsUpDisplayUICommands;
|
|
},
|
|
|
|
/**
|
|
* Collection of HUDIds that map to the tabs/windows/contexts
|
|
* that a HeadsUpDisplay can be activated for.
|
|
*/
|
|
activatedContexts: [],
|
|
|
|
/**
|
|
* Registry of HeadsUpDisplay DOM node ids
|
|
*/
|
|
_headsUpDisplays: {},
|
|
|
|
/**
|
|
* Mapping of HUDIds to URIspecs
|
|
*/
|
|
displayRegistry: {},
|
|
|
|
/**
|
|
* Mapping of HUDIds to contentWindows.
|
|
*/
|
|
windowRegistry: {},
|
|
|
|
/**
|
|
* Mapping of URISpecs to HUDIds
|
|
*/
|
|
uriRegistry: {},
|
|
|
|
/**
|
|
* The sequencer is a generator (after initialization) that returns unique
|
|
* integers
|
|
*/
|
|
sequencer: null,
|
|
|
|
/**
|
|
* Each HeadsUpDisplay has a set of filter preferences
|
|
*/
|
|
filterPrefs: {},
|
|
|
|
/**
|
|
* Event handler to get window errors
|
|
* TODO: a bit of a hack but is able to associate
|
|
* errors thrown in a window's scope we do not know
|
|
* about because of the nsIConsoleMessages not having a
|
|
* window reference.
|
|
* see bug 567165
|
|
*
|
|
* @param nsIDOMWindow aWindow
|
|
* @returns boolean
|
|
*/
|
|
setOnErrorHandler: function HS_setOnErrorHandler(aWindow) {
|
|
var self = this;
|
|
var window = aWindow.wrappedJSObject;
|
|
var console = window.console;
|
|
var origOnerrorFunc = window.onerror;
|
|
window.onerror = function windowOnError(aErrorMsg, aURL, aLineNumber)
|
|
{
|
|
if (aURL && !(aURL in self.uriRegistry)) {
|
|
var lineNum = "";
|
|
if (aLineNumber) {
|
|
lineNum = self.getFormatStr("errLine", [aLineNumber]);
|
|
}
|
|
console.error(aErrorMsg + " @ " + aURL + " " + lineNum);
|
|
}
|
|
|
|
if (origOnerrorFunc) {
|
|
origOnerrorFunc(aErrorMsg, aURL, aLineNumber);
|
|
}
|
|
|
|
return 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 current window
|
|
*
|
|
* @param nsIDOMWindow aContext
|
|
* @returns void
|
|
*/
|
|
activateHUDForContext: function HS_activateHUDForContext(aContext)
|
|
{
|
|
var window = aContext.linkedBrowser.contentWindow;
|
|
var id = aContext.linkedBrowser.parentNode.getAttribute("id");
|
|
this.registerActiveContext(id);
|
|
HUDService.windowInitializer(window);
|
|
},
|
|
|
|
/**
|
|
* Deactivate a HeadsUpDisplay for the current window
|
|
*
|
|
* @param nsIDOMWindow aContext
|
|
* @returns void
|
|
*/
|
|
deactivateHUDForContext: function HS_deactivateHUDForContext(aContext)
|
|
{
|
|
var gBrowser = HUDService.currentContext().gBrowser;
|
|
var window = aContext.linkedBrowser.contentWindow;
|
|
var browser = gBrowser.getBrowserForDocument(window.top.document);
|
|
var tabId = gBrowser.getNotificationBox(browser).getAttribute("id");
|
|
var hudId = "hud_" + tabId;
|
|
var displayNode = this.getHeadsUpDisplay(hudId);
|
|
|
|
this.unregisterActiveContext(hudId);
|
|
this.unregisterDisplay(hudId);
|
|
window.wrappedJSObject.console = null;
|
|
|
|
},
|
|
|
|
/**
|
|
* Clear the specified HeadsUpDisplay
|
|
*
|
|
* @param string aId
|
|
* @returns void
|
|
*/
|
|
clearDisplay: function HS_clearDisplay(aId)
|
|
{
|
|
var displayNode = this.getOutputNodeById(aId);
|
|
var outputNode = displayNode.querySelectorAll(".hud-output-node")[0];
|
|
|
|
while (outputNode.firstChild) {
|
|
outputNode.removeChild(outputNode.firstChild);
|
|
}
|
|
|
|
outputNode.lastTimestamp = 0;
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
},
|
|
|
|
/**
|
|
* Temporarily lifts the subtree rooted at the given node out of the DOM for
|
|
* the duration of the supplied callback. This allows DOM mutations performed
|
|
* inside the callback to avoid triggering reflows.
|
|
*
|
|
* @param nsIDOMNode aNode
|
|
* The node to remove from the tree.
|
|
* @param function aCallback
|
|
* The callback, which should take no parameters. The return value of
|
|
* the callback, if any, is ignored.
|
|
* @returns void
|
|
*/
|
|
liftNode: function(aNode, aCallback) {
|
|
let parentNode = aNode.parentNode;
|
|
let siblingNode = aNode.nextSibling;
|
|
parentNode.removeChild(aNode);
|
|
aCallback();
|
|
parentNode.insertBefore(aNode, siblingNode);
|
|
},
|
|
|
|
/**
|
|
* Turns the display of log nodes on and off appropriately to reflect the
|
|
* adjustment of the message type filter named by @aMessageType.
|
|
*
|
|
* @param string aHUDId
|
|
* The ID of the HUD to alter.
|
|
* @param string aMessageType
|
|
* The message type being filtered ("network", "css", etc.)
|
|
* @param boolean aState
|
|
* True if the filter named by @aMessageType is being turned on; false
|
|
* otherwise.
|
|
* @returns void
|
|
*/
|
|
adjustVisibilityForMessageType:
|
|
function HS_adjustVisibilityForMessageType(aHUDId, aMessageType, aState)
|
|
{
|
|
let displayNode = this.getOutputNodeById(aHUDId);
|
|
let outputNode = displayNode.querySelector(".hud-output-node");
|
|
let doc = outputNode.ownerDocument;
|
|
|
|
this.liftNode(outputNode, function() {
|
|
let xpath = ".//*[contains(@class, 'hud-msg-node') and " +
|
|
"contains(@class, 'hud-" + aMessageType + "')]";
|
|
let result = doc.evaluate(xpath, outputNode, null,
|
|
Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
for (let i = 0; i < result.snapshotLength; i++) {
|
|
if (aState) {
|
|
result.snapshotItem(i).classList.remove("hud-filtered-by-type");
|
|
} else {
|
|
result.snapshotItem(i).classList.add("hud-filtered-by-type");
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Returns the source code of the XPath contains() function necessary to
|
|
* match the given query string.
|
|
*
|
|
* @param string The query string to convert.
|
|
* @returns string
|
|
*/
|
|
buildXPathFunctionForString: function HS_buildXPathFunctionForString(aStr)
|
|
{
|
|
let words = aStr.split(/\s+/), results = [];
|
|
for (let i = 0; i < words.length; i++) {
|
|
let word = words[i];
|
|
if (word === "") {
|
|
continue;
|
|
}
|
|
|
|
let result;
|
|
if (word.indexOf('"') === -1) {
|
|
result = '"' + word + '"';
|
|
}
|
|
else if (word.indexOf("'") === -1) {
|
|
result = "'" + word + "'";
|
|
}
|
|
else {
|
|
result = 'concat("' + word.replace(/"/g, "\", '\"', \"") + '")';
|
|
}
|
|
|
|
results.push("contains(., " + result + ")");
|
|
}
|
|
|
|
return (results.length === 0) ? "true()" : results.join(" and ");
|
|
},
|
|
|
|
/**
|
|
* 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 fn = this.buildXPathFunctionForString(aSearchString);
|
|
let displayNode = this.getOutputNodeById(aHUDId);
|
|
let outputNode = displayNode.querySelector(".hud-output-node");
|
|
let doc = outputNode.ownerDocument;
|
|
this.liftNode(outputNode, function() {
|
|
let xpath = './/*[contains(@class, "hud-msg-node") and ' +
|
|
'not(contains(@class, "hud-filtered-by-string")) and not(' + fn + ')]';
|
|
let result = doc.evaluate(xpath, outputNode, null,
|
|
Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
for (let i = 0; i < result.snapshotLength; i++) {
|
|
result.snapshotItem(i).classList.add("hud-filtered-by-string");
|
|
}
|
|
|
|
xpath = './/*[contains(@class, "hud-msg-node") and contains(@class, ' +
|
|
'"hud-filtered-by-string") and ' + fn + ']';
|
|
result = doc.evaluate(xpath, outputNode, null,
|
|
Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
for (let i = 0; i < result.snapshotLength; i++) {
|
|
result.snapshotItem(i).classList.remove("hud-filtered-by-string");
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Makes a newly-inserted node invisible if the user has filtered it out.
|
|
*
|
|
* @param string aHUDId
|
|
* The ID of the HUD to alter.
|
|
* @param nsIDOMNode aNewNode
|
|
* The newly-inserted console message.
|
|
* @returns void
|
|
*/
|
|
adjustVisibilityForNewlyInsertedNode:
|
|
function HS_adjustVisibilityForNewlyInsertedNode(aHUDId, aNewNode) {
|
|
// Filter on the search string.
|
|
let searchString = this.getFilterStringByHUDId(aHUDId);
|
|
let xpath = ".[" + this.buildXPathFunctionForString(searchString) + "]";
|
|
let doc = aNewNode.ownerDocument;
|
|
let result = doc.evaluate(xpath, aNewNode, null,
|
|
Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
if (result.snapshotLength === 0) {
|
|
// The string filter didn't match, so the node is filtered.
|
|
aNewNode.classList.add("hud-filtered-by-string");
|
|
}
|
|
|
|
// Filter by the message type.
|
|
let classes = aNewNode.classList;
|
|
let msgType = null;
|
|
for (let i = 0; i < classes.length; i++) {
|
|
let klass = classes.item(i);
|
|
if (klass !== "hud-msg-node" && klass.indexOf("hud-") === 0) {
|
|
msgType = klass.substring(4); // Strip off "hud-".
|
|
break;
|
|
}
|
|
}
|
|
if (msgType !== null && !this.getFilterState(aHUDId, msgType)) {
|
|
// The node is filtered by type.
|
|
aNewNode.classList.add("hud-filtered-by-type");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Keeps a weak reference for each HeadsUpDisplay that is created
|
|
*
|
|
*/
|
|
hudWeakReferences: {},
|
|
|
|
/**
|
|
* Register a weak reference of each HeadsUpDisplay that is created
|
|
*
|
|
* @param object aHUDRef
|
|
* @param string aHUDId
|
|
* @returns void
|
|
*/
|
|
registerHUDWeakReference:
|
|
function HS_registerHUDWeakReference(aHUDRef, aHUDId)
|
|
{
|
|
this.hudWeakReferences[aHUDId] = aHUDRef;
|
|
},
|
|
|
|
/**
|
|
* Deletes a HeadsUpDisplay object from memory
|
|
*
|
|
* @param string aHUDId
|
|
* @returns void
|
|
*/
|
|
deleteHeadsUpDisplay: function HS_deleteHeadsUpDisplay(aHUDId)
|
|
{
|
|
delete this.hudWeakReferences[aHUDId].get();
|
|
},
|
|
|
|
/**
|
|
* Register a new Heads Up Display
|
|
*
|
|
* @param string aHUDId
|
|
* @param nsIDOMWindow aContentWindow
|
|
* @returns void
|
|
*/
|
|
registerDisplay: function HS_registerDisplay(aHUDId, aContentWindow)
|
|
{
|
|
// register a display DOM node Id and HUD uriSpec with the service
|
|
|
|
if (!aHUDId || !aContentWindow){
|
|
throw new Error(ERRORS.MISSING_ARGS);
|
|
}
|
|
var URISpec = aContentWindow.document.location.href
|
|
this.filterPrefs[aHUDId] = this.defaultFilterPrefs;
|
|
this.displayRegistry[aHUDId] = URISpec;
|
|
this._headsUpDisplays[aHUDId] = { id: aHUDId, };
|
|
this.registerActiveContext(aHUDId);
|
|
// init storage objects:
|
|
this.storage.createDisplay(aHUDId);
|
|
|
|
var huds = this.uriRegistry[URISpec];
|
|
var foundHUDId = false;
|
|
|
|
if (huds) {
|
|
var len = huds.length;
|
|
for (var i = 0; i < len; i++) {
|
|
if (huds[i] == aHUDId) {
|
|
foundHUDId = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!foundHUDId) {
|
|
this.uriRegistry[URISpec].push(aHUDId);
|
|
}
|
|
}
|
|
else {
|
|
this.uriRegistry[URISpec] = [aHUDId];
|
|
}
|
|
|
|
var windows = this.windowRegistry[aHUDId];
|
|
if (!windows) {
|
|
this.windowRegistry[aHUDId] = [aContentWindow];
|
|
}
|
|
else {
|
|
windows.push(aContentWindow);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* When a display is being destroyed, unregister it first
|
|
*
|
|
* @param string aId
|
|
* @returns void
|
|
*/
|
|
unregisterDisplay: function HS_unregisterDisplay(aId)
|
|
{
|
|
// 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.
|
|
HUDService.clearDisplay(aId);
|
|
|
|
// remove HUD DOM node and
|
|
// remove display references from local registries get the outputNode
|
|
var outputNode = this.mixins.getOutputNodeById(aId);
|
|
var parent = outputNode.parentNode;
|
|
var splitters = parent.querySelectorAll("splitter");
|
|
var len = splitters.length;
|
|
for (var i = 0; i < len; i++) {
|
|
if (splitters[i].getAttribute("class") == "hud-splitter") {
|
|
splitters[i].parentNode.removeChild(splitters[i]);
|
|
break;
|
|
}
|
|
}
|
|
// remove the DOM Nodes
|
|
parent.removeChild(outputNode);
|
|
// remove our record of the DOM Nodes from the registry
|
|
delete this._headsUpDisplays[aId];
|
|
// remove the HeadsUpDisplay object from memory
|
|
this.deleteHeadsUpDisplay(aId);
|
|
// remove the related storage object
|
|
this.storage.removeDisplay(aId);
|
|
// remove the related window objects
|
|
delete this.windowRegistry[aId];
|
|
|
|
let displays = this.displays();
|
|
|
|
var uri = this.displayRegistry[aId];
|
|
var specHudArr = this.uriRegistry[uri];
|
|
|
|
for (var i = 0; i < specHudArr.length; i++) {
|
|
if (specHudArr[i] == aId) {
|
|
specHudArr.splice(i, 1);
|
|
}
|
|
}
|
|
delete displays[aId];
|
|
delete this.displayRegistry[aId];
|
|
},
|
|
|
|
/**
|
|
* Shutdown all HeadsUpDisplays on xpcom-shutdown
|
|
*
|
|
* @returns void
|
|
*/
|
|
shutdown: function HS_shutdown()
|
|
{
|
|
for (var displayId in this._headsUpDisplays) {
|
|
this.unregisterDisplay(displayId);
|
|
}
|
|
// delete the storage as it holds onto channels
|
|
delete this.storage;
|
|
|
|
var xulWindow = aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIWebNavigation)
|
|
.QueryInterface(Ci.nsIDocShellTreeItem)
|
|
.rootTreeItem
|
|
.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIDOMWindow);
|
|
|
|
xulWindow = XPCNativeWrapper.unwrap(xulWindow);
|
|
var gBrowser = xulWindow.gBrowser;
|
|
gBrowser.tabContainer.removeEventListener("TabClose", this.onTabClose, false);
|
|
},
|
|
|
|
/**
|
|
* get the nsIDOMNode outputNode via a nsIURI.spec
|
|
*
|
|
* @param string aURISpec
|
|
* @returns nsIDOMNode
|
|
*/
|
|
getDisplayByURISpec: function HS_getDisplayByURISpec(aURISpec)
|
|
{
|
|
// TODO: what about data:uris? see bug 568626
|
|
var hudIds = this.uriRegistry[aURISpec];
|
|
if (hudIds.length == 1) {
|
|
// only one HUD connected to this URISpec
|
|
return this.getHeadsUpDisplay(hudIds[0]);
|
|
}
|
|
else {
|
|
// TODO: how to determine more fully the origination of this activity?
|
|
// see bug 567165
|
|
return this.getHeadsUpDisplay(hudIds[0]);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
for (let hudId in this.windowRegistry) {
|
|
if (this.windowRegistry[hudId] &&
|
|
this.windowRegistry[hudId].indexOf(aContentWindow) != -1) {
|
|
return hudId;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Gets HUD DOM Node
|
|
* @param string id
|
|
* The Heads Up Display DOM Id
|
|
* @returns nsIDOMNode
|
|
*/
|
|
getHeadsUpDisplay: function HS_getHeadsUpDisplay(aId)
|
|
{
|
|
return this.mixins.getOutputNodeById(aId);
|
|
},
|
|
|
|
/**
|
|
* gets the nsIDOMNode outputNode by ID via the gecko app mixins
|
|
*
|
|
* @param string aId
|
|
* @returns nsIDOMNode
|
|
*/
|
|
getOutputNodeById: function HS_getOutputNodeById(aId)
|
|
{
|
|
return this.mixins.getOutputNodeById(aId);
|
|
},
|
|
|
|
/**
|
|
* Gets an object that contains active DOM Node Ids for all Heads Up Displays
|
|
*
|
|
* @returns object
|
|
*/
|
|
displays: function HS_displays() {
|
|
return this._headsUpDisplays;
|
|
},
|
|
|
|
/**
|
|
* Get an array of HUDIds that match a uri.spec
|
|
*
|
|
* @param string aURISpec
|
|
* @returns array
|
|
*/
|
|
getHUDIdsForURISpec: function HS_getHUDIdsForURISpec(aURISpec)
|
|
{
|
|
if (this.uriRegistry[aURISpec]) {
|
|
return this.uriRegistry[aURISpec];
|
|
}
|
|
return [];
|
|
},
|
|
|
|
/**
|
|
* Gets an array that contains active DOM Node Ids for all HUDs
|
|
* @returns array
|
|
*/
|
|
displaysIndex: function HS_displaysIndex()
|
|
{
|
|
var props = [];
|
|
for (var prop in this._headsUpDisplays) {
|
|
props.push(prop);
|
|
}
|
|
return props;
|
|
},
|
|
|
|
/**
|
|
* get the current filter string for the HeadsUpDisplay
|
|
*
|
|
* @param string aHUDId
|
|
* @returns string
|
|
*/
|
|
getFilterStringByHUDId: function HS_getFilterStringbyHUDId(aHUDId) {
|
|
var hud = this.getHeadsUpDisplay(aHUDId);
|
|
var filterStr = hud.querySelectorAll(".hud-filter-box")[0].value;
|
|
return filterStr;
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
},
|
|
|
|
/**
|
|
* Logs a HUD-generated console message
|
|
* @param object aMessage
|
|
* The message to log, which is a JS object, this is the
|
|
* "raw" log message
|
|
* @param nsIDOMNode aConsoleNode
|
|
* The output DOM node to log the messageNode to
|
|
* @param nsIDOMNode aMessageNode
|
|
* The message DOM Node that will be appended to aConsoleNode
|
|
* @returns void
|
|
*/
|
|
logHUDMessage: function HS_logHUDMessage(aMessage,
|
|
aConsoleNode,
|
|
aMessageNode)
|
|
{
|
|
if (!aMessage) {
|
|
throw new Error(ERRORS.MISSING_ARGS);
|
|
}
|
|
|
|
let lastGroupNode = this.appendGroupIfNecessary(aConsoleNode,
|
|
aMessage.timestamp);
|
|
|
|
lastGroupNode.appendChild(aMessageNode);
|
|
ConsoleUtils.scrollToVisible(aMessageNode);
|
|
|
|
// store this message in the storage module:
|
|
this.storage.recordEntry(aMessage.hudId, aMessage);
|
|
},
|
|
|
|
/**
|
|
* logs a message to the Heads Up Display that originates
|
|
* in the nsIConsoleService
|
|
*
|
|
* @param nsIConsoleMessage aMessage
|
|
* @param nsIDOMNode aConsoleNode
|
|
* @param nsIDOMNode aMessageNode
|
|
* @returns void
|
|
*/
|
|
logConsoleMessage: function HS_logConsoleMessage(aMessage,
|
|
aConsoleNode,
|
|
aMessageNode)
|
|
{
|
|
aConsoleNode.appendChild(aMessageNode);
|
|
ConsoleUtils.scrollToVisible(aMessageNode);
|
|
|
|
// store this message in the storage module:
|
|
this.storage.recordEntry(aMessage.hudId, aMessage);
|
|
},
|
|
|
|
/**
|
|
* Logs a Message.
|
|
* @param aMessage
|
|
* The message to log, which is a JS object, this is the
|
|
* "raw" log message
|
|
* @param aConsoleNode
|
|
* The output DOM node to log the messageNode to
|
|
* @param The message DOM Node that will be appended to aConsoleNode
|
|
* @returns void
|
|
*/
|
|
logMessage: function HS_logMessage(aMessage, aConsoleNode, aMessageNode)
|
|
{
|
|
if (!aMessage) {
|
|
throw new Error(ERRORS.MISSING_ARGS);
|
|
}
|
|
|
|
var hud = this.getHeadsUpDisplay(aMessage.hudId);
|
|
switch (aMessage.origin) {
|
|
case "network":
|
|
case "HUDConsole":
|
|
case "console-listener":
|
|
this.logHUDMessage(aMessage, aConsoleNode, aMessageNode);
|
|
break;
|
|
default:
|
|
// noop
|
|
break;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* report consoleMessages recieved via the HUDConsoleObserver service
|
|
* @param nsIConsoleMessage aConsoleMessage
|
|
* @returns void
|
|
*/
|
|
reportConsoleServiceMessage:
|
|
function HS_reportConsoleServiceMessage(aConsoleMessage)
|
|
{
|
|
this.logActivity("console-listener", null, aConsoleMessage);
|
|
},
|
|
|
|
/**
|
|
* report scriptErrors recieved via the HUDConsoleObserver service
|
|
* @param nsIScriptError aScriptError
|
|
* @returns void
|
|
*/
|
|
reportConsoleServiceContentScriptError:
|
|
function HS_reportConsoleServiceContentScriptError(aScriptError)
|
|
{
|
|
try {
|
|
var uri = Services.io.newURI(aScriptError.sourceName, null, null);
|
|
}
|
|
catch(ex) {
|
|
var uri = { spec: "" };
|
|
}
|
|
this.logActivity("console-listener", uri, aScriptError);
|
|
},
|
|
|
|
/**
|
|
* generates an nsIScriptError
|
|
*
|
|
* @param object aMessage
|
|
* @param integer flag
|
|
* @returns nsIScriptError
|
|
*/
|
|
generateConsoleMessage:
|
|
function HS_generateConsoleMessage(aMessage, flag)
|
|
{
|
|
let message = scriptError; // nsIScriptError
|
|
message.init(aMessage.message, null, null, 0, 0, flag,
|
|
"HUDConsole");
|
|
return message;
|
|
},
|
|
|
|
/**
|
|
* 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,
|
|
|
|
getChromeWindowFromContentWindow:
|
|
function HS_getChromeWindowFromContentWindow(aContentWindow)
|
|
{
|
|
if (!aContentWindow) {
|
|
throw new Error("Cannot get contentWindow via nsILoadContext");
|
|
}
|
|
var win = aContentWindow.QueryInterface(Ci.nsIDOMWindow)
|
|
.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIWebNavigation)
|
|
.QueryInterface(Ci.nsIDocShellTreeItem)
|
|
.rootTreeItem
|
|
.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIDOMWindow)
|
|
.QueryInterface(Ci.nsIDOMChromeWindow);
|
|
return win;
|
|
},
|
|
|
|
/**
|
|
* Requests that haven't finished yet.
|
|
*/
|
|
openRequests: {},
|
|
|
|
/**
|
|
* 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 (aNode, aHttpActivity) {
|
|
let doc = aNode.ownerDocument;
|
|
let parent = doc.getElementById("mainPopupSet");
|
|
let netPanel = new NetworkPanel(parent, aHttpActivity);
|
|
|
|
let panel = netPanel.panel;
|
|
panel.openPopup(aNode, "after_pointer", 0, 0, false, false);
|
|
panel.sizeTo(350, 400);
|
|
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 (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);
|
|
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.logActivity("network", aChannel.URI, 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;
|
|
}
|
|
|
|
// Add listener for the response body.
|
|
let newListener = new ResponseListener(httpActivity);
|
|
aChannel.QueryInterface(Ci.nsITraceableChannel);
|
|
newListener.originalListener = aChannel.setNewListener(newListener);
|
|
httpActivity.response.listener = 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.
|
|
httpActivity.messageObject = loggedNode;
|
|
self.openRequests[httpActivity.id] = httpActivity;
|
|
|
|
// Make the network span clickable.
|
|
let linkNode = loggedNode.messageNode;
|
|
linkNode.setAttribute("aria-haspopup", "true");
|
|
linkNode.onclick = function() {
|
|
self.openNetworkPanel(linkNode, httpActivity);
|
|
}
|
|
}
|
|
else {
|
|
// Iterate over all currently ongoing requests. If aChannel can't
|
|
// be found within them, then exit this function.
|
|
let httpActivity = null;
|
|
for each (var item in self.openRequests) {
|
|
if (item.channel !== aChannel) {
|
|
continue;
|
|
}
|
|
httpActivity = item;
|
|
break;
|
|
}
|
|
|
|
if (!httpActivity) {
|
|
return;
|
|
}
|
|
|
|
let msgObject, updatePanel = false;
|
|
let data, textNode;
|
|
// Store the time information for this activity subtype.
|
|
httpActivity.timing[transCodes[aActivitySubtype]] = aTimestamp;
|
|
|
|
switch (aActivitySubtype) {
|
|
case activityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT:
|
|
let gBrowser = HUDService.currentContext().gBrowser;
|
|
|
|
let sentBody = NetworkHelper.readPostTextFromRequest(
|
|
aChannel, gBrowser);
|
|
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 == gBrowser.contentWindow.location.href) {
|
|
sentBody = NetworkHelper.readPostTextFromPage(gBrowser);
|
|
}
|
|
if (!sentBody) {
|
|
sentBody = "";
|
|
}
|
|
}
|
|
httpActivity.request.body = sentBody;
|
|
break;
|
|
|
|
case activityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER:
|
|
msgObject = httpActivity.messageObject;
|
|
|
|
// 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];
|
|
|
|
// Remove the textNode from the messageNode and add a new one
|
|
// that contains the respond http status.
|
|
textNode = msgObject.messageNode.firstChild;
|
|
textNode.parentNode.removeChild(textNode);
|
|
|
|
data = [ httpActivity.url,
|
|
httpActivity.response.status ];
|
|
|
|
msgObject.messageNode.appendChild(
|
|
msgObject.textFactory(
|
|
msgObject.prefix +
|
|
self.getFormatStr("networkUrlWithStatus", data)));
|
|
|
|
break;
|
|
|
|
case activityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE:
|
|
msgObject = httpActivity.messageObject;
|
|
|
|
|
|
let timing = httpActivity.timing;
|
|
let requestDuration =
|
|
Math.round((timing.RESPONSE_COMPLETE -
|
|
timing.REQUEST_HEADER) / 1000);
|
|
|
|
// Remove the textNode from the messageNode and add a new one
|
|
// that contains the request duration.
|
|
textNode = msgObject.messageNode.firstChild;
|
|
textNode.parentNode.removeChild(textNode);
|
|
|
|
data = [ httpActivity.url,
|
|
httpActivity.response.status,
|
|
requestDuration ];
|
|
|
|
msgObject.messageNode.appendChild(
|
|
msgObject.textFactory(
|
|
msgObject.prefix +
|
|
self.getFormatStr("networkUrlWithStatusAndDuration", data)));
|
|
|
|
delete self.openRequests[item.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",
|
|
0x804b0007: "STATUS_CONNECTING_TO",
|
|
0x804b0004: "STATUS_CONNECTED_TO",
|
|
0x804b0005: "STATUS_SENDING_TO",
|
|
0x804b000a: "STATUS_WAITING_FOR",
|
|
0x804b0006: "STATUS_RECEIVING_FROM"
|
|
}
|
|
};
|
|
|
|
activityDistributor.addObserver(httpObserver);
|
|
},
|
|
|
|
/**
|
|
* Logs network activity
|
|
*
|
|
* @param nsIURI aURI
|
|
* @param object aActivityObject
|
|
* @returns void
|
|
*/
|
|
logNetActivity: function HS_logNetActivity(aType, aURI, aActivityObject)
|
|
{
|
|
var outputNode, hudId;
|
|
try {
|
|
hudId = aActivityObject.hudId;
|
|
outputNode = this.getHeadsUpDisplay(hudId).
|
|
querySelector(".hud-output-node");
|
|
|
|
// get an id to attach to the dom node for lookup of node
|
|
// when updating the log entry with additional http transactions
|
|
var domId = "hud-log-node-" + this.sequenceId();
|
|
|
|
var message = { logLevel: aType,
|
|
activityObj: aActivityObject,
|
|
hudId: hudId,
|
|
origin: "network",
|
|
domId: domId,
|
|
};
|
|
var msgType = this.getStr("typeNetwork");
|
|
var msg = msgType + " " +
|
|
aActivityObject.method +
|
|
" " +
|
|
aActivityObject.url;
|
|
message.message = msg;
|
|
|
|
var messageObject =
|
|
this.messageFactory(message, aType, outputNode, aActivityObject);
|
|
|
|
var timestampedMessage = messageObject.timestampedMessage;
|
|
var urlIdx = timestampedMessage.indexOf(aActivityObject.url);
|
|
messageObject.prefix = timestampedMessage.substring(0, urlIdx);
|
|
|
|
this.logMessage(messageObject.messageObject, outputNode, messageObject.messageNode);
|
|
return messageObject;
|
|
}
|
|
catch (ex) {
|
|
Cu.reportError(ex);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Logs console listener activity
|
|
*
|
|
* @param nsIURI aURI
|
|
* @param object aActivityObject
|
|
* @returns void
|
|
*/
|
|
logConsoleActivity: function HS_logConsoleActivity(aURI, aActivityObject)
|
|
{
|
|
var displayNode, outputNode, hudId;
|
|
try {
|
|
var hudIds = this.uriRegistry[aURI.spec];
|
|
hudId = hudIds[0];
|
|
}
|
|
catch (ex) {
|
|
// TODO: uri spec is not tracked becasue the net request is
|
|
// using a different loadGroup
|
|
// see bug 568034
|
|
if (!displayNode) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
var _msgLogLevel = this.scriptMsgLogLevel[aActivityObject.flags];
|
|
var msgLogLevel = this.getStr(_msgLogLevel);
|
|
|
|
var logLevel = "warn";
|
|
|
|
if (aActivityObject.flags in this.scriptErrorFlags) {
|
|
logLevel = this.scriptErrorFlags[aActivityObject.flags];
|
|
}
|
|
|
|
// in this case, the "activity object" is the
|
|
// nsIScriptError or nsIConsoleMessage
|
|
var message = {
|
|
activity: aActivityObject,
|
|
origin: "console-listener",
|
|
hudId: hudId,
|
|
};
|
|
|
|
var lineColSubs = [aActivityObject.columnNumber,
|
|
aActivityObject.lineNumber];
|
|
var lineCol = this.getFormatStr("errLineCol", lineColSubs);
|
|
|
|
var errFileSubs = [aActivityObject.sourceName];
|
|
var errFile = this.getFormatStr("errFile", errFileSubs);
|
|
|
|
var msgCategory = this.getStr("msgCategory");
|
|
|
|
message.logLevel = logLevel;
|
|
message.level = logLevel;
|
|
|
|
message.message = msgLogLevel + " " +
|
|
aActivityObject.errorMessage + " " +
|
|
errFile + " " +
|
|
lineCol + " " +
|
|
msgCategory + " " + aActivityObject.category;
|
|
|
|
displayNode = this.getHeadsUpDisplay(hudId);
|
|
outputNode = displayNode.querySelectorAll(".hud-output-node")[0];
|
|
|
|
var messageObject =
|
|
this.messageFactory(message, message.level, outputNode, aActivityObject);
|
|
|
|
this.logMessage(messageObject.messageObject, outputNode, messageObject.messageNode);
|
|
},
|
|
|
|
/**
|
|
* Parse log messages for origin or listener type
|
|
* Get the correct outputNode if it exists
|
|
* Finally, call logMessage to write this message to
|
|
* storage and optionally, a DOM output node
|
|
*
|
|
* @param string aType
|
|
* @param nsIURI aURI
|
|
* @param object (or nsIScriptError) aActivityObj
|
|
* @returns void
|
|
*/
|
|
logActivity: function HS_logActivity(aType, aURI, aActivityObject)
|
|
{
|
|
var displayNode, outputNode, hudId;
|
|
|
|
if (aType == "network") {
|
|
return this.logNetActivity(aType, aURI, aActivityObject);
|
|
}
|
|
else if (aType == "console-listener") {
|
|
this.logConsoleActivity(aURI, aActivityObject);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Builds and appends a group to the console if enough time has passed since
|
|
* the last message.
|
|
*
|
|
* @param nsIDOMNode aConsoleNode
|
|
* The DOM node that holds the output of the console (NB: not the HUD
|
|
* node itself).
|
|
* @param number aTimestamp
|
|
* The timestamp of the newest message in milliseconds.
|
|
* @returns nsIDOMNode
|
|
* The group into which the next message should be written.
|
|
*/
|
|
appendGroupIfNecessary:
|
|
function HS_appendGroupIfNecessary(aConsoleNode, aTimestamp)
|
|
{
|
|
let hudBox = aConsoleNode;
|
|
while (hudBox != null && hudBox.getAttribute("class") !== "hud-box") {
|
|
hudBox = hudBox.parentNode;
|
|
}
|
|
|
|
let lastTimestamp = hudBox.lastTimestamp;
|
|
let delta = aTimestamp - lastTimestamp;
|
|
hudBox.lastTimestamp = aTimestamp;
|
|
if (delta < NEW_GROUP_DELAY) {
|
|
// No new group needed. Return the most recently-added group, if there is
|
|
// one.
|
|
let lastGroupNode = aConsoleNode.querySelector(".hud-group:last-child");
|
|
if (lastGroupNode != null) {
|
|
return lastGroupNode;
|
|
}
|
|
}
|
|
|
|
let chromeDocument = aConsoleNode.ownerDocument;
|
|
let groupNode = chromeDocument.createElement("vbox");
|
|
groupNode.setAttribute("class", "hud-group");
|
|
|
|
let separatorNode = chromeDocument.createElement("separator");
|
|
separatorNode.setAttribute("class", "groove hud-divider");
|
|
separatorNode.setAttribute("orient", "horizontal");
|
|
groupNode.appendChild(separatorNode);
|
|
|
|
aConsoleNode.appendChild(groupNode);
|
|
return groupNode;
|
|
},
|
|
|
|
/**
|
|
* gets the DOM Node that maps back to what context/tab that
|
|
* activity originated via the URI
|
|
*
|
|
* @param nsIURI aURI
|
|
* @returns nsIDOMNode
|
|
*/
|
|
getActivityOutputNode: function HS_getActivityOutputNode(aURI)
|
|
{
|
|
// determine which outputNode activity tied to aURI should be logged to.
|
|
var display = this.getDisplayByURISpec(aURI.spec);
|
|
if (display) {
|
|
return this.getOutputNodeById(display);
|
|
}
|
|
else {
|
|
throw new Error("Cannot get outputNode by hudId");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Wrapper method that generates a LogMessage object
|
|
*
|
|
* @param object aMessage
|
|
* @param string aLevel
|
|
* @param nsIDOMNode aOutputNode
|
|
* @param object aActivityObject
|
|
* @returns
|
|
*/
|
|
messageFactory:
|
|
function messageFactory(aMessage, aLevel, aOutputNode, aActivityObject)
|
|
{
|
|
// generate a LogMessage object
|
|
return new LogMessage(aMessage, aLevel, aOutputNode, aActivityObject);
|
|
},
|
|
|
|
/**
|
|
* Initialize the JSTerm object to create a JS Workspace
|
|
*
|
|
* @param nsIDOMWindow aContext
|
|
* @param nsIDOMNode aParentNode
|
|
* @returns void
|
|
*/
|
|
initializeJSTerm: function HS_initializeJSTerm(aContext, aParentNode)
|
|
{
|
|
// create Initial JS Workspace:
|
|
var context = Cu.getWeakReference(aContext);
|
|
var firefoxMixin = new JSTermFirefoxMixin(context, aParentNode);
|
|
var jsTerm = new JSTerm(context, aParentNode, firefoxMixin);
|
|
// TODO: injection of additional functionality needs re-thinking/api
|
|
// see bug 559748
|
|
},
|
|
|
|
/**
|
|
* Passed a HUDId, the corresponding window is returned
|
|
*
|
|
* @param string aHUDId
|
|
* @returns nsIDOMWindow
|
|
*/
|
|
getContentWindowFromHUDId: function HS_getContentWindowFromHUDId(aHUDId)
|
|
{
|
|
var hud = this.getHeadsUpDisplay(aHUDId);
|
|
var nodes = hud.parentNode.childNodes;
|
|
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
if (nodes[i].contentWindow) {
|
|
return nodes[i].contentWindow;
|
|
}
|
|
}
|
|
throw new Error("HS_getContentWindowFromHUD: Cannot get contentWindow");
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
},
|
|
|
|
scriptErrorFlags: {
|
|
0: "error",
|
|
1: "warn",
|
|
2: "exception",
|
|
4: "strict"
|
|
},
|
|
|
|
/**
|
|
* replacement strings (L10N)
|
|
*/
|
|
scriptMsgLogLevel: {
|
|
0: "typeError",
|
|
1: "typeWarning",
|
|
2: "typeException",
|
|
4: "typeStrict",
|
|
},
|
|
|
|
/**
|
|
* onTabClose event handler function
|
|
*
|
|
* @param aEvent
|
|
* @returns void
|
|
*/
|
|
onTabClose: function HS_onTabClose(aEvent)
|
|
{
|
|
var browser = aEvent.target;
|
|
var tabId = gBrowser.getNotificationBox(browser).getAttribute("id");
|
|
var hudId = "hud_" + tabId;
|
|
this.unregisterDisplay(hudId);
|
|
},
|
|
|
|
/**
|
|
* 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 = XPCNativeWrapper.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;
|
|
}
|
|
|
|
if (aContentWindow.document.location.href == "about:blank" &&
|
|
HUDWindowObserver.initialConsoleCreated == false) {
|
|
// TODO: need to make this work with about:blank in the future
|
|
// see bug 568661
|
|
return;
|
|
}
|
|
|
|
let gBrowser = xulWindow.gBrowser;
|
|
|
|
|
|
var container = gBrowser.tabContainer;
|
|
container.addEventListener("TabClose", this.onTabClose, false);
|
|
|
|
if (gBrowser && !HUDWindowObserver.initialConsoleCreated) {
|
|
HUDWindowObserver.initialConsoleCreated = true;
|
|
}
|
|
|
|
let _browser =
|
|
gBrowser.getBrowserForDocument(aContentWindow.document.wrappedJSObject);
|
|
let nBox = gBrowser.getNotificationBox(_browser);
|
|
let nBoxId = nBox.getAttribute("id");
|
|
let hudId = "hud_" + nBoxId;
|
|
|
|
if (!this.canActivateContext(hudId)) {
|
|
return;
|
|
}
|
|
|
|
this.registerDisplay(hudId, aContentWindow);
|
|
|
|
// check if aContentWindow has a console Object
|
|
let _console = aContentWindow.wrappedJSObject.console;
|
|
if (!_console) {
|
|
// no console exists. does the HUD exist?
|
|
let hudNode;
|
|
let childNodes = nBox.childNodes;
|
|
|
|
for (var i = 0; i < childNodes.length; i++) {
|
|
let id = childNodes[i].getAttribute("id");
|
|
if (id.split("_")[0] == "hud") {
|
|
hudNode = childNodes[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!hudNode) {
|
|
// get nBox object and call new HUD
|
|
let config = { parentNode: nBox,
|
|
contentWindow: aContentWindow
|
|
};
|
|
|
|
let _hud = new HeadsUpDisplay(config);
|
|
|
|
let hudWeakRef = Cu.getWeakReference(_hud);
|
|
HUDService.registerHUDWeakReference(hudWeakRef, hudId);
|
|
}
|
|
else {
|
|
// only need to attach a console object to the window object
|
|
let config = { hudNode: hudNode,
|
|
consoleOnly: true,
|
|
contentWindow: aContentWindow
|
|
};
|
|
|
|
let _hud = new HeadsUpDisplay(config);
|
|
|
|
let hudWeakRef = Cu.getWeakReference(_hud);
|
|
HUDService.registerHUDWeakReference(hudWeakRef, hudId);
|
|
|
|
aContentWindow.wrappedJSObject.console = _hud.console;
|
|
}
|
|
}
|
|
// capture JS Errors
|
|
this.setOnErrorHandler(aContentWindow);
|
|
}
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// 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,
|
|
// }
|
|
//
|
|
// or, just create a new console - as there is already a HUD in place
|
|
// config: { hudNode: existingHUDDOMNode,
|
|
// consoleOnly: true,
|
|
// contentWindow: aWindow
|
|
// }
|
|
|
|
if (aConfig.consoleOnly) {
|
|
this.HUDBox = aConfig.hudNode;
|
|
this.parentNode = aConfig.hudNode.parentNode;
|
|
this.notificationBox = this.parentNode;
|
|
this.contentWindow = aConfig.contentWindow;
|
|
this.uriSpec = aConfig.contentWindow.location.href;
|
|
this.reattachConsole();
|
|
this.HUDBox.querySelectorAll(".jsterm-input-node")[0].focus();
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// create textNode Factory:
|
|
this.textFactory = NodeFactory("text", "xul", this.chromeDocument);
|
|
|
|
this.chromeWindow = HUDService.getChromeWindowFromContentWindow(this.contentWindow);
|
|
|
|
// create a panel dynamically and attach to the parentNode
|
|
let hudBox = this.createHUD();
|
|
|
|
let splitter = this.chromeDocument.createElement("splitter");
|
|
splitter.setAttribute("class", "hud-splitter");
|
|
|
|
this.notificationBox.insertBefore(splitter,
|
|
this.notificationBox.childNodes[1]);
|
|
|
|
let console = this.createConsole();
|
|
|
|
this.HUDBox.lastTimestamp = 0;
|
|
|
|
this.contentWindow.wrappedJSObject.console = console;
|
|
|
|
// create the JSTerm input element
|
|
try {
|
|
this.createConsoleInput(this.contentWindow, this.consoleWrap, this.outputNode);
|
|
this.HUDBox.querySelectorAll(".jsterm-input-node")[0].focus();
|
|
}
|
|
catch (ex) {
|
|
Cu.reportError(ex);
|
|
}
|
|
}
|
|
|
|
HeadsUpDisplay.prototype = {
|
|
/**
|
|
* L10N shortcut function
|
|
*
|
|
* @param string aName
|
|
* @returns string
|
|
*/
|
|
getStr: function HUD_getStr(aName)
|
|
{
|
|
return stringBundle.GetStringFromName(aName);
|
|
},
|
|
|
|
/**
|
|
* L10N shortcut function
|
|
*
|
|
* @param string aName
|
|
* @param array aArray
|
|
* @returns string
|
|
*/
|
|
getFormatStr: function HUD_getFormatStr(aName, aArray)
|
|
{
|
|
return stringBundle.formatStringFromName(aName, aArray, aArray.length);
|
|
},
|
|
|
|
/**
|
|
* The JSTerm object that contains the console's inputNode
|
|
*
|
|
*/
|
|
jsterm: null,
|
|
|
|
/**
|
|
* creates and attaches the console input node
|
|
*
|
|
* @param nsIDOMWindow aWindow
|
|
* @returns void
|
|
*/
|
|
createConsoleInput:
|
|
function HUD_createConsoleInput(aWindow, aParentNode, aExistingConsole)
|
|
{
|
|
var context = Cu.getWeakReference(aWindow);
|
|
|
|
if (appName() == "FIREFOX") {
|
|
let outputCSSClassOverride = "hud-msg-node";
|
|
let mixin = new JSTermFirefoxMixin(context, aParentNode, aExistingConsole, outputCSSClassOverride);
|
|
this.jsterm = new JSTerm(context, aParentNode, mixin);
|
|
}
|
|
else {
|
|
throw new Error("Unsupported Gecko Application");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Re-attaches a console when the contentWindow is recreated
|
|
*
|
|
* @returns void
|
|
*/
|
|
reattachConsole: function HUD_reattachConsole()
|
|
{
|
|
this.hudId = this.HUDBox.getAttribute("id");
|
|
|
|
this.outputNode = this.HUDBox.querySelectorAll(".hud-output-node")[0];
|
|
|
|
this.chromeWindow = HUDService.
|
|
getChromeWindowFromContentWindow(this.contentWindow);
|
|
this.chromeDocument = this.HUDBox.ownerDocument;
|
|
|
|
if (this.outputNode) {
|
|
// createConsole
|
|
this.createConsole();
|
|
}
|
|
else {
|
|
throw new Error("Cannot get output node");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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.HUDBox = this.makeXULNode("vbox");
|
|
this.HUDBox.setAttribute("id", this.hudId);
|
|
this.HUDBox.setAttribute("class", "hud-box");
|
|
|
|
var height = Math.ceil((this.contentWindow.innerHeight * .33)) + "px";
|
|
var style = "height: " + height + ";";
|
|
this.HUDBox.setAttribute("style", style);
|
|
|
|
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.outputNode = this.makeXULNode("scrollbox");
|
|
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.addEventListener("DOMNodeInserted", function(ev) {
|
|
// DOMNodeInserted is also called when the output node is being *itself*
|
|
// (re)inserted into the DOM (which happens during a search, for
|
|
// example). For this reason, we need to ensure that we only check
|
|
// message nodes.
|
|
let node = ev.target;
|
|
if (node.nodeType === node.ELEMENT_NODE &&
|
|
node.classList.contains("hud-msg-node")) {
|
|
HUDService.adjustVisibilityForNewlyInsertedNode(self.hudId, ev.target);
|
|
}
|
|
}, false);
|
|
|
|
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", this.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;
|
|
consoleWrap.appendChild(consoleFilterToolbar);
|
|
|
|
consoleWrap.appendChild(this.outputNode);
|
|
|
|
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()
|
|
{
|
|
let buttons = ["Network", "CSSParser", "Exception", "Error",
|
|
"Info", "Warn", "Log",];
|
|
|
|
const pageButtons = [
|
|
{ prefKey: "network", name: "PageNet" },
|
|
{ prefKey: "cssparser", name: "PageCSS" },
|
|
{ prefKey: "exception", name: "PageJS" }
|
|
];
|
|
const consoleButtons = [
|
|
{ prefKey: "error", name: "ConsoleErrors" },
|
|
{ prefKey: "warn", name: "ConsoleWarnings" },
|
|
{ prefKey: "info", name: "ConsoleInfo" },
|
|
{ prefKey: "log", name: "ConsoleLog" }
|
|
];
|
|
|
|
let toolbar = this.makeXULNode("toolbar");
|
|
toolbar.setAttribute("class", "hud-console-filter-toolbar");
|
|
toolbar.setAttribute("mode", "text");
|
|
|
|
let pageCategoryTitle = this.getStr("categoryPage");
|
|
this.addButtonCategory(toolbar, pageCategoryTitle, pageButtons);
|
|
|
|
let separator = this.makeXULNode("separator");
|
|
separator.setAttribute("orient", "vertical");
|
|
toolbar.appendChild(separator);
|
|
|
|
let consoleCategoryTitle = this.getStr("categoryConsole");
|
|
this.addButtonCategory(toolbar, consoleCategoryTitle, consoleButtons);
|
|
|
|
toolbar.appendChild(this.filterSpacer);
|
|
toolbar.appendChild(this.filterBox);
|
|
return toolbar;
|
|
},
|
|
|
|
/**
|
|
* Creates the context menu on the console, which contains the "clear
|
|
* console" functionality.
|
|
*
|
|
* @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);
|
|
|
|
let copyItem = this.makeXULNode("menuitem");
|
|
copyItem.setAttribute("label", this.getStr("copyCmd.label"));
|
|
copyItem.setAttribute("accesskey", this.getStr("copyCmd.accesskey"));
|
|
copyItem.setAttribute("key", "key_copy");
|
|
copyItem.setAttribute("command", "cmd_copy");
|
|
menuPopup.appendChild(copyItem);
|
|
|
|
menuPopup.appendChild(this.makeXULNode("menuseparator"));
|
|
|
|
let clearItem = this.makeXULNode("menuitem");
|
|
clearItem.setAttribute("label", this.getStr("itemClear"));
|
|
clearItem.setAttribute("hudId", this.hudId);
|
|
clearItem.setAttribute("buttonType", "clear");
|
|
clearItem.setAttribute("oncommand", "HUDConsoleUI.command(this);");
|
|
menuPopup.appendChild(clearItem);
|
|
|
|
aConsoleWrapper.appendChild(menuPopup);
|
|
aConsoleWrapper.setAttribute("context", id);
|
|
},
|
|
|
|
makeButton: function HUD_makeButton(aName, aPrefKey, aType)
|
|
{
|
|
var self = this;
|
|
let prefKey = aPrefKey;
|
|
|
|
let btn;
|
|
if (aType == "checkbox") {
|
|
btn = this.makeXULNode("checkbox");
|
|
btn.setAttribute("type", aType);
|
|
} else {
|
|
btn = this.makeXULNode("toolbarbutton");
|
|
}
|
|
|
|
btn.setAttribute("hudId", this.hudId);
|
|
btn.setAttribute("buttonType", prefKey);
|
|
btn.setAttribute("class", "hud-filter-btn");
|
|
let key = "btn" + aName;
|
|
btn.setAttribute("label", this.getStr(key));
|
|
key = "tip" + aName;
|
|
btn.setAttribute("tooltip", this.getStr(key));
|
|
|
|
if (aType == "checkbox") {
|
|
btn.setAttribute("checked", this.filterPrefs[prefKey]);
|
|
function toggle(btn) {
|
|
self.consoleFilterCommands.toggle(btn);
|
|
};
|
|
|
|
btn.setAttribute("oncommand", "HUDConsoleUI.toggleFilter(this);");
|
|
}
|
|
else {
|
|
var command = "HUDConsoleUI.command(this)";
|
|
btn.setAttribute("oncommand", command);
|
|
}
|
|
return btn;
|
|
},
|
|
|
|
/**
|
|
* Appends a category title and a series of buttons to the filter bar.
|
|
*
|
|
* @param nsIDOMNode aToolbar
|
|
* The DOM node to which to add the category.
|
|
* @param string aTitle
|
|
* The title for the category.
|
|
* @param Array aButtons
|
|
* The buttons, specified as objects with "name" and "prefKey"
|
|
* properties.
|
|
* @returns nsIDOMNode
|
|
*/
|
|
addButtonCategory: function(aToolbar, aTitle, aButtons) {
|
|
let lbl = this.makeXULNode("label");
|
|
lbl.setAttribute("class", "hud-filter-cat");
|
|
lbl.setAttribute("value", aTitle);
|
|
aToolbar.appendChild(lbl);
|
|
|
|
for (let i = 0; i < aButtons.length; i++) {
|
|
let btn = aButtons[i];
|
|
aToolbar.appendChild(this.makeButton(btn.name, btn.prefKey, "checkbox"));
|
|
}
|
|
},
|
|
|
|
createHUD: function HUD_createHUD()
|
|
{
|
|
let self = this;
|
|
if (this.HUDBox) {
|
|
return this.HUDBox;
|
|
}
|
|
else {
|
|
this.makeHUDNodes();
|
|
|
|
let nodes = this.notificationBox.insertBefore(this.HUDBox,
|
|
this.notificationBox.childNodes[0]);
|
|
|
|
return this.HUDBox;
|
|
}
|
|
},
|
|
|
|
get console() { return this._console || this.createConsole(); },
|
|
|
|
getLogCount: function HUD_getLogCount()
|
|
{
|
|
return this.outputNode.childNodes.length;
|
|
},
|
|
|
|
getLogNodes: function HUD_getLogNodes()
|
|
{
|
|
return this.outputNode.childNodes;
|
|
},
|
|
|
|
/**
|
|
* This console will accept a message, get the tab's meta-data and send
|
|
* properly-formatted message object to the service identifying
|
|
* where it came from, etc...
|
|
*
|
|
* @returns console
|
|
*/
|
|
createConsole: function HUD_createConsole()
|
|
{
|
|
return new HUDConsole(this);
|
|
},
|
|
|
|
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"
|
|
}
|
|
};
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
// HUDConsole factory function
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* The console object that is attached to each contentWindow
|
|
*
|
|
* @param object aHeadsUpDisplay
|
|
* @returns object
|
|
*/
|
|
function HUDConsole(aHeadsUpDisplay)
|
|
{
|
|
let hud = aHeadsUpDisplay;
|
|
let hudId = hud.hudId;
|
|
let outputNode = hud.outputNode;
|
|
let chromeDocument = hud.chromeDocument;
|
|
|
|
aHeadsUpDisplay._console = this;
|
|
|
|
let sendToHUDService = function console_send(aLevel, aArguments)
|
|
{
|
|
let ts = ConsoleUtils.timestamp();
|
|
let messageNode = hud.makeXULNode("label");
|
|
|
|
let klass = "hud-msg-node hud-" + aLevel;
|
|
|
|
messageNode.setAttribute("class", klass);
|
|
|
|
let argumentArray = [];
|
|
for (var i = 0; i < aArguments.length; i++) {
|
|
argumentArray.push(aArguments[i]);
|
|
}
|
|
|
|
let message = argumentArray.join(' ');
|
|
let timestampedMessage = ConsoleUtils.timestampString(ts) + ": " +
|
|
message;
|
|
|
|
messageNode.appendChild(chromeDocument.createTextNode(timestampedMessage));
|
|
|
|
// need a constructor here to properly set all attrs
|
|
let messageObject = {
|
|
logLevel: aLevel,
|
|
hudId: hud.hudId,
|
|
message: message,
|
|
timestamp: ts,
|
|
origin: "HUDConsole",
|
|
};
|
|
|
|
HUDService.logMessage(messageObject, hud.outputNode, messageNode);
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
// Console API.
|
|
this.log = function console_log()
|
|
{
|
|
sendToHUDService("log", arguments);
|
|
},
|
|
|
|
this.info = function console_info()
|
|
{
|
|
sendToHUDService("info", arguments);
|
|
},
|
|
|
|
this.warn = function console_warn()
|
|
{
|
|
sendToHUDService("warn", arguments);
|
|
},
|
|
|
|
this.error = function console_error()
|
|
{
|
|
sendToHUDService("error", arguments);
|
|
},
|
|
|
|
this.exception = function console_exception()
|
|
{
|
|
sendToHUDService("exception", arguments);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Creates a DOM Node factory for XUL nodes - as well as textNodes
|
|
* @param aFactoryType
|
|
* "xul" or "text"
|
|
* @returns DOM Node Factory function
|
|
*/
|
|
function NodeFactory(aFactoryType, aNameSpace, aDocument)
|
|
{
|
|
// aDocument is presumed to be a XULDocument
|
|
if (aFactoryType == "text") {
|
|
function factory(aText) {
|
|
return aDocument.createTextNode(aText);
|
|
}
|
|
return factory;
|
|
}
|
|
else {
|
|
if (aNameSpace == "xul") {
|
|
function factory(aTag)
|
|
{
|
|
return aDocument.createElement(aTag);
|
|
}
|
|
return factory;
|
|
}
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// 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 (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 = 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[properties.length - 1].trimLeft();
|
|
properties.pop();
|
|
for each (var prop in properties) {
|
|
prop = prop.trim();
|
|
|
|
// If obj is undefined or null, then there is no change 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 (obj.__lookupGetter__(prop)) {
|
|
return null;
|
|
}
|
|
obj = obj[prop];
|
|
}
|
|
}
|
|
else {
|
|
matchProp = properties[0].trimLeft();
|
|
}
|
|
|
|
// If obj is undefined or null, then there is no change to run
|
|
// completion on it. Exit here.
|
|
if (typeof obj === "undefined" || obj === null) {
|
|
return null;
|
|
}
|
|
|
|
let matches = [];
|
|
for (var prop in obj) {
|
|
matches.push(prop);
|
|
}
|
|
|
|
matches = matches.filter(function(item) {
|
|
return item.indexOf(matchProp) == 0;
|
|
}).sort();
|
|
|
|
return {
|
|
matchProp: matchProp,
|
|
matches: matches
|
|
};
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// JSTerm
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* 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
|
|
*
|
|
*
|
|
*
|
|
* @param object aContext
|
|
* Usually nsIDOMWindow, but doesn't have to be
|
|
* @param nsIDOMNode aParentNode
|
|
* @param object aMixin
|
|
* Gecko-app (or Jetpack) specific utility object
|
|
* @returns void
|
|
*/
|
|
function JSTerm(aContext, aParentNode, aMixin)
|
|
{
|
|
// set the context, attach the UI by appending to aParentNode
|
|
|
|
this.application = appName();
|
|
this.context = aContext;
|
|
this.parentNode = aParentNode;
|
|
this.mixins = aMixin;
|
|
|
|
this.xulElementFactory =
|
|
NodeFactory("xul", "xul", aParentNode.ownerDocument);
|
|
|
|
this.textFactory = NodeFactory("text", "xul", aParentNode.ownerDocument);
|
|
|
|
this.setTimeout = aParentNode.ownerDocument.defaultView.setTimeout;
|
|
|
|
this.historyIndex = 0;
|
|
this.historyPlaceHolder = 0; // this.history.length;
|
|
this.log = LogFactory("*** JSTerm:");
|
|
this.init();
|
|
}
|
|
|
|
JSTerm.prototype = {
|
|
|
|
propertyProvider: JSPropertyProvider,
|
|
|
|
COMPLETE_FORWARD: 0,
|
|
COMPLETE_BACKWARD: 1,
|
|
COMPLETE_HINT_ONLY: 2,
|
|
|
|
init: function JST_init()
|
|
{
|
|
this.createSandbox();
|
|
this.inputNode = this.mixins.inputNode;
|
|
let eventHandlerKeyDown = this.keyDown();
|
|
this.inputNode.addEventListener('keypress', eventHandlerKeyDown, false);
|
|
let eventHandlerInput = this.inputEventHandler();
|
|
this.inputNode.addEventListener('input', eventHandlerInput, false);
|
|
this.outputNode = this.mixins.outputNode;
|
|
if (this.mixins.cssClassOverride) {
|
|
this.cssClassOverride = this.mixins.cssClassOverride;
|
|
}
|
|
},
|
|
|
|
get codeInputString()
|
|
{
|
|
// TODO: filter the input for windows line breaks, conver to unix
|
|
// see bug 572812
|
|
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._window.wrappedJSObject.jsterm = {};
|
|
this.console = this._window.wrappedJSObject.console;
|
|
this.sandbox = new Cu.Sandbox(this._window);
|
|
this.sandbox.window = this._window;
|
|
this.sandbox.console = this.console;
|
|
this.sandbox.__proto__ = this._window.wrappedJSObject;
|
|
},
|
|
|
|
get _window()
|
|
{
|
|
return this.context.get().QueryInterface(Ci.nsIDOMWindowInternal);
|
|
},
|
|
/**
|
|
* Evaluates a string in the sandbox. The string is currently wrapped by a
|
|
* with(window) { aString } construct, see bug 574033.
|
|
*
|
|
* @param string aString
|
|
* String to evaluate in the sandbox.
|
|
* @returns something
|
|
* The result of the evaluation.
|
|
*/
|
|
evalInSandbox: function JST_evalInSandbox(aString)
|
|
{
|
|
let execStr = "with(window) {" + aString + "}";
|
|
return Cu.evalInSandbox(execStr, this.sandbox, "default", "HUD Console", 1);
|
|
},
|
|
|
|
|
|
execute: function JST_execute(aExecuteString)
|
|
{
|
|
// attempt to execute the content of the inputNode
|
|
aExecuteString = aExecuteString || this.inputNode.value;
|
|
if (!aExecuteString) {
|
|
this.console.log("no value to execute");
|
|
return;
|
|
}
|
|
|
|
this.writeOutput(aExecuteString, true);
|
|
|
|
try {
|
|
var result = this.evalInSandbox(aExecuteString);
|
|
|
|
if (result || result === false) {
|
|
this.writeOutputJS(aExecuteString, result);
|
|
}
|
|
else if (result === undefined) {
|
|
this.writeOutput("undefined", false);
|
|
}
|
|
else if (result === null) {
|
|
this.writeOutput("null", false);
|
|
}
|
|
}
|
|
catch (ex) {
|
|
this.console.error(ex);
|
|
}
|
|
|
|
this.history.push(aExecuteString);
|
|
this.historyIndex++;
|
|
this.historyPlaceHolder = this.history.length;
|
|
this.inputNode.value = "";
|
|
},
|
|
|
|
/**
|
|
* 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 two buttons:
|
|
// 1. `Update`: reexecutes the string executed on the command line. The
|
|
// result will be inspected by this panel.
|
|
// 2. `Close`: destroys the 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: HUDService.getStr("update.button"),
|
|
accesskey: HUDService.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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
buttons.push({
|
|
label: HUDService.getStr("close.button"),
|
|
accesskey: HUDService.getStr("close.accesskey"),
|
|
oncommand: function () {
|
|
propPanel.destroy();
|
|
}
|
|
});
|
|
|
|
let doc = self.parentNode.ownerDocument;
|
|
let parent = doc.getElementById("mainPopupSet");
|
|
let title = (aEvalString
|
|
? HUDService.getFormatStr("jsPropertyInspectTitle", [aEvalString])
|
|
: HUDService.getStr("jsPropertyTitle"));
|
|
propPanel = new PropertyPanel(parent, doc, title, aOutputObject, buttons);
|
|
|
|
let panel = propPanel.panel;
|
|
panel.openPopup(aAnchor, "after_pointer", 0, 0, false, false);
|
|
panel.sizeTo(200, 400);
|
|
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 aOutputObject
|
|
* Object to be written to the outputNode.
|
|
*/
|
|
writeOutputJS: function JST_writeOutputJS(aEvalString, aOutputObject)
|
|
{
|
|
let lastGroupNode = HUDService.appendGroupIfNecessary(this.outputNode,
|
|
Date.now());
|
|
|
|
var self = this;
|
|
var node = this.xulElementFactory("label");
|
|
node.setAttribute("class", "jsterm-output-line");
|
|
node.setAttribute("aria-haspopup", "true");
|
|
node.onclick = function() {
|
|
self.openPropertyPanel(aEvalString, aOutputObject, node);
|
|
}
|
|
|
|
// TODO: format the aOutputObject and don't just use the
|
|
// aOuputObject.toString() function: [object object] -> Object {prop, ...}
|
|
// See bug 586249.
|
|
let textNode = this.textFactory(aOutputObject);
|
|
node.appendChild(textNode);
|
|
|
|
lastGroupNode.appendChild(node);
|
|
ConsoleUtils.scrollToVisible(node);
|
|
},
|
|
|
|
/**
|
|
* Writes a message to the HUD that originates from the interactive
|
|
* JavaScript console.
|
|
*
|
|
* @param string aOutputMessage
|
|
* The message to display.
|
|
* @param boolean aIsInput
|
|
* True if the message is the user's input, false if the message is
|
|
* the result of the expression the user typed.
|
|
* @returns void
|
|
*/
|
|
writeOutput: function JST_writeOutput(aOutputMessage, aIsInput)
|
|
{
|
|
let lastGroupNode = HUDService.appendGroupIfNecessary(this.outputNode,
|
|
Date.now());
|
|
|
|
var node = this.xulElementFactory("label");
|
|
if (aIsInput) {
|
|
node.setAttribute("class", "jsterm-input-line");
|
|
aOutputMessage = "> " + aOutputMessage;
|
|
}
|
|
else {
|
|
node.setAttribute("class", "jsterm-output-line");
|
|
}
|
|
|
|
if (this.cssClassOverride) {
|
|
let classes = this.cssClassOverride.split(" ");
|
|
for (let i = 0; i < classes.length; i++) {
|
|
node.classList.add(classes[i]);
|
|
}
|
|
}
|
|
|
|
var textNode = this.textFactory(aOutputMessage);
|
|
node.appendChild(textNode);
|
|
|
|
lastGroupNode.appendChild(node);
|
|
ConsoleUtils.scrollToVisible(node);
|
|
},
|
|
|
|
clearOutput: function JST_clearOutput()
|
|
{
|
|
let outputNode = this.outputNode;
|
|
|
|
while (outputNode.firstChild) {
|
|
outputNode.removeChild(outputNode.firstChild);
|
|
}
|
|
|
|
outputNode.lastTimestamp = 0;
|
|
},
|
|
|
|
inputEventHandler: function JSTF_inputEventHandler()
|
|
{
|
|
var self = this;
|
|
function handleInputEvent(aEvent) {
|
|
self.inputNode.setAttribute("rows",
|
|
Math.min(8, self.inputNode.value.split("\n").length));
|
|
}
|
|
return handleInputEvent;
|
|
},
|
|
|
|
keyDown: function JSTF_keyDown(aEvent)
|
|
{
|
|
var self = this;
|
|
function handleKeyDown(aEvent) {
|
|
// ctrl-a
|
|
var setTimeout = aEvent.target.ownerDocument.defaultView.setTimeout;
|
|
var target = aEvent.target;
|
|
var tmp;
|
|
|
|
if (aEvent.ctrlKey) {
|
|
switch (aEvent.charCode) {
|
|
case 97:
|
|
// control-a
|
|
tmp = self.codeInputString;
|
|
setTimeout(function() {
|
|
self.inputNode.value = tmp;
|
|
self.inputNode.setSelectionRange(0, 0);
|
|
}, 0);
|
|
break;
|
|
case 101:
|
|
// control-e
|
|
tmp = self.codeInputString;
|
|
self.inputNode.value = "";
|
|
setTimeout(function(){
|
|
var endPos = tmp.length + 1;
|
|
self.inputNode.value = tmp;
|
|
}, 0);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
else if (aEvent.shiftKey && aEvent.keyCode == 13) {
|
|
// shift return
|
|
// TODO: expand the inputNode height by one line
|
|
return;
|
|
}
|
|
else {
|
|
switch(aEvent.keyCode) {
|
|
case 13:
|
|
// return
|
|
self.execute();
|
|
aEvent.preventDefault();
|
|
break;
|
|
case 38:
|
|
// up arrow: history previous
|
|
if (self.caretInFirstLine()){
|
|
self.historyPeruse(true);
|
|
if (aEvent.cancelable) {
|
|
let inputEnd = self.inputNode.value.length;
|
|
self.inputNode.setSelectionRange(inputEnd, inputEnd);
|
|
aEvent.preventDefault();
|
|
}
|
|
}
|
|
break;
|
|
case 40:
|
|
// down arrow: history next
|
|
if (self.caretInLastLine()){
|
|
self.historyPeruse(false);
|
|
if (aEvent.cancelable) {
|
|
let inputEnd = self.inputNode.value.length;
|
|
self.inputNode.setSelectionRange(inputEnd, inputEnd);
|
|
aEvent.preventDefault();
|
|
}
|
|
}
|
|
break;
|
|
case 9:
|
|
// tab key
|
|
// If there are more than one possible completion, pressing tab
|
|
// means taking the next completion, shift_tab means taking
|
|
// the previous completion.
|
|
if (aEvent.shiftKey) {
|
|
self.complete(self.COMPLETE_BACKWARD);
|
|
}
|
|
else {
|
|
self.complete(self.COMPLETE_FORWARD);
|
|
}
|
|
var bool = aEvent.cancelable;
|
|
if (bool) {
|
|
aEvent.preventDefault();
|
|
}
|
|
else {
|
|
// noop
|
|
}
|
|
aEvent.target.focus();
|
|
break;
|
|
case 8:
|
|
// backspace key
|
|
case 46:
|
|
// delete key
|
|
// necessary so that default is not reached.
|
|
break;
|
|
default:
|
|
// all not handled keys
|
|
// Store the current inputNode value. If the value is the same
|
|
// after keyDown event was handled (after 0ms) then the user
|
|
// moved the cursor. If the value changed, then call the complete
|
|
// function to show completion on new value.
|
|
var value = self.inputNode.value;
|
|
setTimeout(function() {
|
|
if (self.inputNode.value !== value) {
|
|
self.complete(self.COMPLETE_HINT_ONLY);
|
|
}
|
|
}, 0);
|
|
break;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
return handleKeyDown;
|
|
},
|
|
|
|
historyPeruse: function JST_historyPeruse(aFlag) {
|
|
if (!this.history.length) {
|
|
return;
|
|
}
|
|
|
|
// Up Arrow key
|
|
if (aFlag) {
|
|
if (this.historyPlaceHolder <= 0) {
|
|
return;
|
|
}
|
|
|
|
let inputVal = this.history[--this.historyPlaceHolder];
|
|
if (inputVal){
|
|
this.inputNode.value = inputVal;
|
|
}
|
|
}
|
|
// Down Arrow key
|
|
else {
|
|
if (this.historyPlaceHolder == this.history.length - 1) {
|
|
this.historyPlaceHolder ++;
|
|
this.inputNode.value = "";
|
|
return;
|
|
}
|
|
else if (this.historyPlaceHolder >= (this.history.length)) {
|
|
return;
|
|
}
|
|
else {
|
|
let inputVal = this.history[++this.historyPlaceHolder];
|
|
if (inputVal){
|
|
this.inputNode.value = inputVal;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
refocus: function JSTF_refocus()
|
|
{
|
|
this.inputNode.blur();
|
|
this.inputNode.focus();
|
|
},
|
|
|
|
caretInFirstLine: function JSTF_caretInFirstLine()
|
|
{
|
|
var firstLineBreak = this.codeInputString.indexOf("\n");
|
|
return ((firstLineBreak == -1) ||
|
|
(this.inputNode.selectionStart <= firstLineBreak));
|
|
},
|
|
|
|
caretInLastLine: function JSTF_caretInLastLine()
|
|
{
|
|
var lastLineBreak = this.codeInputString.lastIndexOf("\n");
|
|
return (this.inputNode.selectionEnd > lastLineBreak);
|
|
},
|
|
|
|
history: [],
|
|
|
|
// 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 void
|
|
*/
|
|
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) {
|
|
return;
|
|
}
|
|
let selStart = inputNode.selectionStart, selEnd = inputNode.selectionEnd;
|
|
|
|
// 'Normalize' the selection so that end is always after start.
|
|
if (selStart > selEnd) {
|
|
let newSelEnd = selStart;
|
|
selStart = selEnd;
|
|
selEnd = newSelEnd;
|
|
}
|
|
|
|
// Only complete if the selection is at the end of the input.
|
|
if (selEnd != inputValue.length) {
|
|
this.lastCompletion = null;
|
|
return;
|
|
}
|
|
|
|
// Remove the selected text from the inputValue.
|
|
inputValue = inputValue.substring(0, selStart);
|
|
|
|
let matches;
|
|
let matchIndexToUse;
|
|
let matchOffset;
|
|
let completionStr;
|
|
|
|
// If there is a saved completion from last time and the used value for
|
|
// completion stayed the same, then use the stored completion.
|
|
if (this.lastCompletion && inputValue == this.lastCompletion.value) {
|
|
matches = this.lastCompletion.matches;
|
|
matchOffset = this.lastCompletion.matchOffset;
|
|
if (type === this.COMPLETE_BACKWARD) {
|
|
this.lastCompletion.index --;
|
|
}
|
|
else if (type === this.COMPLETE_FORWARD) {
|
|
this.lastCompletion.index ++;
|
|
}
|
|
matchIndexToUse = this.lastCompletion.index;
|
|
}
|
|
else {
|
|
// Look up possible completion values.
|
|
let completion = this.propertyProvider(this.sandbox.window, inputValue);
|
|
if (!completion) {
|
|
return;
|
|
}
|
|
matches = completion.matches;
|
|
matchIndexToUse = 0;
|
|
matchOffset = completion.matchProp.length
|
|
// Store this match;
|
|
this.lastCompletion = {
|
|
index: 0,
|
|
value: inputValue,
|
|
matches: matches,
|
|
matchOffset: matchOffset
|
|
};
|
|
}
|
|
|
|
if (matches.length != 0) {
|
|
// Ensure that the matchIndexToUse is always a valid array index.
|
|
if (matchIndexToUse < 0) {
|
|
matchIndexToUse = matches.length + (matchIndexToUse % matches.length);
|
|
if (matchIndexToUse == matches.length) {
|
|
matchIndexToUse = 0;
|
|
}
|
|
}
|
|
else {
|
|
matchIndexToUse = matchIndexToUse % matches.length;
|
|
}
|
|
|
|
completionStr = matches[matchIndexToUse].substring(matchOffset);
|
|
this.inputNode.value = inputValue + completionStr;
|
|
|
|
selEnd = inputValue.length + completionStr.length;
|
|
|
|
// If there is more than one possible completion or the completed part
|
|
// should get displayed only without moving the cursor at the end of the
|
|
// completion.
|
|
if (matches.length > 1 || type === this.COMPLETE_HINT_ONLY) {
|
|
inputNode.setSelectionRange(selStart, selEnd);
|
|
}
|
|
else {
|
|
inputNode.setSelectionRange(selEnd, selEnd);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* JSTermFirefoxMixin
|
|
*
|
|
* JavaScript Terminal Firefox Mixin
|
|
*
|
|
*/
|
|
function
|
|
JSTermFirefoxMixin(aContext,
|
|
aParentNode,
|
|
aExistingConsole,
|
|
aCSSClassOverride)
|
|
{
|
|
// 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.cssClassOverride = aCSSClassOverride;
|
|
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()
|
|
{
|
|
let inputNode = this.xulElementFactory("textbox");
|
|
inputNode.setAttribute("class", "jsterm-input-node");
|
|
inputNode.setAttribute("multiline", "true");
|
|
inputNode.setAttribute("rows", "1");
|
|
|
|
if (this.existingConsoleNode == undefined) {
|
|
// create elements
|
|
let term = this.xulElementFactory("vbox");
|
|
term.setAttribute("class", "jsterm-wrapper-node");
|
|
term.setAttribute("flex", "1");
|
|
|
|
let outputNode = this.xulElementFactory("vbox");
|
|
outputNode.setAttribute("class", "jsterm-output-node");
|
|
|
|
// construction
|
|
term.appendChild(outputNode);
|
|
term.appendChild(inputNode);
|
|
|
|
this.outputNode = outputNode;
|
|
this.inputNode = inputNode;
|
|
this.term = term;
|
|
}
|
|
else {
|
|
this.inputNode = inputNode;
|
|
this.term = inputNode;
|
|
this.outputNode = this.existingConsoleNode;
|
|
}
|
|
},
|
|
|
|
get inputValue()
|
|
{
|
|
return this.inputNode.value;
|
|
},
|
|
|
|
attachUI: function JSTF_attachUI()
|
|
{
|
|
this.parentNode.appendChild(this.term);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* LogMessage represents a single message logged to the "outputNode" console
|
|
*/
|
|
function LogMessage(aMessage, aLevel, aOutputNode, aActivityObject)
|
|
{
|
|
if (!aOutputNode || !aOutputNode.ownerDocument) {
|
|
throw new Error("aOutputNode is required and should be type nsIDOMNode");
|
|
}
|
|
if (!aMessage.origin) {
|
|
throw new Error("Cannot create and log a message without an origin");
|
|
}
|
|
this.message = aMessage;
|
|
if (aMessage.domId) {
|
|
// domId is optional - we only need it if the logmessage is
|
|
// being asynchronously updated
|
|
this.domId = aMessage.domId;
|
|
}
|
|
this.activityObject = aActivityObject;
|
|
this.outputNode = aOutputNode;
|
|
this.level = aLevel;
|
|
this.origin = aMessage.origin;
|
|
|
|
this.xulElementFactory =
|
|
NodeFactory("xul", "xul", aOutputNode.ownerDocument);
|
|
|
|
this.textFactory = NodeFactory("text", "xul", aOutputNode.ownerDocument);
|
|
|
|
this.createLogNode();
|
|
}
|
|
|
|
LogMessage.prototype = {
|
|
|
|
/**
|
|
* create a console log div node
|
|
*
|
|
* @returns nsIDOMNode
|
|
*/
|
|
createLogNode: function LM_createLogNode()
|
|
{
|
|
this.messageNode = this.xulElementFactory("label");
|
|
|
|
var ts = ConsoleUtils.timestamp();
|
|
this.timestampedMessage = ConsoleUtils.timestampString(ts) + ": " +
|
|
this.message.message;
|
|
var messageTxtNode = this.textFactory(this.timestampedMessage);
|
|
|
|
this.messageNode.appendChild(messageTxtNode);
|
|
|
|
var klass = "hud-msg-node hud-" + this.level;
|
|
this.messageNode.setAttribute("class", klass);
|
|
|
|
var self = this;
|
|
|
|
var messageObject = {
|
|
logLevel: self.level,
|
|
message: self.message,
|
|
timestamp: ts,
|
|
activity: self.activityObject,
|
|
origin: self.origin,
|
|
hudId: self.message.hudId,
|
|
};
|
|
|
|
this.messageObject = messageObject;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* 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 = {
|
|
|
|
/**
|
|
* Firefox-specific method for getting an array of chrome Window objects
|
|
*/
|
|
get chromeWindows()
|
|
{
|
|
var windows = [];
|
|
var enumerator = Services.ww.getWindowEnumerator(null);
|
|
while (enumerator.hasMoreElements()) {
|
|
windows.push(enumerator.getNext());
|
|
}
|
|
return windows;
|
|
},
|
|
|
|
/**
|
|
* Firefox-specific method for getting the DOM node (per tab) that message
|
|
* nodes are appended to.
|
|
* @param aId
|
|
* The DOM node's id.
|
|
*/
|
|
getOutputNodeById: function FAH_getOutputNodeById(aId)
|
|
{
|
|
if (!aId) {
|
|
throw new Error("FAH_getOutputNodeById: id is null!!");
|
|
}
|
|
var enumerator = Services.ww.getWindowEnumerator(null);
|
|
while (enumerator.hasMoreElements()) {
|
|
let window = enumerator.getNext();
|
|
let node = window.document.getElementById(aId);
|
|
if (node) {
|
|
return node;
|
|
}
|
|
}
|
|
throw new Error("Cannot get outputNode by id");
|
|
},
|
|
|
|
/**
|
|
* 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 = {
|
|
|
|
/**
|
|
* Generates a millisecond resolution timestamp.
|
|
*
|
|
* @returns integer
|
|
*/
|
|
timestamp: function ConsoleUtils_timestamp()
|
|
{
|
|
return Date.now();
|
|
},
|
|
|
|
/**
|
|
* Generates a formatted timestamp string for displaying in console messages.
|
|
*
|
|
* @param integer [ms] Optional, allows you to specify the timestamp in
|
|
* milliseconds since the UNIX epoch.
|
|
* @returns string The timestamp formatted for display.
|
|
*/
|
|
timestampString: function ConsoleUtils_timestampString(ms)
|
|
{
|
|
// TODO: L10N see bug 568656
|
|
var d = new Date(ms ? ms : null);
|
|
|
|
function pad(n, mil)
|
|
{
|
|
if (mil) {
|
|
return n < 100 ? "0" + n : n;
|
|
}
|
|
else {
|
|
return n < 10 ? "0" + n : n;
|
|
}
|
|
}
|
|
|
|
return pad(d.getHours()) + ":"
|
|
+ pad(d.getMinutes()) + ":"
|
|
+ pad(d.getSeconds()) + ":"
|
|
+ pad(d.getMilliseconds(), 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) {
|
|
let scrollBoxNode = aNode.parentNode;
|
|
while (scrollBoxNode.tagName !== "scrollbox") {
|
|
scrollBoxNode = scrollBoxNode.parentNode;
|
|
}
|
|
|
|
let boxObject = scrollBoxNode.boxObject;
|
|
let nsIScrollBoxObject = boxObject.QueryInterface(Ci.nsIScrollBoxObject);
|
|
nsIScrollBoxObject.ensureElementIsVisible(aNode);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Creates a DOM Node factory for XUL nodes - as well as textNodes
|
|
* @param aFactoryType
|
|
* "xul" or "text"
|
|
* @returns DOM Node Factory function
|
|
*/
|
|
function NodeFactory(aFactoryType, aNameSpace, aDocument)
|
|
{
|
|
// aDocument is presumed to be a XULDocument
|
|
if (aFactoryType == "text") {
|
|
function factory(aText) {
|
|
return aDocument.createTextNode(aText);
|
|
}
|
|
return factory;
|
|
}
|
|
else {
|
|
if (aNameSpace == "xul") {
|
|
function factory(aTag) {
|
|
return aDocument.createElement(aTag);
|
|
}
|
|
return factory;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// HeadsUpDisplayUICommands
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
HeadsUpDisplayUICommands = {
|
|
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 hud = gBrowser.selectedTab.ownerDocument.getElementById(hudId);
|
|
if (hud) {
|
|
HUDService.deactivateHUDForContext(gBrowser.selectedTab);
|
|
}
|
|
else {
|
|
HUDService.activateHUDForContext(gBrowser.selectedTab);
|
|
}
|
|
},
|
|
|
|
toggleFilter: function UIC_toggleFilter(aButton) {
|
|
var filter = aButton.getAttribute("buttonType");
|
|
var hudId = aButton.getAttribute("hudId");
|
|
var state = HUDService.getFilterState(hudId, filter);
|
|
if (state) {
|
|
HUDService.setFilterState(hudId, filter, false);
|
|
aButton.setAttribute("checked", false);
|
|
}
|
|
else {
|
|
HUDService.setFilterState(hudId, filter, true);
|
|
aButton.setAttribute("checked", true);
|
|
}
|
|
},
|
|
|
|
command: function UIC_command(aButton) {
|
|
var filter = aButton.getAttribute("buttonType");
|
|
var hudId = aButton.getAttribute("hudId");
|
|
if (filter == "clear") {
|
|
HUDService.clearDisplay(hudId);
|
|
}
|
|
},
|
|
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// ConsoleStorage
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
var prefs = Services.prefs;
|
|
|
|
const GLOBAL_STORAGE_INDEX_ID = "GLOBAL_CONSOLE";
|
|
const PREFS_BRANCH_PREF = "devtools.hud.display.filter";
|
|
const PREFS_PREFIX = "devtools.hud.display.filter.";
|
|
const PREFS = { network: PREFS_PREFIX + "network",
|
|
cssparser: PREFS_PREFIX + "cssparser",
|
|
exception: PREFS_PREFIX + "exception",
|
|
error: PREFS_PREFIX + "error",
|
|
info: PREFS_PREFIX + "info",
|
|
warn: PREFS_PREFIX + "warn",
|
|
log: PREFS_PREFIX + "log",
|
|
global: PREFS_PREFIX + "global",
|
|
};
|
|
|
|
function ConsoleStorage()
|
|
{
|
|
this.sequencer = null;
|
|
this.consoleDisplays = {};
|
|
// each display will have an index that tracks each ConsoleEntry
|
|
this.displayIndexes = {};
|
|
this.globalStorageIndex = [];
|
|
this.globalDisplay = {};
|
|
this.createDisplay(GLOBAL_STORAGE_INDEX_ID);
|
|
// TODO: need to create a method that truncates the message
|
|
// see bug 570543
|
|
|
|
// store an index of display prefs
|
|
this.displayPrefs = {};
|
|
|
|
// check prefs for existence, create & load if absent, load them if present
|
|
let filterPrefs;
|
|
let defaultDisplayPrefs;
|
|
|
|
try {
|
|
filterPrefs = prefs.getBoolPref(PREFS_BRANCH_PREF);
|
|
}
|
|
catch (ex) {
|
|
filterPrefs = false;
|
|
}
|
|
|
|
// TODO: for FINAL release,
|
|
// use the sitePreferencesService to save specific site prefs
|
|
// see bug 570545
|
|
|
|
if (filterPrefs) {
|
|
defaultDisplayPrefs = {
|
|
network: (prefs.getBoolPref(PREFS.network) ? true: false),
|
|
cssparser: (prefs.getBoolPref(PREFS.cssparser) ? true: false),
|
|
exception: (prefs.getBoolPref(PREFS.exception) ? true: false),
|
|
error: (prefs.getBoolPref(PREFS.error) ? true: false),
|
|
info: (prefs.getBoolPref(PREFS.info) ? true: false),
|
|
warn: (prefs.getBoolPref(PREFS.warn) ? true: false),
|
|
log: (prefs.getBoolPref(PREFS.log) ? true: false),
|
|
global: (prefs.getBoolPref(PREFS.global) ? true: false),
|
|
};
|
|
}
|
|
else {
|
|
prefs.setBoolPref(PREFS_BRANCH_PREF, false);
|
|
// default prefs for each HeadsUpDisplay
|
|
prefs.setBoolPref(PREFS.network, true);
|
|
prefs.setBoolPref(PREFS.cssparser, true);
|
|
prefs.setBoolPref(PREFS.exception, true);
|
|
prefs.setBoolPref(PREFS.error, true);
|
|
prefs.setBoolPref(PREFS.info, true);
|
|
prefs.setBoolPref(PREFS.warn, true);
|
|
prefs.setBoolPref(PREFS.log, true);
|
|
prefs.setBoolPref(PREFS.global, false);
|
|
|
|
defaultDisplayPrefs = {
|
|
network: prefs.getBoolPref(PREFS.network),
|
|
cssparser: prefs.getBoolPref(PREFS.cssparser),
|
|
exception: prefs.getBoolPref(PREFS.exception),
|
|
error: prefs.getBoolPref(PREFS.error),
|
|
info: prefs.getBoolPref(PREFS.info),
|
|
warn: prefs.getBoolPref(PREFS.warn),
|
|
log: prefs.getBoolPref(PREFS.log),
|
|
global: prefs.getBoolPref(PREFS.global),
|
|
};
|
|
}
|
|
this.defaultDisplayPrefs = defaultDisplayPrefs;
|
|
}
|
|
|
|
ConsoleStorage.prototype = {
|
|
|
|
updateDefaultDisplayPrefs:
|
|
function CS_updateDefaultDisplayPrefs(aPrefsObject) {
|
|
prefs.setBoolPref(PREFS.network, (aPrefsObject.network ? true : false));
|
|
prefs.setBoolPref(PREFS.cssparser, (aPrefsObject.cssparser ? true : false));
|
|
prefs.setBoolPref(PREFS.exception, (aPrefsObject.exception ? true : false));
|
|
prefs.setBoolPref(PREFS.error, (aPrefsObject.error ? true : false));
|
|
prefs.setBoolPref(PREFS.info, (aPrefsObject.info ? true : false));
|
|
prefs.setBoolPref(PREFS.warn, (aPrefsObject.warn ? true : false));
|
|
prefs.setBoolPref(PREFS.log, (aPrefsObject.log ? true : false));
|
|
prefs.setBoolPref(PREFS.global, (aPrefsObject.global ? true : false));
|
|
},
|
|
|
|
sequenceId: function CS_sequencerId()
|
|
{
|
|
if (!this.sequencer) {
|
|
this.sequencer = this.createSequencer();
|
|
}
|
|
return this.sequencer.next();
|
|
},
|
|
|
|
createSequencer: function CS_createSequencer()
|
|
{
|
|
function sequencer(aInt) {
|
|
while(1) {
|
|
aInt++;
|
|
yield aInt;
|
|
}
|
|
}
|
|
return sequencer(-1);
|
|
},
|
|
|
|
globalStore: function CS_globalStore(aIndex)
|
|
{
|
|
return this.displayStore(GLOBAL_CONSOLE_DOM_NODE_ID);
|
|
},
|
|
|
|
displayStore: function CS_displayStore(aId)
|
|
{
|
|
var self = this;
|
|
var idx = -1;
|
|
var id = aId;
|
|
var aLength = self.displayIndexes[id].length;
|
|
|
|
function displayStoreGenerator(aInt, aLength)
|
|
{
|
|
// create a generator object to iterate through any of the display stores
|
|
// from any index-starting-point
|
|
while(1) {
|
|
// throw if we exceed the length of displayIndexes?
|
|
aInt++;
|
|
var indexIt = self.displayIndexes[id];
|
|
var index = indexIt[aInt];
|
|
if (aLength < aInt) {
|
|
// try to see if we have more entries:
|
|
var newLength = self.displayIndexes[id].length;
|
|
if (newLength > aLength) {
|
|
aLength = newLength;
|
|
}
|
|
else {
|
|
throw new StopIteration();
|
|
}
|
|
}
|
|
var entry = self.consoleDisplays[id][index];
|
|
yield entry;
|
|
}
|
|
}
|
|
|
|
return displayStoreGenerator(-1, aLength);
|
|
},
|
|
|
|
recordEntries: function CS_recordEntries(aHUDId, aConfigArray)
|
|
{
|
|
var len = aConfigArray.length;
|
|
for (var i = 0; i < len; i++){
|
|
this.recordEntry(aHUDId, aConfigArray[i]);
|
|
}
|
|
},
|
|
|
|
|
|
recordEntry: function CS_recordEntry(aHUDId, aConfig)
|
|
{
|
|
var id = this.sequenceId();
|
|
|
|
this.globalStorageIndex[id] = { hudId: aHUDId };
|
|
|
|
var displayStorage = this.consoleDisplays[aHUDId];
|
|
|
|
var displayIndex = this.displayIndexes[aHUDId];
|
|
|
|
if (displayStorage && displayIndex) {
|
|
var entry = new ConsoleEntry(aConfig, id);
|
|
displayIndex.push(entry.id);
|
|
displayStorage[entry.id] = entry;
|
|
return entry;
|
|
}
|
|
else {
|
|
throw new Error("Cannot get displayStorage or index object for id " + aHUDId);
|
|
}
|
|
},
|
|
|
|
getEntry: function CS_getEntry(aId)
|
|
{
|
|
var display = this.globalStorageIndex[aId];
|
|
var storName = display.hudId;
|
|
return this.consoleDisplays[storName][aId];
|
|
},
|
|
|
|
updateEntry: function CS_updateEntry(aUUID)
|
|
{
|
|
// update an individual entry
|
|
// TODO: see bug 568634
|
|
},
|
|
|
|
createDisplay: function CS_createdisplay(aId)
|
|
{
|
|
if (!this.consoleDisplays[aId]) {
|
|
this.consoleDisplays[aId] = {};
|
|
this.displayIndexes[aId] = [];
|
|
}
|
|
},
|
|
|
|
removeDisplay: function CS_removeDisplay(aId)
|
|
{
|
|
try {
|
|
delete this.consoleDisplays[aId];
|
|
delete this.displayIndexes[aId];
|
|
}
|
|
catch (ex) {
|
|
Cu.reportError("Could not remove console display for id " + aId);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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, "xpcom-shutdown", false);
|
|
Services.obs.addObserver(this, "content-document-global-created", false);
|
|
},
|
|
|
|
observe: function HWO_observe(aSubject, aTopic, aData)
|
|
{
|
|
if (aTopic == "content-document-global-created") {
|
|
HUDService.windowInitializer(aSubject);
|
|
}
|
|
else if (aTopic == "xpcom-shutdown") {
|
|
this.uninit();
|
|
}
|
|
},
|
|
|
|
uninit: function HWO_uninit()
|
|
{
|
|
Services.obs.removeObserver(this, "content-document-global-created");
|
|
HUDService.shutdown();
|
|
},
|
|
|
|
/**
|
|
* once an initial console is created set this to true so we don't
|
|
* over initialize
|
|
*/
|
|
initialConsoleCreated: false,
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// HUDConsoleObserver
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
/**
|
|
* HUDConsoleObserver: Observes nsIConsoleService for global consoleMessages,
|
|
* if a message originates inside a contentWindow we are tracking,
|
|
* then route that message to the HUDService for logging.
|
|
*/
|
|
|
|
HUDConsoleObserver = {
|
|
QueryInterface: XPCOMUtils.generateQI(
|
|
[Ci.nsIObserver]
|
|
),
|
|
|
|
init: function HCO_init()
|
|
{
|
|
Services.console.registerListener(this);
|
|
Services.obs.addObserver(this, "xpcom-shutdown", false);
|
|
},
|
|
|
|
observe: function HCO_observe(aSubject, aTopic, aData)
|
|
{
|
|
if (aTopic == "xpcom-shutdown") {
|
|
Services.console.unregisterListener(this);
|
|
}
|
|
|
|
if (aSubject instanceof Ci.nsIScriptError) {
|
|
switch (aSubject.category) {
|
|
case "XPConnect JavaScript":
|
|
case "component javascript":
|
|
case "chrome javascript":
|
|
// we ignore these CHROME-originating errors as we only
|
|
// care about content
|
|
return;
|
|
case "HUDConsole":
|
|
case "CSS Parser":
|
|
case "content javascript":
|
|
HUDService.reportConsoleServiceContentScriptError(aSubject);
|
|
return;
|
|
default:
|
|
HUDService.reportConsoleServiceMessage(aSubject);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// 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");
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// HUDService (exported symbol)
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
try {
|
|
// start the HUDService
|
|
// This is in a try block because we want to kill everything if
|
|
// *any* of this fails
|
|
var HUDService = new HUD_SERVICE();
|
|
HUDWindowObserver.init();
|
|
HUDConsoleObserver.init();
|
|
}
|
|
catch (ex) {
|
|
Cu.reportError("HUDService failed initialization.\n" + ex);
|
|
// TODO: kill anything that may have started up
|
|
// see bug 568665
|
|
}
|