/* -*- 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 (original author) * Rob Campbell * Johnathan Nightingale * Patrick Walton * Julian Viereck * Mihai Șucan * * 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); let newInputStream = storageStream.newInputStream(0); try { this.originalListener.onDataAvailable(aRequest, aContext, newInputStream, aOffset, aCount); } catch(ex) { aRequest.cancel(ex); } }, /** * See documentation at * https://developer.mozilla.org/En/NsIRequestObserver * * @param nsIRequest aRequest * @param nsISupports aContext */ onStartRequest: function RL_onStartRequest(aRequest, aContext) { try { this.originalListener.onStartRequest(aRequest, aContext); } catch(ex) { aRequest.cancel(ex); } }, /** * 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) { try { this.originalListener.onStopRequest(aRequest, aContext, aStatusCode); } catch (ex) { } 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: * * ${line}: * ${aList[line]}
* * 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.lineNumber, aActivityObject.columnNumber]; 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 }