michael@0: /* -*- js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const {Cc, Ci, Cu} = require("chrome"); michael@0: michael@0: let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils; michael@0: michael@0: loader.lazyServiceGetter(this, "clipboardHelper", michael@0: "@mozilla.org/widget/clipboardhelper;1", michael@0: "nsIClipboardHelper"); michael@0: loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm"); michael@0: loader.lazyImporter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise"); michael@0: loader.lazyGetter(this, "EventEmitter", () => require("devtools/toolkit/event-emitter")); michael@0: loader.lazyGetter(this, "AutocompletePopup", michael@0: () => require("devtools/shared/autocomplete-popup").AutocompletePopup); michael@0: loader.lazyGetter(this, "ToolSidebar", michael@0: () => require("devtools/framework/sidebar").ToolSidebar); michael@0: loader.lazyGetter(this, "NetworkPanel", michael@0: () => require("devtools/webconsole/network-panel").NetworkPanel); michael@0: loader.lazyGetter(this, "ConsoleOutput", michael@0: () => require("devtools/webconsole/console-output").ConsoleOutput); michael@0: loader.lazyGetter(this, "Messages", michael@0: () => require("devtools/webconsole/console-output").Messages); michael@0: loader.lazyImporter(this, "EnvironmentClient", "resource://gre/modules/devtools/dbg-client.jsm"); michael@0: loader.lazyImporter(this, "ObjectClient", "resource://gre/modules/devtools/dbg-client.jsm"); michael@0: loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm"); michael@0: loader.lazyImporter(this, "VariablesViewController", "resource:///modules/devtools/VariablesViewController.jsm"); michael@0: loader.lazyImporter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); michael@0: loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm"); michael@0: michael@0: const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; michael@0: let l10n = new WebConsoleUtils.l10n(STRINGS_URI); michael@0: michael@0: const XHTML_NS = "http://www.w3.org/1999/xhtml"; michael@0: michael@0: const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/docs/Security/MixedContent"; michael@0: michael@0: const INSECURE_PASSWORDS_LEARN_MORE = "https://developer.mozilla.org/docs/Security/InsecurePasswords"; michael@0: michael@0: const STRICT_TRANSPORT_SECURITY_LEARN_MORE = "https://developer.mozilla.org/docs/Security/HTTP_Strict_Transport_Security"; michael@0: michael@0: const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers"; michael@0: michael@0: const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul"; michael@0: michael@0: const CONSOLE_DIR_VIEW_HEIGHT = 0.6; michael@0: michael@0: const IGNORED_SOURCE_URLS = ["debugger eval code", "self-hosted"]; michael@0: michael@0: // The amount of time in milliseconds that we wait before performing a live michael@0: // search. michael@0: const SEARCH_DELAY = 200; michael@0: michael@0: // The number of lines that are displayed in the console output by default, for michael@0: // each category. The user can change this number by adjusting the hidden michael@0: // "devtools.hud.loglimit.{network,cssparser,exception,console}" preferences. michael@0: const DEFAULT_LOG_LIMIT = 200; michael@0: michael@0: // The various categories of messages. We start numbering at zero so we can michael@0: // use these as indexes into the MESSAGE_PREFERENCE_KEYS matrix below. michael@0: const CATEGORY_NETWORK = 0; michael@0: const CATEGORY_CSS = 1; michael@0: const CATEGORY_JS = 2; michael@0: const CATEGORY_WEBDEV = 3; michael@0: const CATEGORY_INPUT = 4; // always on michael@0: const CATEGORY_OUTPUT = 5; // always on michael@0: const CATEGORY_SECURITY = 6; michael@0: michael@0: // The possible message severities. As before, we start at zero so we can use michael@0: // these as indexes into MESSAGE_PREFERENCE_KEYS. michael@0: const SEVERITY_ERROR = 0; michael@0: const SEVERITY_WARNING = 1; michael@0: const SEVERITY_INFO = 2; michael@0: const SEVERITY_LOG = 3; michael@0: michael@0: // The fragment of a CSS class name that identifies each category. michael@0: const CATEGORY_CLASS_FRAGMENTS = [ michael@0: "network", michael@0: "cssparser", michael@0: "exception", michael@0: "console", michael@0: "input", michael@0: "output", michael@0: "security", michael@0: ]; michael@0: michael@0: // The fragment of a CSS class name that identifies each severity. michael@0: const SEVERITY_CLASS_FRAGMENTS = [ michael@0: "error", michael@0: "warn", michael@0: "info", michael@0: "log", michael@0: ]; michael@0: michael@0: // The preference keys to use for each category/severity combination, indexed michael@0: // first by category (rows) and then by severity (columns). michael@0: // michael@0: // Most of these rather idiosyncratic names are historical and predate the michael@0: // division of message type into "category" and "severity". michael@0: const MESSAGE_PREFERENCE_KEYS = [ michael@0: // Error Warning Info Log michael@0: [ "network", "netwarn", null, "networkinfo", ], // Network michael@0: [ "csserror", "cssparser", null, "csslog", ], // CSS michael@0: [ "exception", "jswarn", null, "jslog", ], // JS michael@0: [ "error", "warn", "info", "log", ], // Web Developer michael@0: [ null, null, null, null, ], // Input michael@0: [ null, null, null, null, ], // Output michael@0: [ "secerror", "secwarn", null, null, ], // Security michael@0: ]; michael@0: michael@0: // A mapping from the console API log event levels to the Web Console michael@0: // severities. michael@0: const LEVELS = { michael@0: error: SEVERITY_ERROR, michael@0: exception: SEVERITY_ERROR, michael@0: assert: SEVERITY_ERROR, michael@0: warn: SEVERITY_WARNING, michael@0: info: SEVERITY_INFO, michael@0: log: SEVERITY_LOG, michael@0: trace: SEVERITY_LOG, michael@0: debug: SEVERITY_LOG, michael@0: dir: SEVERITY_LOG, michael@0: group: SEVERITY_LOG, michael@0: groupCollapsed: SEVERITY_LOG, michael@0: groupEnd: SEVERITY_LOG, michael@0: time: SEVERITY_LOG, michael@0: timeEnd: SEVERITY_LOG, michael@0: count: SEVERITY_LOG michael@0: }; michael@0: michael@0: // The lowest HTTP response code (inclusive) that is considered an error. michael@0: const MIN_HTTP_ERROR_CODE = 400; michael@0: // The highest HTTP response code (inclusive) that is considered an error. michael@0: const MAX_HTTP_ERROR_CODE = 599; michael@0: michael@0: // Constants used for defining the direction of JSTerm input history navigation. michael@0: const HISTORY_BACK = -1; michael@0: const HISTORY_FORWARD = 1; michael@0: michael@0: // The indent of a console group in pixels. michael@0: const GROUP_INDENT = 12; michael@0: michael@0: // The number of messages to display in a single display update. If we display michael@0: // too many messages at once we slow the Firefox UI too much. michael@0: const MESSAGES_IN_INTERVAL = DEFAULT_LOG_LIMIT; michael@0: michael@0: // The delay between display updates - tells how often we should *try* to push michael@0: // new messages to screen. This value is optimistic, updates won't always michael@0: // happen. Keep this low so the Web Console output feels live. michael@0: const OUTPUT_INTERVAL = 50; // milliseconds michael@0: michael@0: // When the output queue has more than MESSAGES_IN_INTERVAL items we throttle michael@0: // output updates to this number of milliseconds. So during a lot of output we michael@0: // update every N milliseconds given here. michael@0: const THROTTLE_UPDATES = 1000; // milliseconds michael@0: michael@0: // The preference prefix for all of the Web Console filters. michael@0: const FILTER_PREFS_PREFIX = "devtools.webconsole.filter."; michael@0: michael@0: // The minimum font size. michael@0: const MIN_FONT_SIZE = 10; michael@0: michael@0: const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout"; michael@0: const PREF_PERSISTLOG = "devtools.webconsole.persistlog"; michael@0: const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages"; michael@0: michael@0: /** michael@0: * A WebConsoleFrame instance is an interactive console initialized *per target* michael@0: * that displays console log data as well as provides an interactive terminal to michael@0: * manipulate the target's document content. michael@0: * michael@0: * The WebConsoleFrame is responsible for the actual Web Console UI michael@0: * implementation. michael@0: * michael@0: * @constructor michael@0: * @param object aWebConsoleOwner michael@0: * The WebConsole owner object. michael@0: */ michael@0: function WebConsoleFrame(aWebConsoleOwner) michael@0: { michael@0: this.owner = aWebConsoleOwner; michael@0: this.hudId = this.owner.hudId; michael@0: this.window = this.owner.iframeWindow; michael@0: michael@0: this._repeatNodes = {}; michael@0: this._outputQueue = []; michael@0: this._pruneCategoriesQueue = {}; michael@0: this._networkRequests = {}; michael@0: this.filterPrefs = {}; michael@0: michael@0: this.output = new ConsoleOutput(this); michael@0: michael@0: this._toggleFilter = this._toggleFilter.bind(this); michael@0: this._onPanelSelected = this._onPanelSelected.bind(this); michael@0: this._flushMessageQueue = this._flushMessageQueue.bind(this); michael@0: this._onToolboxPrefChanged = this._onToolboxPrefChanged.bind(this); michael@0: michael@0: this._outputTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: this._outputTimerInitialized = false; michael@0: michael@0: EventEmitter.decorate(this); michael@0: } michael@0: exports.WebConsoleFrame = WebConsoleFrame; michael@0: michael@0: WebConsoleFrame.prototype = { michael@0: /** michael@0: * The WebConsole instance that owns this frame. michael@0: * @see hudservice.js::WebConsole michael@0: * @type object michael@0: */ michael@0: owner: null, michael@0: michael@0: /** michael@0: * Proxy between the Web Console and the remote Web Console instance. This michael@0: * object holds methods used for connecting, listening and disconnecting from michael@0: * the remote server, using the remote debugging protocol. michael@0: * michael@0: * @see WebConsoleConnectionProxy michael@0: * @type object michael@0: */ michael@0: proxy: null, michael@0: michael@0: /** michael@0: * Getter for the xul:popupset that holds any popups we open. michael@0: * @type nsIDOMElement michael@0: */ michael@0: get popupset() this.owner.mainPopupSet, michael@0: michael@0: /** michael@0: * Holds the initialization promise object. michael@0: * @private michael@0: * @type object michael@0: */ michael@0: _initDefer: null, michael@0: michael@0: /** michael@0: * Holds the network requests currently displayed by the Web Console. Each key michael@0: * represents the connection ID and the value is network request information. michael@0: * @private michael@0: * @type object michael@0: */ michael@0: _networkRequests: null, michael@0: michael@0: /** michael@0: * Last time when we displayed any message in the output. michael@0: * michael@0: * @private michael@0: * @type number michael@0: * Timestamp in milliseconds since the Unix epoch. michael@0: */ michael@0: _lastOutputFlush: 0, michael@0: michael@0: /** michael@0: * Message nodes are stored here in a queue for later display. michael@0: * michael@0: * @private michael@0: * @type array michael@0: */ michael@0: _outputQueue: null, michael@0: michael@0: /** michael@0: * Keep track of the categories we need to prune from time to time. michael@0: * michael@0: * @private michael@0: * @type array michael@0: */ michael@0: _pruneCategoriesQueue: null, michael@0: michael@0: /** michael@0: * Function invoked whenever the output queue is emptied. This is used by some michael@0: * tests. michael@0: * michael@0: * @private michael@0: * @type function michael@0: */ michael@0: _flushCallback: null, michael@0: michael@0: /** michael@0: * Timer used for flushing the messages output queue. michael@0: * michael@0: * @private michael@0: * @type nsITimer michael@0: */ michael@0: _outputTimer: null, michael@0: _outputTimerInitialized: null, michael@0: michael@0: /** michael@0: * Store for tracking repeated nodes. michael@0: * @private michael@0: * @type object michael@0: */ michael@0: _repeatNodes: null, michael@0: michael@0: /** michael@0: * Preferences for filtering messages by type. michael@0: * @see this._initDefaultFilterPrefs() michael@0: * @type object michael@0: */ michael@0: filterPrefs: null, michael@0: michael@0: /** michael@0: * Prefix used for filter preferences. michael@0: * @private michael@0: * @type string michael@0: */ michael@0: _filterPrefsPrefix: FILTER_PREFS_PREFIX, michael@0: michael@0: /** michael@0: * The nesting depth of the currently active console group. michael@0: */ michael@0: groupDepth: 0, michael@0: michael@0: /** michael@0: * The current target location. michael@0: * @type string michael@0: */ michael@0: contentLocation: "", michael@0: michael@0: /** michael@0: * The JSTerm object that manage the console's input. michael@0: * @see JSTerm michael@0: * @type object michael@0: */ michael@0: jsterm: null, michael@0: michael@0: /** michael@0: * The element that holds all of the messages we display. michael@0: * @type nsIDOMElement michael@0: */ michael@0: outputNode: null, michael@0: michael@0: /** michael@0: * The ConsoleOutput instance that manages all output. michael@0: * @type object michael@0: */ michael@0: output: null, michael@0: michael@0: /** michael@0: * The input element that allows the user to filter messages by string. michael@0: * @type nsIDOMElement michael@0: */ michael@0: filterBox: null, michael@0: michael@0: /** michael@0: * Getter for the debugger WebConsoleClient. michael@0: * @type object michael@0: */ michael@0: get webConsoleClient() this.proxy ? this.proxy.webConsoleClient : null, michael@0: michael@0: _destroyer: null, michael@0: michael@0: // Used in tests. michael@0: _saveRequestAndResponseBodies: false, michael@0: michael@0: // Chevron width at the starting of Web Console's input box. michael@0: _chevronWidth: 0, michael@0: // Width of the monospace characters in Web Console's input box. michael@0: _inputCharWidth: 0, michael@0: michael@0: /** michael@0: * Tells whether to save the bodies of network requests and responses. michael@0: * Disabled by default to save memory. michael@0: * michael@0: * @return boolean michael@0: * The saveRequestAndResponseBodies pref value. michael@0: */ michael@0: getSaveRequestAndResponseBodies: michael@0: function WCF_getSaveRequestAndResponseBodies() { michael@0: let deferred = promise.defer(); michael@0: let toGet = [ michael@0: "NetworkMonitor.saveRequestAndResponseBodies" michael@0: ]; michael@0: michael@0: // Make sure the web console client connection is established first. michael@0: this.webConsoleClient.getPreferences(toGet, aResponse => { michael@0: if (!aResponse.error) { michael@0: this._saveRequestAndResponseBodies = aResponse.preferences[toGet[0]]; michael@0: deferred.resolve(this._saveRequestAndResponseBodies); michael@0: } michael@0: else { michael@0: deferred.reject(aResponse.error); michael@0: } michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Setter for saving of network request and response bodies. michael@0: * michael@0: * @param boolean aValue michael@0: * The new value you want to set. michael@0: */ michael@0: setSaveRequestAndResponseBodies: michael@0: function WCF_setSaveRequestAndResponseBodies(aValue) { michael@0: if (!this.webConsoleClient) { michael@0: // Don't continue if the webconsole disconnected. michael@0: return promise.resolve(null); michael@0: } michael@0: michael@0: let deferred = promise.defer(); michael@0: let newValue = !!aValue; michael@0: let toSet = { michael@0: "NetworkMonitor.saveRequestAndResponseBodies": newValue, michael@0: }; michael@0: michael@0: // Make sure the web console client connection is established first. michael@0: this.webConsoleClient.setPreferences(toSet, aResponse => { michael@0: if (!aResponse.error) { michael@0: this._saveRequestAndResponseBodies = newValue; michael@0: deferred.resolve(aResponse); michael@0: } michael@0: else { michael@0: deferred.reject(aResponse.error); michael@0: } michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Getter for the persistent logging preference. michael@0: * @type boolean michael@0: */ michael@0: get persistLog() { michael@0: return Services.prefs.getBoolPref(PREF_PERSISTLOG); michael@0: }, michael@0: michael@0: /** michael@0: * Initialize the WebConsoleFrame instance. michael@0: * @return object michael@0: * A promise object for the initialization. michael@0: */ michael@0: init: function WCF_init() michael@0: { michael@0: this._initUI(); michael@0: return this._initConnection(); michael@0: }, michael@0: michael@0: /** michael@0: * Connect to the server using the remote debugging protocol. michael@0: * michael@0: * @private michael@0: * @return object michael@0: * A promise object that is resolved/reject based on the connection michael@0: * result. michael@0: */ michael@0: _initConnection: function WCF__initConnection() michael@0: { michael@0: if (this._initDefer) { michael@0: return this._initDefer.promise; michael@0: } michael@0: michael@0: this._initDefer = promise.defer(); michael@0: this.proxy = new WebConsoleConnectionProxy(this, this.owner.target); michael@0: michael@0: this.proxy.connect().then(() => { // on success michael@0: this._initDefer.resolve(this); michael@0: }, (aReason) => { // on failure michael@0: let node = this.createMessageNode(CATEGORY_JS, SEVERITY_ERROR, michael@0: aReason.error + ": " + aReason.message); michael@0: this.outputMessage(CATEGORY_JS, node); michael@0: this._initDefer.reject(aReason); michael@0: }).then(() => { michael@0: let id = WebConsoleUtils.supportsString(this.hudId); michael@0: Services.obs.notifyObservers(id, "web-console-created", null); michael@0: }); michael@0: michael@0: return this._initDefer.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Find the Web Console UI elements and setup event listeners as needed. michael@0: * @private michael@0: */ michael@0: _initUI: function WCF__initUI() michael@0: { michael@0: this.document = this.window.document; michael@0: this.rootElement = this.document.documentElement; michael@0: michael@0: this._initDefaultFilterPrefs(); michael@0: michael@0: // Register the controller to handle "select all" properly. michael@0: this._commandController = new CommandController(this); michael@0: this.window.controllers.insertControllerAt(0, this._commandController); michael@0: michael@0: this._contextMenuHandler = new ConsoleContextMenu(this); michael@0: michael@0: let doc = this.document; michael@0: michael@0: this.filterBox = doc.querySelector(".hud-filter-box"); michael@0: this.outputNode = doc.getElementById("output-container"); michael@0: this.completeNode = doc.querySelector(".jsterm-complete-node"); michael@0: this.inputNode = doc.querySelector(".jsterm-input-node"); michael@0: michael@0: this._setFilterTextBoxEvents(); michael@0: this._initFilterButtons(); michael@0: michael@0: let fontSize = this.owner._browserConsole ? michael@0: Services.prefs.getIntPref("devtools.webconsole.fontSize") : 0; michael@0: michael@0: if (fontSize != 0) { michael@0: fontSize = Math.max(MIN_FONT_SIZE, fontSize); michael@0: michael@0: this.outputNode.style.fontSize = fontSize + "px"; michael@0: this.completeNode.style.fontSize = fontSize + "px"; michael@0: this.inputNode.style.fontSize = fontSize + "px"; michael@0: } michael@0: michael@0: if (this.owner._browserConsole) { michael@0: for (let id of ["Enlarge", "Reduce", "Reset"]) { michael@0: this.document.getElementById("cmd_fullZoom" + id) michael@0: .removeAttribute("disabled"); michael@0: } michael@0: } michael@0: michael@0: // Update the character width and height needed for the popup offset michael@0: // calculations. michael@0: this._updateCharSize(); michael@0: michael@0: let updateSaveBodiesPrefUI = (aElement) => { michael@0: this.getSaveRequestAndResponseBodies().then(aValue => { michael@0: aElement.setAttribute("checked", aValue); michael@0: this.emit("save-bodies-ui-toggled"); michael@0: }); michael@0: } michael@0: michael@0: let reverseSaveBodiesPref = ({ target: aElement }) => { michael@0: this.getSaveRequestAndResponseBodies().then(aValue => { michael@0: this.setSaveRequestAndResponseBodies(!aValue); michael@0: aElement.setAttribute("checked", aValue); michael@0: this.emit("save-bodies-pref-reversed"); michael@0: }); michael@0: } michael@0: michael@0: let saveBodies = doc.getElementById("saveBodies"); michael@0: saveBodies.addEventListener("command", reverseSaveBodiesPref); michael@0: saveBodies.disabled = !this.getFilterState("networkinfo") && michael@0: !this.getFilterState("network"); michael@0: michael@0: let saveBodiesContextMenu = doc.getElementById("saveBodiesContextMenu"); michael@0: saveBodiesContextMenu.addEventListener("command", reverseSaveBodiesPref); michael@0: saveBodiesContextMenu.disabled = !this.getFilterState("networkinfo") && michael@0: !this.getFilterState("network"); michael@0: michael@0: saveBodies.parentNode.addEventListener("popupshowing", () => { michael@0: updateSaveBodiesPrefUI(saveBodies); michael@0: saveBodies.disabled = !this.getFilterState("networkinfo") && michael@0: !this.getFilterState("network"); michael@0: }); michael@0: michael@0: saveBodiesContextMenu.parentNode.addEventListener("popupshowing", () => { michael@0: updateSaveBodiesPrefUI(saveBodiesContextMenu); michael@0: saveBodiesContextMenu.disabled = !this.getFilterState("networkinfo") && michael@0: !this.getFilterState("network"); michael@0: }); michael@0: michael@0: let clearButton = doc.getElementsByClassName("webconsole-clear-console-button")[0]; michael@0: clearButton.addEventListener("command", () => { michael@0: this.owner._onClearButton(); michael@0: this.jsterm.clearOutput(true); michael@0: }); michael@0: michael@0: this.jsterm = new JSTerm(this); michael@0: this.jsterm.init(); michael@0: michael@0: let toolbox = gDevTools.getToolbox(this.owner.target); michael@0: if (toolbox) { michael@0: toolbox.on("webconsole-selected", this._onPanelSelected); michael@0: } michael@0: michael@0: /* michael@0: * Focus input line whenever the output area is clicked. michael@0: * Reusing _addMEssageLinkCallback since it correctly filters michael@0: * drag and select events. michael@0: */ michael@0: this._addFocusCallback(this.outputNode, (evt) => { michael@0: if ((evt.target.nodeName.toLowerCase() != "a") && michael@0: (evt.target.parentNode.nodeName.toLowerCase() != "a")) { michael@0: this.jsterm.inputNode.focus(); michael@0: } michael@0: }); michael@0: michael@0: // Toggle the timestamp on preference change michael@0: gDevTools.on("pref-changed", this._onToolboxPrefChanged); michael@0: this._onToolboxPrefChanged("pref-changed", { michael@0: pref: PREF_MESSAGE_TIMESTAMP, michael@0: newValue: Services.prefs.getBoolPref(PREF_MESSAGE_TIMESTAMP), michael@0: }); michael@0: michael@0: // focus input node michael@0: this.jsterm.inputNode.focus(); michael@0: }, michael@0: michael@0: /** michael@0: * Sets the focus to JavaScript input field when the web console tab is michael@0: * selected or when there is a split console present. michael@0: * @private michael@0: */ michael@0: _onPanelSelected: function WCF__onPanelSelected(evt, id) michael@0: { michael@0: this.jsterm.inputNode.focus(); michael@0: }, michael@0: michael@0: /** michael@0: * Initialize the default filter preferences. michael@0: * @private michael@0: */ michael@0: _initDefaultFilterPrefs: function WCF__initDefaultFilterPrefs() michael@0: { michael@0: let prefs = ["network", "networkinfo", "csserror", "cssparser", "csslog", michael@0: "exception", "jswarn", "jslog", "error", "info", "warn", "log", michael@0: "secerror", "secwarn", "netwarn"]; michael@0: for (let pref of prefs) { michael@0: this.filterPrefs[pref] = Services.prefs michael@0: .getBoolPref(this._filterPrefsPrefix + pref); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Attach / detach reflow listeners depending on the checked status michael@0: * of the `CSS > Log` menuitem. michael@0: * michael@0: * @param function [aCallback=null] michael@0: * Optional function to invoke when the listener has been michael@0: * added/removed. michael@0: * michael@0: */ michael@0: _updateReflowActivityListener: michael@0: function WCF__updateReflowActivityListener(aCallback) michael@0: { michael@0: if (this.webConsoleClient) { michael@0: let pref = this._filterPrefsPrefix + "csslog"; michael@0: if (Services.prefs.getBoolPref(pref)) { michael@0: this.webConsoleClient.startListeners(["ReflowActivity"], aCallback); michael@0: } else { michael@0: this.webConsoleClient.stopListeners(["ReflowActivity"], aCallback); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Sets the events for the filter input field. michael@0: * @private michael@0: */ michael@0: _setFilterTextBoxEvents: function WCF__setFilterTextBoxEvents() michael@0: { michael@0: let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: let timerEvent = this.adjustVisibilityOnSearchStringChange.bind(this); michael@0: michael@0: let onChange = function _onChange() { michael@0: // To improve responsiveness, we let the user finish typing before we michael@0: // perform the search. michael@0: timer.cancel(); michael@0: timer.initWithCallback(timerEvent, SEARCH_DELAY, michael@0: Ci.nsITimer.TYPE_ONE_SHOT); michael@0: }; michael@0: michael@0: this.filterBox.addEventListener("command", onChange, false); michael@0: this.filterBox.addEventListener("input", onChange, false); michael@0: }, michael@0: michael@0: /** michael@0: * Creates one of the filter buttons on the toolbar. michael@0: * michael@0: * @private michael@0: * @param nsIDOMNode aParent michael@0: * The node to which the filter button should be appended. michael@0: * @param object aDescriptor michael@0: * A descriptor that contains info about the button. Contains "name", michael@0: * "category", and "prefKey" properties, and optionally a "severities" michael@0: * property. michael@0: */ michael@0: _initFilterButtons: function WCF__initFilterButtons() michael@0: { michael@0: let categories = this.document michael@0: .querySelectorAll(".webconsole-filter-button[category]"); michael@0: Array.forEach(categories, function(aButton) { michael@0: aButton.addEventListener("click", this._toggleFilter, false); michael@0: michael@0: let someChecked = false; michael@0: let severities = aButton.querySelectorAll("menuitem[prefKey]"); michael@0: Array.forEach(severities, function(aMenuItem) { michael@0: aMenuItem.addEventListener("command", this._toggleFilter, false); michael@0: michael@0: let prefKey = aMenuItem.getAttribute("prefKey"); michael@0: let checked = this.filterPrefs[prefKey]; michael@0: aMenuItem.setAttribute("checked", checked); michael@0: someChecked = someChecked || checked; michael@0: }, this); michael@0: michael@0: aButton.setAttribute("checked", someChecked); michael@0: }, this); michael@0: michael@0: if (!this.owner._browserConsole) { michael@0: // The Browser Console displays nsIConsoleMessages which are messages that michael@0: // end up in the JS category, but they are not errors or warnings, they michael@0: // are just log messages. The Web Console does not show such messages. michael@0: let jslog = this.document.querySelector("menuitem[prefKey=jslog]"); michael@0: jslog.hidden = true; michael@0: } michael@0: michael@0: if (Services.appinfo.OS == "Darwin") { michael@0: let net = this.document.querySelector("toolbarbutton[category=net]"); michael@0: let accesskey = net.getAttribute("accesskeyMacOSX"); michael@0: net.setAttribute("accesskey", accesskey); michael@0: michael@0: let logging = this.document.querySelector("toolbarbutton[category=logging]"); michael@0: logging.removeAttribute("accesskey"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Increase, decrease or reset the font size. michael@0: * michael@0: * @param string size michael@0: * The size of the font change. Accepted values are "+" and "-". michael@0: * An unmatched size assumes a font reset. michael@0: */ michael@0: changeFontSize: function WCF_changeFontSize(aSize) michael@0: { michael@0: let fontSize = this.window michael@0: .getComputedStyle(this.outputNode, null) michael@0: .getPropertyValue("font-size").replace("px", ""); michael@0: michael@0: if (this.outputNode.style.fontSize) { michael@0: fontSize = this.outputNode.style.fontSize.replace("px", ""); michael@0: } michael@0: michael@0: if (aSize == "+" || aSize == "-") { michael@0: fontSize = parseInt(fontSize, 10); michael@0: michael@0: if (aSize == "+") { michael@0: fontSize += 1; michael@0: } michael@0: else { michael@0: fontSize -= 1; michael@0: } michael@0: michael@0: if (fontSize < MIN_FONT_SIZE) { michael@0: fontSize = MIN_FONT_SIZE; michael@0: } michael@0: michael@0: Services.prefs.setIntPref("devtools.webconsole.fontSize", fontSize); michael@0: fontSize = fontSize + "px"; michael@0: michael@0: this.completeNode.style.fontSize = fontSize; michael@0: this.inputNode.style.fontSize = fontSize; michael@0: this.outputNode.style.fontSize = fontSize; michael@0: } michael@0: else { michael@0: this.completeNode.style.fontSize = ""; michael@0: this.inputNode.style.fontSize = ""; michael@0: this.outputNode.style.fontSize = ""; michael@0: Services.prefs.clearUserPref("devtools.webconsole.fontSize"); michael@0: } michael@0: this._updateCharSize(); michael@0: }, michael@0: michael@0: /** michael@0: * Calculates the width and height of a single character of the input box. michael@0: * This will be used in opening the popup at the correct offset. michael@0: * michael@0: * @private michael@0: */ michael@0: _updateCharSize: function WCF__updateCharSize() michael@0: { michael@0: let doc = this.document; michael@0: let tempLabel = doc.createElementNS(XHTML_NS, "span"); michael@0: let style = tempLabel.style; michael@0: style.position = "fixed"; michael@0: style.padding = "0"; michael@0: style.margin = "0"; michael@0: style.width = "auto"; michael@0: style.color = "transparent"; michael@0: WebConsoleUtils.copyTextStyles(this.inputNode, tempLabel); michael@0: tempLabel.textContent = "x"; michael@0: doc.documentElement.appendChild(tempLabel); michael@0: this._inputCharWidth = tempLabel.offsetWidth; michael@0: tempLabel.parentNode.removeChild(tempLabel); michael@0: // Calculate the width of the chevron placed at the beginning of the input michael@0: // box. Remove 4 more pixels to accomodate the padding of the popup. michael@0: this._chevronWidth = +doc.defaultView.getComputedStyle(this.inputNode) michael@0: .paddingLeft.replace(/[^0-9.]/g, "") - 4; michael@0: }, michael@0: michael@0: /** michael@0: * The event handler that is called whenever a user switches a filter on or michael@0: * off. michael@0: * michael@0: * @private michael@0: * @param nsIDOMEvent aEvent michael@0: * The event that triggered the filter change. michael@0: */ michael@0: _toggleFilter: function WCF__toggleFilter(aEvent) michael@0: { michael@0: let target = aEvent.target; michael@0: let tagName = target.tagName; michael@0: if (tagName != aEvent.currentTarget.tagName) { michael@0: return; michael@0: } michael@0: michael@0: switch (tagName) { michael@0: case "toolbarbutton": { michael@0: let originalTarget = aEvent.originalTarget; michael@0: let classes = originalTarget.classList; michael@0: michael@0: if (originalTarget.localName !== "toolbarbutton") { michael@0: // Oddly enough, the click event is sent to the menu button when michael@0: // selecting a menu item with the mouse. Detect this case and bail michael@0: // out. michael@0: break; michael@0: } michael@0: michael@0: if (!classes.contains("toolbarbutton-menubutton-button") && michael@0: originalTarget.getAttribute("type") === "menu-button") { michael@0: // This is a filter button with a drop-down. The user clicked the michael@0: // drop-down, so do nothing. (The menu will automatically appear michael@0: // without our intervention.) michael@0: break; michael@0: } michael@0: michael@0: // Toggle on the targeted filter button, and if the user alt clicked, michael@0: // toggle off all other filter buttons and their associated filters. michael@0: let state = target.getAttribute("checked") !== "true"; michael@0: if (aEvent.getModifierState("Alt")) { michael@0: let buttons = this.document michael@0: .querySelectorAll(".webconsole-filter-button"); michael@0: Array.forEach(buttons, (button) => { michael@0: if (button !== target) { michael@0: button.setAttribute("checked", false); michael@0: this._setMenuState(button, false); michael@0: } michael@0: }); michael@0: state = true; michael@0: } michael@0: target.setAttribute("checked", state); michael@0: michael@0: // This is a filter button with a drop-down, and the user clicked the michael@0: // main part of the button. Go through all the severities and toggle michael@0: // their associated filters. michael@0: this._setMenuState(target, state); michael@0: michael@0: // CSS reflow logging can decrease web page performance. michael@0: // Make sure the option is always unchecked when the CSS filter button is selected. michael@0: // See bug 971798. michael@0: if (target.getAttribute("category") == "css" && state) { michael@0: let csslogMenuItem = target.querySelector("menuitem[prefKey=csslog]"); michael@0: csslogMenuItem.setAttribute("checked", false); michael@0: this.setFilterState("csslog", false); michael@0: } michael@0: michael@0: break; michael@0: } michael@0: michael@0: case "menuitem": { michael@0: let state = target.getAttribute("checked") !== "true"; michael@0: target.setAttribute("checked", state); michael@0: michael@0: let prefKey = target.getAttribute("prefKey"); michael@0: this.setFilterState(prefKey, state); michael@0: michael@0: // Disable the log response and request body if network logging is off. michael@0: if (prefKey == "networkinfo" || prefKey == "network") { michael@0: let checkState = !this.getFilterState("networkinfo") && michael@0: !this.getFilterState("network"); michael@0: this.document.getElementById("saveBodies").disabled = checkState; michael@0: this.document.getElementById("saveBodiesContextMenu").disabled = checkState; michael@0: } michael@0: michael@0: // Adjust the state of the button appropriately. michael@0: let menuPopup = target.parentNode; michael@0: michael@0: let someChecked = false; michael@0: let menuItem = menuPopup.firstChild; michael@0: while (menuItem) { michael@0: if (menuItem.hasAttribute("prefKey") && michael@0: menuItem.getAttribute("checked") === "true") { michael@0: someChecked = true; michael@0: break; michael@0: } michael@0: menuItem = menuItem.nextSibling; michael@0: } michael@0: let toolbarButton = menuPopup.parentNode; michael@0: toolbarButton.setAttribute("checked", someChecked); michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Set the menu attributes for a specific toggle button. michael@0: * michael@0: * @private michael@0: * @param XULElement aTarget michael@0: * Button with drop down items to be toggled. michael@0: * @param boolean aState michael@0: * True if the menu item is being toggled on, and false otherwise. michael@0: */ michael@0: _setMenuState: function WCF__setMenuState(aTarget, aState) michael@0: { michael@0: let menuItems = aTarget.querySelectorAll("menuitem"); michael@0: Array.forEach(menuItems, (item) => { michael@0: item.setAttribute("checked", aState); michael@0: let prefKey = item.getAttribute("prefKey"); michael@0: this.setFilterState(prefKey, aState); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Set the filter state for a specific toggle button. michael@0: * michael@0: * @param string aToggleType michael@0: * @param boolean aState michael@0: * @returns void michael@0: */ michael@0: setFilterState: function WCF_setFilterState(aToggleType, aState) michael@0: { michael@0: this.filterPrefs[aToggleType] = aState; michael@0: this.adjustVisibilityForMessageType(aToggleType, aState); michael@0: Services.prefs.setBoolPref(this._filterPrefsPrefix + aToggleType, aState); michael@0: this._updateReflowActivityListener(); michael@0: }, michael@0: michael@0: /** michael@0: * Get the filter state for a specific toggle button. michael@0: * michael@0: * @param string aToggleType michael@0: * @returns boolean michael@0: */ michael@0: getFilterState: function WCF_getFilterState(aToggleType) michael@0: { michael@0: return this.filterPrefs[aToggleType]; michael@0: }, michael@0: michael@0: /** michael@0: * Check that the passed string matches the filter arguments. michael@0: * michael@0: * @param String aString michael@0: * to search for filter words in. michael@0: * @param String aFilter michael@0: * is a string containing all of the words to filter on. michael@0: * @returns boolean michael@0: */ michael@0: stringMatchesFilters: function WCF_stringMatchesFilters(aString, aFilter) michael@0: { michael@0: if (!aFilter || !aString) { michael@0: return true; michael@0: } michael@0: michael@0: let searchStr = aString.toLowerCase(); michael@0: let filterStrings = aFilter.toLowerCase().split(/\s+/); michael@0: return !filterStrings.some(function (f) { michael@0: return searchStr.indexOf(f) == -1; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Turns the display of log nodes on and off appropriately to reflect the michael@0: * adjustment of the message type filter named by @aPrefKey. michael@0: * michael@0: * @param string aPrefKey michael@0: * The preference key for the message type being filtered: one of the michael@0: * values in the MESSAGE_PREFERENCE_KEYS table. michael@0: * @param boolean aState michael@0: * True if the filter named by @aMessageType is being turned on; false michael@0: * otherwise. michael@0: * @returns void michael@0: */ michael@0: adjustVisibilityForMessageType: michael@0: function WCF_adjustVisibilityForMessageType(aPrefKey, aState) michael@0: { michael@0: let outputNode = this.outputNode; michael@0: let doc = this.document; michael@0: michael@0: // Look for message nodes (".message") with the given preference key michael@0: // (filter="error", filter="cssparser", etc.) and add or remove the michael@0: // "filtered-by-type" class, which turns on or off the display. michael@0: michael@0: let xpath = ".//*[contains(@class, 'message') and " + michael@0: "@filter='" + aPrefKey + "']"; michael@0: let result = doc.evaluate(xpath, outputNode, null, michael@0: Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); michael@0: for (let i = 0; i < result.snapshotLength; i++) { michael@0: let node = result.snapshotItem(i); michael@0: if (aState) { michael@0: node.classList.remove("filtered-by-type"); michael@0: } michael@0: else { michael@0: node.classList.add("filtered-by-type"); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Turns the display of log nodes on and off appropriately to reflect the michael@0: * adjustment of the search string. michael@0: */ michael@0: adjustVisibilityOnSearchStringChange: michael@0: function WCF_adjustVisibilityOnSearchStringChange() michael@0: { michael@0: let nodes = this.outputNode.getElementsByClassName("message"); michael@0: let searchString = this.filterBox.value; michael@0: michael@0: for (let i = 0, n = nodes.length; i < n; ++i) { michael@0: let node = nodes[i]; michael@0: michael@0: // hide nodes that match the strings michael@0: let text = node.textContent; michael@0: michael@0: // if the text matches the words in aSearchString... michael@0: if (this.stringMatchesFilters(text, searchString)) { michael@0: node.classList.remove("filtered-by-string"); michael@0: } michael@0: else { michael@0: node.classList.add("filtered-by-string"); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Applies the user's filters to a newly-created message node via CSS michael@0: * classes. michael@0: * michael@0: * @param nsIDOMNode aNode michael@0: * The newly-created message node. michael@0: * @return boolean michael@0: * True if the message was filtered or false otherwise. michael@0: */ michael@0: filterMessageNode: function WCF_filterMessageNode(aNode) michael@0: { michael@0: let isFiltered = false; michael@0: michael@0: // Filter by the message type. michael@0: let prefKey = MESSAGE_PREFERENCE_KEYS[aNode.category][aNode.severity]; michael@0: if (prefKey && !this.getFilterState(prefKey)) { michael@0: // The node is filtered by type. michael@0: aNode.classList.add("filtered-by-type"); michael@0: isFiltered = true; michael@0: } michael@0: michael@0: // Filter on the search string. michael@0: let search = this.filterBox.value; michael@0: let text = aNode.clipboardText; michael@0: michael@0: // if string matches the filter text michael@0: if (!this.stringMatchesFilters(text, search)) { michael@0: aNode.classList.add("filtered-by-string"); michael@0: isFiltered = true; michael@0: } michael@0: michael@0: if (isFiltered && aNode.classList.contains("inlined-variables-view")) { michael@0: aNode.classList.add("hidden-message"); michael@0: } michael@0: michael@0: return isFiltered; michael@0: }, michael@0: michael@0: /** michael@0: * Merge the attributes of the two nodes that are about to be filtered. michael@0: * Increment the number of repeats of aOriginal. michael@0: * michael@0: * @param nsIDOMNode aOriginal michael@0: * The Original Node. The one being merged into. michael@0: * @param nsIDOMNode aFiltered michael@0: * The node being filtered out because it is repeated. michael@0: */ michael@0: mergeFilteredMessageNode: michael@0: function WCF_mergeFilteredMessageNode(aOriginal, aFiltered) michael@0: { michael@0: let repeatNode = aOriginal.getElementsByClassName("message-repeats")[0]; michael@0: if (!repeatNode) { michael@0: return; // no repeat node, return early. michael@0: } michael@0: michael@0: let occurrences = parseInt(repeatNode.getAttribute("value")) + 1; michael@0: repeatNode.setAttribute("value", occurrences); michael@0: repeatNode.textContent = occurrences; michael@0: let str = l10n.getStr("messageRepeats.tooltip2"); michael@0: repeatNode.title = PluralForm.get(occurrences, str) michael@0: .replace("#1", occurrences); michael@0: }, michael@0: michael@0: /** michael@0: * Filter the message node from the output if it is a repeat. michael@0: * michael@0: * @private michael@0: * @param nsIDOMNode aNode michael@0: * The message node to be filtered or not. michael@0: * @returns nsIDOMNode|null michael@0: * Returns the duplicate node if the message was filtered, null michael@0: * otherwise. michael@0: */ michael@0: _filterRepeatedMessage: function WCF__filterRepeatedMessage(aNode) michael@0: { michael@0: let repeatNode = aNode.getElementsByClassName("message-repeats")[0]; michael@0: if (!repeatNode) { michael@0: return null; michael@0: } michael@0: michael@0: let uid = repeatNode._uid; michael@0: let dupeNode = null; michael@0: michael@0: if (aNode.category == CATEGORY_CSS || michael@0: aNode.category == CATEGORY_SECURITY) { michael@0: dupeNode = this._repeatNodes[uid]; michael@0: if (!dupeNode) { michael@0: this._repeatNodes[uid] = aNode; michael@0: } michael@0: } michael@0: else if ((aNode.category == CATEGORY_WEBDEV || michael@0: aNode.category == CATEGORY_JS) && michael@0: aNode.category != CATEGORY_NETWORK && michael@0: !aNode.classList.contains("inlined-variables-view")) { michael@0: let lastMessage = this.outputNode.lastChild; michael@0: if (!lastMessage) { michael@0: return null; michael@0: } michael@0: michael@0: let lastRepeatNode = lastMessage.getElementsByClassName("message-repeats")[0]; michael@0: if (lastRepeatNode && lastRepeatNode._uid == uid) { michael@0: dupeNode = lastMessage; michael@0: } michael@0: } michael@0: michael@0: if (dupeNode) { michael@0: this.mergeFilteredMessageNode(dupeNode, aNode); michael@0: return dupeNode; michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Display cached messages that may have been collected before the UI is michael@0: * displayed. michael@0: * michael@0: * @param array aRemoteMessages michael@0: * Array of cached messages coming from the remote Web Console michael@0: * content instance. michael@0: */ michael@0: displayCachedMessages: function WCF_displayCachedMessages(aRemoteMessages) michael@0: { michael@0: if (!aRemoteMessages.length) { michael@0: return; michael@0: } michael@0: michael@0: aRemoteMessages.forEach(function(aMessage) { michael@0: switch (aMessage._type) { michael@0: case "PageError": { michael@0: let category = Utils.categoryForScriptError(aMessage); michael@0: this.outputMessage(category, this.reportPageError, michael@0: [category, aMessage]); michael@0: break; michael@0: } michael@0: case "LogMessage": michael@0: this.handleLogMessage(aMessage); michael@0: break; michael@0: case "ConsoleAPI": michael@0: this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, michael@0: [aMessage]); michael@0: break; michael@0: } michael@0: }, this); michael@0: }, michael@0: michael@0: /** michael@0: * Logs a message to the Web Console that originates from the Web Console michael@0: * server. michael@0: * michael@0: * @param object aMessage michael@0: * The message received from the server. michael@0: * @return nsIDOMElement|null michael@0: * The message element to display in the Web Console output. michael@0: */ michael@0: logConsoleAPIMessage: function WCF_logConsoleAPIMessage(aMessage) michael@0: { michael@0: let body = null; michael@0: let clipboardText = null; michael@0: let sourceURL = aMessage.filename; michael@0: let sourceLine = aMessage.lineNumber; michael@0: let level = aMessage.level; michael@0: let args = aMessage.arguments; michael@0: let objectActors = new Set(); michael@0: let node = null; michael@0: michael@0: // Gather the actor IDs. michael@0: args.forEach((aValue) => { michael@0: if (WebConsoleUtils.isActorGrip(aValue)) { michael@0: objectActors.add(aValue.actor); michael@0: } michael@0: }); michael@0: michael@0: switch (level) { michael@0: case "log": michael@0: case "info": michael@0: case "warn": michael@0: case "error": michael@0: case "exception": michael@0: case "assert": michael@0: case "debug": { michael@0: let msg = new Messages.ConsoleGeneric(aMessage); michael@0: node = msg.init(this.output).render().element; michael@0: break; michael@0: } michael@0: case "trace": { michael@0: let msg = new Messages.ConsoleTrace(aMessage); michael@0: node = msg.init(this.output).render().element; michael@0: break; michael@0: } michael@0: case "dir": { michael@0: body = { arguments: args }; michael@0: let clipboardArray = []; michael@0: args.forEach((aValue) => { michael@0: clipboardArray.push(VariablesView.getString(aValue)); michael@0: }); michael@0: clipboardText = clipboardArray.join(" "); michael@0: break; michael@0: } michael@0: michael@0: case "group": michael@0: case "groupCollapsed": michael@0: clipboardText = body = aMessage.groupName; michael@0: this.groupDepth++; michael@0: break; michael@0: michael@0: case "groupEnd": michael@0: if (this.groupDepth > 0) { michael@0: this.groupDepth--; michael@0: } michael@0: break; michael@0: michael@0: case "time": { michael@0: let timer = aMessage.timer; michael@0: if (!timer) { michael@0: return null; michael@0: } michael@0: if (timer.error) { michael@0: Cu.reportError(l10n.getStr(timer.error)); michael@0: return null; michael@0: } michael@0: body = l10n.getFormatStr("timerStarted", [timer.name]); michael@0: clipboardText = body; michael@0: break; michael@0: } michael@0: michael@0: case "timeEnd": { michael@0: let timer = aMessage.timer; michael@0: if (!timer) { michael@0: return null; michael@0: } michael@0: let duration = Math.round(timer.duration * 100) / 100; michael@0: body = l10n.getFormatStr("timeEnd", [timer.name, duration]); michael@0: clipboardText = body; michael@0: break; michael@0: } michael@0: michael@0: case "count": { michael@0: let counter = aMessage.counter; michael@0: if (!counter) { michael@0: return null; michael@0: } michael@0: if (counter.error) { michael@0: Cu.reportError(l10n.getStr(counter.error)); michael@0: return null; michael@0: } michael@0: let msg = new Messages.ConsoleGeneric(aMessage); michael@0: node = msg.init(this.output).render().element; michael@0: break; michael@0: } michael@0: michael@0: default: michael@0: Cu.reportError("Unknown Console API log level: " + level); michael@0: return null; michael@0: } michael@0: michael@0: // Release object actors for arguments coming from console API methods that michael@0: // we ignore their arguments. michael@0: switch (level) { michael@0: case "group": michael@0: case "groupCollapsed": michael@0: case "groupEnd": michael@0: case "time": michael@0: case "timeEnd": michael@0: case "count": michael@0: for (let actor of objectActors) { michael@0: this._releaseObject(actor); michael@0: } michael@0: objectActors.clear(); michael@0: } michael@0: michael@0: if (level == "groupEnd") { michael@0: return null; // no need to continue michael@0: } michael@0: michael@0: if (!node) { michael@0: node = this.createMessageNode(CATEGORY_WEBDEV, LEVELS[level], body, michael@0: sourceURL, sourceLine, clipboardText, michael@0: level, aMessage.timeStamp); michael@0: if (aMessage.private) { michael@0: node.setAttribute("private", true); michael@0: } michael@0: } michael@0: michael@0: if (objectActors.size > 0) { michael@0: node._objectActors = objectActors; michael@0: michael@0: if (!node._messageObject) { michael@0: let repeatNode = node.getElementsByClassName("message-repeats")[0]; michael@0: repeatNode._uid += [...objectActors].join("-"); michael@0: } michael@0: } michael@0: michael@0: return node; michael@0: }, michael@0: michael@0: /** michael@0: * Handle ConsoleAPICall objects received from the server. This method outputs michael@0: * the window.console API call. michael@0: * michael@0: * @param object aMessage michael@0: * The console API message received from the server. michael@0: */ michael@0: handleConsoleAPICall: function WCF_handleConsoleAPICall(aMessage) michael@0: { michael@0: this.outputMessage(CATEGORY_WEBDEV, this.logConsoleAPIMessage, [aMessage]); michael@0: }, michael@0: michael@0: /** michael@0: * Reports an error in the page source, either JavaScript or CSS. michael@0: * michael@0: * @param nsIScriptError aScriptError michael@0: * The error message to report. michael@0: * @return nsIDOMElement|undefined michael@0: * The message element to display in the Web Console output. michael@0: */ michael@0: reportPageError: function WCF_reportPageError(aCategory, aScriptError) michael@0: { michael@0: // Warnings and legacy strict errors become warnings; other types become michael@0: // errors. michael@0: let severity = SEVERITY_ERROR; michael@0: if (aScriptError.warning || aScriptError.strict) { michael@0: severity = SEVERITY_WARNING; michael@0: } michael@0: michael@0: let objectActors = new Set(); michael@0: michael@0: // Gather the actor IDs. michael@0: for (let prop of ["errorMessage", "lineText"]) { michael@0: let grip = aScriptError[prop]; michael@0: if (WebConsoleUtils.isActorGrip(grip)) { michael@0: objectActors.add(grip.actor); michael@0: } michael@0: } michael@0: michael@0: let errorMessage = aScriptError.errorMessage; michael@0: if (errorMessage.type && errorMessage.type == "longString") { michael@0: errorMessage = errorMessage.initial; michael@0: } michael@0: michael@0: let node = this.createMessageNode(aCategory, severity, michael@0: errorMessage, michael@0: aScriptError.sourceName, michael@0: aScriptError.lineNumber, null, null, michael@0: aScriptError.timeStamp); michael@0: michael@0: // Select the body of the message node that is displayed in the console michael@0: let msgBody = node.getElementsByClassName("message-body")[0]; michael@0: // Add the more info link node to messages that belong to certain categories michael@0: this.addMoreInfoLink(msgBody, aScriptError); michael@0: michael@0: if (aScriptError.private) { michael@0: node.setAttribute("private", true); michael@0: } michael@0: michael@0: if (objectActors.size > 0) { michael@0: node._objectActors = objectActors; michael@0: } michael@0: michael@0: return node; michael@0: }, michael@0: michael@0: /** michael@0: * Handle PageError objects received from the server. This method outputs the michael@0: * given error. michael@0: * michael@0: * @param nsIScriptError aPageError michael@0: * The error received from the server. michael@0: */ michael@0: handlePageError: function WCF_handlePageError(aPageError) michael@0: { michael@0: let category = Utils.categoryForScriptError(aPageError); michael@0: this.outputMessage(category, this.reportPageError, [category, aPageError]); michael@0: }, michael@0: michael@0: /** michael@0: * Handle log messages received from the server. This method outputs the given michael@0: * message. michael@0: * michael@0: * @param object aPacket michael@0: * The message packet received from the server. michael@0: */ michael@0: handleLogMessage: function WCF_handleLogMessage(aPacket) michael@0: { michael@0: if (aPacket.message) { michael@0: this.outputMessage(CATEGORY_JS, this._reportLogMessage, [aPacket]); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Display log messages received from the server. michael@0: * michael@0: * @private michael@0: * @param object aPacket michael@0: * The message packet received from the server. michael@0: * @return nsIDOMElement michael@0: * The message element to render for the given log message. michael@0: */ michael@0: _reportLogMessage: function WCF__reportLogMessage(aPacket) michael@0: { michael@0: let msg = aPacket.message; michael@0: if (msg.type && msg.type == "longString") { michael@0: msg = msg.initial; michael@0: } michael@0: let node = this.createMessageNode(CATEGORY_JS, SEVERITY_LOG, msg, null, michael@0: null, null, null, aPacket.timeStamp); michael@0: if (WebConsoleUtils.isActorGrip(aPacket.message)) { michael@0: node._objectActors = new Set([aPacket.message.actor]); michael@0: } michael@0: return node; michael@0: }, michael@0: michael@0: /** michael@0: * Log network event. michael@0: * michael@0: * @param object aActorId michael@0: * The network event actor ID to log. michael@0: * @return nsIDOMElement|null michael@0: * The message element to display in the Web Console output. michael@0: */ michael@0: logNetEvent: function WCF_logNetEvent(aActorId) michael@0: { michael@0: let networkInfo = this._networkRequests[aActorId]; michael@0: if (!networkInfo) { michael@0: return null; michael@0: } michael@0: michael@0: let request = networkInfo.request; michael@0: let clipboardText = request.method + " " + request.url; michael@0: let severity = SEVERITY_LOG; michael@0: let mixedRequest = michael@0: WebConsoleUtils.isMixedHTTPSRequest(request.url, this.contentLocation); michael@0: if (mixedRequest) { michael@0: severity = SEVERITY_WARNING; michael@0: } michael@0: michael@0: let methodNode = this.document.createElementNS(XHTML_NS, "span"); michael@0: methodNode.className = "method"; michael@0: methodNode.textContent = request.method + " "; michael@0: michael@0: let messageNode = this.createMessageNode(CATEGORY_NETWORK, severity, michael@0: methodNode, null, null, michael@0: clipboardText); michael@0: if (networkInfo.private) { michael@0: messageNode.setAttribute("private", true); michael@0: } michael@0: messageNode._connectionId = aActorId; michael@0: messageNode.url = request.url; michael@0: michael@0: let body = methodNode.parentNode; michael@0: body.setAttribute("aria-haspopup", true); michael@0: michael@0: let displayUrl = request.url; michael@0: let pos = displayUrl.indexOf("?"); michael@0: if (pos > -1) { michael@0: displayUrl = displayUrl.substr(0, pos); michael@0: } michael@0: michael@0: let urlNode = this.document.createElementNS(XHTML_NS, "a"); michael@0: urlNode.className = "url"; michael@0: urlNode.setAttribute("title", request.url); michael@0: urlNode.href = request.url; michael@0: urlNode.textContent = displayUrl; michael@0: urlNode.draggable = false; michael@0: body.appendChild(urlNode); michael@0: body.appendChild(this.document.createTextNode(" ")); michael@0: michael@0: if (mixedRequest) { michael@0: messageNode.classList.add("mixed-content"); michael@0: this.makeMixedContentNode(body); michael@0: } michael@0: michael@0: let statusNode = this.document.createElementNS(XHTML_NS, "a"); michael@0: statusNode.className = "status"; michael@0: body.appendChild(statusNode); michael@0: michael@0: let onClick = () => { michael@0: if (!messageNode._panelOpen) { michael@0: this.openNetworkPanel(messageNode, networkInfo); michael@0: } michael@0: }; michael@0: michael@0: this._addMessageLinkCallback(urlNode, onClick); michael@0: this._addMessageLinkCallback(statusNode, onClick); michael@0: michael@0: networkInfo.node = messageNode; michael@0: michael@0: this._updateNetMessage(aActorId); michael@0: michael@0: return messageNode; michael@0: }, michael@0: michael@0: /** michael@0: * Create a mixed content warning Node. michael@0: * michael@0: * @param aLinkNode michael@0: * Parent to the requested urlNode. michael@0: */ michael@0: makeMixedContentNode: function WCF_makeMixedContentNode(aLinkNode) michael@0: { michael@0: let mixedContentWarning = "[" + l10n.getStr("webConsoleMixedContentWarning") + "]"; michael@0: michael@0: // Mixed content warning message links to a Learn More page michael@0: let mixedContentWarningNode = this.document.createElementNS(XHTML_NS, "a"); michael@0: mixedContentWarningNode.title = MIXED_CONTENT_LEARN_MORE; michael@0: mixedContentWarningNode.href = MIXED_CONTENT_LEARN_MORE; michael@0: mixedContentWarningNode.className = "learn-more-link"; michael@0: mixedContentWarningNode.textContent = mixedContentWarning; michael@0: mixedContentWarningNode.draggable = false; michael@0: michael@0: aLinkNode.appendChild(mixedContentWarningNode); michael@0: michael@0: this._addMessageLinkCallback(mixedContentWarningNode, (aEvent) => { michael@0: aEvent.stopPropagation(); michael@0: this.owner.openLink(MIXED_CONTENT_LEARN_MORE); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Adds a more info link node to messages based on the nsIScriptError object michael@0: * that we need to report to the console michael@0: * michael@0: * @param aNode michael@0: * The node to which we will be adding the more info link node michael@0: * @param aScriptError michael@0: * The script error object that we are reporting to the console michael@0: */ michael@0: addMoreInfoLink: function WCF_addMoreInfoLink(aNode, aScriptError) michael@0: { michael@0: let url; michael@0: switch (aScriptError.category) { michael@0: case "Insecure Password Field": michael@0: url = INSECURE_PASSWORDS_LEARN_MORE; michael@0: break; michael@0: case "Mixed Content Message": michael@0: case "Mixed Content Blocker": michael@0: url = MIXED_CONTENT_LEARN_MORE; michael@0: break; michael@0: case "Invalid HSTS Headers": michael@0: url = STRICT_TRANSPORT_SECURITY_LEARN_MORE; michael@0: break; michael@0: default: michael@0: // Unknown category. Return without adding more info node. michael@0: return; michael@0: } michael@0: michael@0: this.addLearnMoreWarningNode(aNode, url); michael@0: }, michael@0: michael@0: /* michael@0: * Appends a clickable warning node to the node passed michael@0: * as a parameter to the function. When a user clicks on the appended michael@0: * warning node, the browser navigates to the provided url. michael@0: * michael@0: * @param aNode michael@0: * The node to which we will be adding a clickable warning node. michael@0: * @param aURL michael@0: * The url which points to the page where the user can learn more michael@0: * about security issues associated with the specific message that's michael@0: * being logged. michael@0: */ michael@0: addLearnMoreWarningNode: michael@0: function WCF_addLearnMoreWarningNode(aNode, aURL) michael@0: { michael@0: let moreInfoLabel = "[" + l10n.getStr("webConsoleMoreInfoLabel") + "]"; michael@0: michael@0: let warningNode = this.document.createElementNS(XHTML_NS, "a"); michael@0: warningNode.title = aURL; michael@0: warningNode.href = aURL; michael@0: warningNode.draggable = false; michael@0: warningNode.textContent = moreInfoLabel; michael@0: warningNode.className = "learn-more-link"; michael@0: michael@0: this._addMessageLinkCallback(warningNode, (aEvent) => { michael@0: aEvent.stopPropagation(); michael@0: this.owner.openLink(aURL); michael@0: }); michael@0: michael@0: aNode.appendChild(warningNode); michael@0: }, michael@0: michael@0: /** michael@0: * Log file activity. michael@0: * michael@0: * @param string aFileURI michael@0: * The file URI that was loaded. michael@0: * @return nsIDOMElement|undefined michael@0: * The message element to display in the Web Console output. michael@0: */ michael@0: logFileActivity: function WCF_logFileActivity(aFileURI) michael@0: { michael@0: let urlNode = this.document.createElementNS(XHTML_NS, "a"); michael@0: urlNode.setAttribute("title", aFileURI); michael@0: urlNode.className = "url"; michael@0: urlNode.textContent = aFileURI; michael@0: urlNode.draggable = false; michael@0: urlNode.href = aFileURI; michael@0: michael@0: let outputNode = this.createMessageNode(CATEGORY_NETWORK, SEVERITY_LOG, michael@0: urlNode, null, null, aFileURI); michael@0: michael@0: this._addMessageLinkCallback(urlNode, () => { michael@0: this.owner.viewSource(aFileURI); michael@0: }); michael@0: michael@0: return outputNode; michael@0: }, michael@0: michael@0: /** michael@0: * Handle the file activity messages coming from the remote Web Console. michael@0: * michael@0: * @param string aFileURI michael@0: * The file URI that was requested. michael@0: */ michael@0: handleFileActivity: function WCF_handleFileActivity(aFileURI) michael@0: { michael@0: this.outputMessage(CATEGORY_NETWORK, this.logFileActivity, [aFileURI]); michael@0: }, michael@0: michael@0: /** michael@0: * Handle the reflow activity messages coming from the remote Web Console. michael@0: * michael@0: * @param object aMessage michael@0: * An object holding information about a reflow batch. michael@0: */ michael@0: logReflowActivity: function WCF_logReflowActivity(aMessage) michael@0: { michael@0: let {start, end, sourceURL, sourceLine} = aMessage; michael@0: let duration = Math.round((end - start) * 100) / 100; michael@0: let node = this.document.createElementNS(XHTML_NS, "span"); michael@0: if (sourceURL) { michael@0: node.textContent = l10n.getFormatStr("reflow.messageWithLink", [duration]); michael@0: let a = this.document.createElementNS(XHTML_NS, "a"); michael@0: a.href = "#"; michael@0: a.draggable = "false"; michael@0: let filename = WebConsoleUtils.abbreviateSourceURL(sourceURL); michael@0: let functionName = aMessage.functionName || l10n.getStr("stacktrace.anonymousFunction"); michael@0: a.textContent = l10n.getFormatStr("reflow.messageLinkText", michael@0: [functionName, filename, sourceLine]); michael@0: this._addMessageLinkCallback(a, () => { michael@0: this.owner.viewSourceInDebugger(sourceURL, sourceLine); michael@0: }); michael@0: node.appendChild(a); michael@0: } else { michael@0: node.textContent = l10n.getFormatStr("reflow.messageWithNoLink", [duration]); michael@0: } michael@0: return this.createMessageNode(CATEGORY_CSS, SEVERITY_LOG, node); michael@0: }, michael@0: michael@0: michael@0: handleReflowActivity: function WCF_handleReflowActivity(aMessage) michael@0: { michael@0: this.outputMessage(CATEGORY_CSS, this.logReflowActivity, [aMessage]); michael@0: }, michael@0: michael@0: /** michael@0: * Inform user that the window.console API has been replaced by a script michael@0: * in a content page. michael@0: */ michael@0: logWarningAboutReplacedAPI: function WCF_logWarningAboutReplacedAPI() michael@0: { michael@0: let node = this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING, michael@0: l10n.getStr("ConsoleAPIDisabled")); michael@0: this.outputMessage(CATEGORY_JS, node); michael@0: }, michael@0: michael@0: /** michael@0: * Handle the network events coming from the remote Web Console. michael@0: * michael@0: * @param object aActor michael@0: * The NetworkEventActor grip. michael@0: */ michael@0: handleNetworkEvent: function WCF_handleNetworkEvent(aActor) michael@0: { michael@0: let networkInfo = { michael@0: node: null, michael@0: actor: aActor.actor, michael@0: discardRequestBody: true, michael@0: discardResponseBody: true, michael@0: startedDateTime: aActor.startedDateTime, michael@0: request: { michael@0: url: aActor.url, michael@0: method: aActor.method, michael@0: }, michael@0: response: {}, michael@0: timings: {}, michael@0: updates: [], // track the list of network event updates michael@0: private: aActor.private, michael@0: }; michael@0: michael@0: this._networkRequests[aActor.actor] = networkInfo; michael@0: this.outputMessage(CATEGORY_NETWORK, this.logNetEvent, [aActor.actor]); michael@0: }, michael@0: michael@0: /** michael@0: * Handle network event updates coming from the server. michael@0: * michael@0: * @param string aActorId michael@0: * The network event actor ID. michael@0: * @param string aType michael@0: * Update type. michael@0: * @param object aPacket michael@0: * Update details. michael@0: */ michael@0: handleNetworkEventUpdate: michael@0: function WCF_handleNetworkEventUpdate(aActorId, aType, aPacket) michael@0: { michael@0: let networkInfo = this._networkRequests[aActorId]; michael@0: if (!networkInfo) { michael@0: return; michael@0: } michael@0: michael@0: networkInfo.updates.push(aType); michael@0: michael@0: switch (aType) { michael@0: case "requestHeaders": michael@0: networkInfo.request.headersSize = aPacket.headersSize; michael@0: break; michael@0: case "requestPostData": michael@0: networkInfo.discardRequestBody = aPacket.discardRequestBody; michael@0: networkInfo.request.bodySize = aPacket.dataSize; michael@0: break; michael@0: case "responseStart": michael@0: networkInfo.response.httpVersion = aPacket.response.httpVersion; michael@0: networkInfo.response.status = aPacket.response.status; michael@0: networkInfo.response.statusText = aPacket.response.statusText; michael@0: networkInfo.response.headersSize = aPacket.response.headersSize; michael@0: networkInfo.discardResponseBody = aPacket.response.discardResponseBody; michael@0: break; michael@0: case "responseContent": michael@0: networkInfo.response.content = { michael@0: mimeType: aPacket.mimeType, michael@0: }; michael@0: networkInfo.response.bodySize = aPacket.contentSize; michael@0: networkInfo.discardResponseBody = aPacket.discardResponseBody; michael@0: break; michael@0: case "eventTimings": michael@0: networkInfo.totalTime = aPacket.totalTime; michael@0: break; michael@0: } michael@0: michael@0: if (networkInfo.node && this._updateNetMessage(aActorId)) { michael@0: this.emit("messages-updated", new Set([networkInfo.node])); michael@0: } michael@0: michael@0: // For unit tests we pass the HTTP activity object to the test callback, michael@0: // once requests complete. michael@0: if (this.owner.lastFinishedRequestCallback && michael@0: networkInfo.updates.indexOf("responseContent") > -1 && michael@0: networkInfo.updates.indexOf("eventTimings") > -1) { michael@0: this.owner.lastFinishedRequestCallback(networkInfo, this); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Update an output message to reflect the latest state of a network request, michael@0: * given a network event actor ID. michael@0: * michael@0: * @private michael@0: * @param string aActorId michael@0: * The network event actor ID for which you want to update the message. michael@0: * @return boolean michael@0: * |true| if the message node was updated, or |false| otherwise. michael@0: */ michael@0: _updateNetMessage: function WCF__updateNetMessage(aActorId) michael@0: { michael@0: let networkInfo = this._networkRequests[aActorId]; michael@0: if (!networkInfo || !networkInfo.node) { michael@0: return; michael@0: } michael@0: michael@0: let messageNode = networkInfo.node; michael@0: let updates = networkInfo.updates; michael@0: let hasEventTimings = updates.indexOf("eventTimings") > -1; michael@0: let hasResponseStart = updates.indexOf("responseStart") > -1; michael@0: let request = networkInfo.request; michael@0: let response = networkInfo.response; michael@0: let updated = false; michael@0: michael@0: if (hasEventTimings || hasResponseStart) { michael@0: let status = []; michael@0: if (response.httpVersion && response.status) { michael@0: status = [response.httpVersion, response.status, response.statusText]; michael@0: } michael@0: if (hasEventTimings) { michael@0: status.push(l10n.getFormatStr("NetworkPanel.durationMS", michael@0: [networkInfo.totalTime])); michael@0: } michael@0: let statusText = "[" + status.join(" ") + "]"; michael@0: michael@0: let statusNode = messageNode.getElementsByClassName("status")[0]; michael@0: statusNode.textContent = statusText; michael@0: michael@0: messageNode.clipboardText = [request.method, request.url, statusText] michael@0: .join(" "); michael@0: michael@0: if (hasResponseStart && response.status >= MIN_HTTP_ERROR_CODE && michael@0: response.status <= MAX_HTTP_ERROR_CODE) { michael@0: this.setMessageType(messageNode, CATEGORY_NETWORK, SEVERITY_ERROR); michael@0: } michael@0: michael@0: updated = true; michael@0: } michael@0: michael@0: if (messageNode._netPanel) { michael@0: messageNode._netPanel.update(); michael@0: } michael@0: michael@0: return updated; michael@0: }, michael@0: michael@0: /** michael@0: * Opens a NetworkPanel. michael@0: * michael@0: * @param nsIDOMNode aNode michael@0: * The message node you want the panel to be anchored to. michael@0: * @param object aHttpActivity michael@0: * The HTTP activity object that holds network request and response michael@0: * information. This object is given to the NetworkPanel constructor. michael@0: * @return object michael@0: * The new NetworkPanel instance. michael@0: */ michael@0: openNetworkPanel: function WCF_openNetworkPanel(aNode, aHttpActivity) michael@0: { michael@0: let actor = aHttpActivity.actor; michael@0: michael@0: if (actor) { michael@0: this.webConsoleClient.getRequestHeaders(actor, function(aResponse) { michael@0: if (aResponse.error) { michael@0: Cu.reportError("WCF_openNetworkPanel getRequestHeaders:" + michael@0: aResponse.error); michael@0: return; michael@0: } michael@0: michael@0: aHttpActivity.request.headers = aResponse.headers; michael@0: michael@0: this.webConsoleClient.getRequestCookies(actor, onRequestCookies); michael@0: }.bind(this)); michael@0: } michael@0: michael@0: let onRequestCookies = function(aResponse) { michael@0: if (aResponse.error) { michael@0: Cu.reportError("WCF_openNetworkPanel getRequestCookies:" + michael@0: aResponse.error); michael@0: return; michael@0: } michael@0: michael@0: aHttpActivity.request.cookies = aResponse.cookies; michael@0: michael@0: this.webConsoleClient.getResponseHeaders(actor, onResponseHeaders); michael@0: }.bind(this); michael@0: michael@0: let onResponseHeaders = function(aResponse) { michael@0: if (aResponse.error) { michael@0: Cu.reportError("WCF_openNetworkPanel getResponseHeaders:" + michael@0: aResponse.error); michael@0: return; michael@0: } michael@0: michael@0: aHttpActivity.response.headers = aResponse.headers; michael@0: michael@0: this.webConsoleClient.getResponseCookies(actor, onResponseCookies); michael@0: }.bind(this); michael@0: michael@0: let onResponseCookies = function(aResponse) { michael@0: if (aResponse.error) { michael@0: Cu.reportError("WCF_openNetworkPanel getResponseCookies:" + michael@0: aResponse.error); michael@0: return; michael@0: } michael@0: michael@0: aHttpActivity.response.cookies = aResponse.cookies; michael@0: michael@0: this.webConsoleClient.getRequestPostData(actor, onRequestPostData); michael@0: }.bind(this); michael@0: michael@0: let onRequestPostData = function(aResponse) { michael@0: if (aResponse.error) { michael@0: Cu.reportError("WCF_openNetworkPanel getRequestPostData:" + michael@0: aResponse.error); michael@0: return; michael@0: } michael@0: michael@0: aHttpActivity.request.postData = aResponse.postData; michael@0: aHttpActivity.discardRequestBody = aResponse.postDataDiscarded; michael@0: michael@0: this.webConsoleClient.getResponseContent(actor, onResponseContent); michael@0: }.bind(this); michael@0: michael@0: let onResponseContent = function(aResponse) { michael@0: if (aResponse.error) { michael@0: Cu.reportError("WCF_openNetworkPanel getResponseContent:" + michael@0: aResponse.error); michael@0: return; michael@0: } michael@0: michael@0: aHttpActivity.response.content = aResponse.content; michael@0: aHttpActivity.discardResponseBody = aResponse.contentDiscarded; michael@0: michael@0: this.webConsoleClient.getEventTimings(actor, onEventTimings); michael@0: }.bind(this); michael@0: michael@0: let onEventTimings = function(aResponse) { michael@0: if (aResponse.error) { michael@0: Cu.reportError("WCF_openNetworkPanel getEventTimings:" + michael@0: aResponse.error); michael@0: return; michael@0: } michael@0: michael@0: aHttpActivity.timings = aResponse.timings; michael@0: michael@0: openPanel(); michael@0: }.bind(this); michael@0: michael@0: let openPanel = function() { michael@0: aNode._netPanel = netPanel; michael@0: michael@0: let panel = netPanel.panel; michael@0: panel.openPopup(aNode, "after_pointer", 0, 0, false, false); michael@0: panel.sizeTo(450, 500); michael@0: panel.setAttribute("hudId", this.hudId); michael@0: michael@0: panel.addEventListener("popuphiding", function WCF_netPanel_onHide() { michael@0: panel.removeEventListener("popuphiding", WCF_netPanel_onHide); michael@0: michael@0: aNode._panelOpen = false; michael@0: aNode._netPanel = null; michael@0: }); michael@0: michael@0: aNode._panelOpen = true; michael@0: }.bind(this); michael@0: michael@0: let netPanel = new NetworkPanel(this.popupset, aHttpActivity, this); michael@0: netPanel.linkNode = aNode; michael@0: michael@0: if (!actor) { michael@0: openPanel(); michael@0: } michael@0: michael@0: return netPanel; michael@0: }, michael@0: michael@0: /** michael@0: * Handler for page location changes. michael@0: * michael@0: * @param string aURI michael@0: * New page location. michael@0: * @param string aTitle michael@0: * New page title. michael@0: */ michael@0: onLocationChange: function WCF_onLocationChange(aURI, aTitle) michael@0: { michael@0: this.contentLocation = aURI; michael@0: if (this.owner.onLocationChange) { michael@0: this.owner.onLocationChange(aURI, aTitle); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the tabNavigated notification. michael@0: * michael@0: * @param string aEvent michael@0: * Event name. michael@0: * @param object aPacket michael@0: * Notification packet received from the server. michael@0: */ michael@0: handleTabNavigated: function WCF_handleTabNavigated(aEvent, aPacket) michael@0: { michael@0: if (aEvent == "will-navigate") { michael@0: if (this.persistLog) { michael@0: let marker = new Messages.NavigationMarker(aPacket.url, Date.now()); michael@0: this.output.addMessage(marker); michael@0: } michael@0: else { michael@0: this.jsterm.clearOutput(); michael@0: } michael@0: } michael@0: michael@0: if (aPacket.url) { michael@0: this.onLocationChange(aPacket.url, aPacket.title); michael@0: } michael@0: michael@0: if (aEvent == "navigate" && !aPacket.nativeConsoleAPI) { michael@0: this.logWarningAboutReplacedAPI(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Output a message node. This filters a node appropriately, then sends it to michael@0: * the output, regrouping and pruning output as necessary. michael@0: * michael@0: * Note: this call is async - the given message node may not be displayed when michael@0: * you call this method. michael@0: * michael@0: * @param integer aCategory michael@0: * The category of the message you want to output. See the CATEGORY_* michael@0: * constants. michael@0: * @param function|nsIDOMElement aMethodOrNode michael@0: * The method that creates the message element to send to the output or michael@0: * the actual element. If a method is given it will be bound to the HUD michael@0: * object and the arguments will be |aArguments|. michael@0: * @param array [aArguments] michael@0: * If a method is given to output the message element then the method michael@0: * will be invoked with the list of arguments given here. michael@0: */ michael@0: outputMessage: function WCF_outputMessage(aCategory, aMethodOrNode, aArguments) michael@0: { michael@0: if (!this._outputQueue.length) { michael@0: // If the queue is empty we consider that now was the last output flush. michael@0: // This avoid an immediate output flush when the timer executes. michael@0: this._lastOutputFlush = Date.now(); michael@0: } michael@0: michael@0: this._outputQueue.push([aCategory, aMethodOrNode, aArguments]); michael@0: michael@0: if (!this._outputTimerInitialized) { michael@0: this._initOutputTimer(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Try to flush the output message queue. This takes the messages in the michael@0: * output queue and displays them. Outputting stops at MESSAGES_IN_INTERVAL. michael@0: * Further output is queued to happen later - see OUTPUT_INTERVAL. michael@0: * michael@0: * @private michael@0: */ michael@0: _flushMessageQueue: function WCF__flushMessageQueue() michael@0: { michael@0: if (!this._outputTimer) { michael@0: return; michael@0: } michael@0: michael@0: let timeSinceFlush = Date.now() - this._lastOutputFlush; michael@0: if (this._outputQueue.length > MESSAGES_IN_INTERVAL && michael@0: timeSinceFlush < THROTTLE_UPDATES) { michael@0: this._initOutputTimer(); michael@0: return; michael@0: } michael@0: michael@0: // Determine how many messages we can display now. michael@0: let toDisplay = Math.min(this._outputQueue.length, MESSAGES_IN_INTERVAL); michael@0: if (toDisplay < 1) { michael@0: this._outputTimerInitialized = false; michael@0: return; michael@0: } michael@0: michael@0: // Try to prune the message queue. michael@0: let shouldPrune = false; michael@0: if (this._outputQueue.length > toDisplay && this._pruneOutputQueue()) { michael@0: toDisplay = Math.min(this._outputQueue.length, toDisplay); michael@0: shouldPrune = true; michael@0: } michael@0: michael@0: let batch = this._outputQueue.splice(0, toDisplay); michael@0: if (!batch.length) { michael@0: this._outputTimerInitialized = false; michael@0: return; michael@0: } michael@0: michael@0: let outputNode = this.outputNode; michael@0: let lastVisibleNode = null; michael@0: let scrollNode = outputNode.parentNode; michael@0: let scrolledToBottom = Utils.isOutputScrolledToBottom(outputNode); michael@0: let hudIdSupportsString = WebConsoleUtils.supportsString(this.hudId); michael@0: michael@0: // Output the current batch of messages. michael@0: let newMessages = new Set(); michael@0: let updatedMessages = new Set(); michael@0: for (let item of batch) { michael@0: let result = this._outputMessageFromQueue(hudIdSupportsString, item); michael@0: if (result) { michael@0: if (result.isRepeated) { michael@0: updatedMessages.add(result.isRepeated); michael@0: } michael@0: else { michael@0: newMessages.add(result.node); michael@0: } michael@0: if (result.visible && result.node == this.outputNode.lastChild) { michael@0: lastVisibleNode = result.node; michael@0: } michael@0: } michael@0: } michael@0: michael@0: let oldScrollHeight = 0; michael@0: michael@0: // Prune messages if needed. We do not do this for every flush call to michael@0: // improve performance. michael@0: let removedNodes = 0; michael@0: if (shouldPrune || !this._outputQueue.length) { michael@0: oldScrollHeight = scrollNode.scrollHeight; michael@0: michael@0: let categories = Object.keys(this._pruneCategoriesQueue); michael@0: categories.forEach(function _pruneOutput(aCategory) { michael@0: removedNodes += this.pruneOutputIfNecessary(aCategory); michael@0: }, this); michael@0: this._pruneCategoriesQueue = {}; michael@0: } michael@0: michael@0: let isInputOutput = lastVisibleNode && michael@0: (lastVisibleNode.category == CATEGORY_INPUT || michael@0: lastVisibleNode.category == CATEGORY_OUTPUT); michael@0: michael@0: // Scroll to the new node if it is not filtered, and if the output node is michael@0: // scrolled at the bottom or if the new node is a jsterm input/output michael@0: // message. michael@0: if (lastVisibleNode && (scrolledToBottom || isInputOutput)) { michael@0: Utils.scrollToVisible(lastVisibleNode); michael@0: } michael@0: else if (!scrolledToBottom && removedNodes > 0 && michael@0: oldScrollHeight != scrollNode.scrollHeight) { michael@0: // If there were pruned messages and if scroll is not at the bottom, then michael@0: // we need to adjust the scroll location. michael@0: scrollNode.scrollTop -= oldScrollHeight - scrollNode.scrollHeight; michael@0: } michael@0: michael@0: if (newMessages.size) { michael@0: this.emit("messages-added", newMessages); michael@0: } michael@0: if (updatedMessages.size) { michael@0: this.emit("messages-updated", updatedMessages); michael@0: } michael@0: michael@0: // If the queue is not empty, schedule another flush. michael@0: if (this._outputQueue.length > 0) { michael@0: this._initOutputTimer(); michael@0: } michael@0: else { michael@0: this._outputTimerInitialized = false; michael@0: if (this._flushCallback && this._flushCallback() === false) { michael@0: this._flushCallback = null; michael@0: } michael@0: } michael@0: michael@0: this._lastOutputFlush = Date.now(); michael@0: }, michael@0: michael@0: /** michael@0: * Initialize the output timer. michael@0: * @private michael@0: */ michael@0: _initOutputTimer: function WCF__initOutputTimer() michael@0: { michael@0: if (!this._outputTimer) { michael@0: return; michael@0: } michael@0: michael@0: this._outputTimerInitialized = true; michael@0: this._outputTimer.initWithCallback(this._flushMessageQueue, michael@0: OUTPUT_INTERVAL, michael@0: Ci.nsITimer.TYPE_ONE_SHOT); michael@0: }, michael@0: michael@0: /** michael@0: * Output a message from the queue. michael@0: * michael@0: * @private michael@0: * @param nsISupportsString aHudIdSupportsString michael@0: * The HUD ID as an nsISupportsString. michael@0: * @param array aItem michael@0: * An item from the output queue - this item represents a message. michael@0: * @return object michael@0: * An object that holds the following properties: michael@0: * - node: the DOM element of the message. michael@0: * - isRepeated: the DOM element of the original message, if this is michael@0: * a repeated message, otherwise null. michael@0: * - visible: boolean that tells if the message is visible. michael@0: */ michael@0: _outputMessageFromQueue: michael@0: function WCF__outputMessageFromQueue(aHudIdSupportsString, aItem) michael@0: { michael@0: let [category, methodOrNode, args] = aItem; michael@0: michael@0: let node = typeof methodOrNode == "function" ? michael@0: methodOrNode.apply(this, args || []) : michael@0: methodOrNode; michael@0: if (!node) { michael@0: return null; michael@0: } michael@0: michael@0: let afterNode = node._outputAfterNode; michael@0: if (afterNode) { michael@0: delete node._outputAfterNode; michael@0: } michael@0: michael@0: let isFiltered = this.filterMessageNode(node); michael@0: michael@0: let isRepeated = this._filterRepeatedMessage(node); michael@0: michael@0: let visible = !isRepeated && !isFiltered; michael@0: if (!isRepeated) { michael@0: this.outputNode.insertBefore(node, michael@0: afterNode ? afterNode.nextSibling : null); michael@0: this._pruneCategoriesQueue[node.category] = true; michael@0: michael@0: let nodeID = node.getAttribute("id"); michael@0: Services.obs.notifyObservers(aHudIdSupportsString, michael@0: "web-console-message-created", nodeID); michael@0: michael@0: } michael@0: michael@0: if (node._onOutput) { michael@0: node._onOutput(); michael@0: delete node._onOutput; michael@0: } michael@0: michael@0: return { michael@0: visible: visible, michael@0: node: node, michael@0: isRepeated: isRepeated, michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Prune the queue of messages to display. This avoids displaying messages michael@0: * that will be removed at the end of the queue anyway. michael@0: * @private michael@0: */ michael@0: _pruneOutputQueue: function WCF__pruneOutputQueue() michael@0: { michael@0: let nodes = {}; michael@0: michael@0: // Group the messages per category. michael@0: this._outputQueue.forEach(function(aItem, aIndex) { michael@0: let [category] = aItem; michael@0: if (!(category in nodes)) { michael@0: nodes[category] = []; michael@0: } michael@0: nodes[category].push(aIndex); michael@0: }, this); michael@0: michael@0: let pruned = 0; michael@0: michael@0: // Loop through the categories we found and prune if needed. michael@0: for (let category in nodes) { michael@0: let limit = Utils.logLimitForCategory(category); michael@0: let indexes = nodes[category]; michael@0: if (indexes.length > limit) { michael@0: let n = Math.max(0, indexes.length - limit); michael@0: pruned += n; michael@0: for (let i = n - 1; i >= 0; i--) { michael@0: this._pruneItemFromQueue(this._outputQueue[indexes[i]]); michael@0: this._outputQueue.splice(indexes[i], 1); michael@0: } michael@0: } michael@0: } michael@0: michael@0: return pruned; michael@0: }, michael@0: michael@0: /** michael@0: * Prune an item from the output queue. michael@0: * michael@0: * @private michael@0: * @param array aItem michael@0: * The item you want to remove from the output queue. michael@0: */ michael@0: _pruneItemFromQueue: function WCF__pruneItemFromQueue(aItem) michael@0: { michael@0: // TODO: handle object releasing in a more elegant way once all console michael@0: // messages use the new API - bug 778766. michael@0: michael@0: let [category, methodOrNode, args] = aItem; michael@0: if (typeof methodOrNode != "function" && methodOrNode._objectActors) { michael@0: for (let actor of methodOrNode._objectActors) { michael@0: this._releaseObject(actor); michael@0: } michael@0: methodOrNode._objectActors.clear(); michael@0: } michael@0: michael@0: if (methodOrNode == this.output._flushMessageQueue && michael@0: args[0]._objectActors) { michael@0: for (let arg of args) { michael@0: if (!arg._objectActors) { michael@0: continue; michael@0: } michael@0: for (let actor of arg._objectActors) { michael@0: this._releaseObject(actor); michael@0: } michael@0: arg._objectActors.clear(); michael@0: } michael@0: } michael@0: michael@0: if (category == CATEGORY_NETWORK) { michael@0: let connectionId = null; michael@0: if (methodOrNode == this.logNetEvent) { michael@0: connectionId = args[0]; michael@0: } michael@0: else if (typeof methodOrNode != "function") { michael@0: connectionId = methodOrNode._connectionId; michael@0: } michael@0: if (connectionId && connectionId in this._networkRequests) { michael@0: delete this._networkRequests[connectionId]; michael@0: this._releaseObject(connectionId); michael@0: } michael@0: } michael@0: else if (category == CATEGORY_WEBDEV && michael@0: methodOrNode == this.logConsoleAPIMessage) { michael@0: args[0].arguments.forEach((aValue) => { michael@0: if (WebConsoleUtils.isActorGrip(aValue)) { michael@0: this._releaseObject(aValue.actor); michael@0: } michael@0: }); michael@0: } michael@0: else if (category == CATEGORY_JS && michael@0: methodOrNode == this.reportPageError) { michael@0: let pageError = args[1]; michael@0: for (let prop of ["errorMessage", "lineText"]) { michael@0: let grip = pageError[prop]; michael@0: if (WebConsoleUtils.isActorGrip(grip)) { michael@0: this._releaseObject(grip.actor); michael@0: } michael@0: } michael@0: } michael@0: else if (category == CATEGORY_JS && michael@0: methodOrNode == this._reportLogMessage) { michael@0: if (WebConsoleUtils.isActorGrip(args[0].message)) { michael@0: this._releaseObject(args[0].message.actor); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Ensures that the number of message nodes of type aCategory don't exceed that michael@0: * category's line limit by removing old messages as needed. michael@0: * michael@0: * @param integer aCategory michael@0: * The category of message nodes to prune if needed. michael@0: * @return number michael@0: * The number of removed nodes. michael@0: */ michael@0: pruneOutputIfNecessary: function WCF_pruneOutputIfNecessary(aCategory) michael@0: { michael@0: let logLimit = Utils.logLimitForCategory(aCategory); michael@0: let messageNodes = this.outputNode.querySelectorAll(".message[category=" + michael@0: CATEGORY_CLASS_FRAGMENTS[aCategory] + "]"); michael@0: let n = Math.max(0, messageNodes.length - logLimit); michael@0: let toRemove = Array.prototype.slice.call(messageNodes, 0, n); michael@0: toRemove.forEach(this.removeOutputMessage, this); michael@0: michael@0: return n; michael@0: }, michael@0: michael@0: /** michael@0: * Remove a given message from the output. michael@0: * michael@0: * @param nsIDOMNode aNode michael@0: * The message node you want to remove. michael@0: */ michael@0: removeOutputMessage: function WCF_removeOutputMessage(aNode) michael@0: { michael@0: if (aNode._messageObject) { michael@0: aNode._messageObject.destroy(); michael@0: } michael@0: michael@0: if (aNode._objectActors) { michael@0: for (let actor of aNode._objectActors) { michael@0: this._releaseObject(actor); michael@0: } michael@0: aNode._objectActors.clear(); michael@0: } michael@0: michael@0: if (aNode.category == CATEGORY_CSS || michael@0: aNode.category == CATEGORY_SECURITY) { michael@0: let repeatNode = aNode.getElementsByClassName("message-repeats")[0]; michael@0: if (repeatNode && repeatNode._uid) { michael@0: delete this._repeatNodes[repeatNode._uid]; michael@0: } michael@0: } michael@0: else if (aNode._connectionId && michael@0: aNode.category == CATEGORY_NETWORK) { michael@0: delete this._networkRequests[aNode._connectionId]; michael@0: this._releaseObject(aNode._connectionId); michael@0: } michael@0: else if (aNode.classList.contains("inlined-variables-view")) { michael@0: let view = aNode._variablesView; michael@0: if (view) { michael@0: view.controller.releaseActors(); michael@0: } michael@0: aNode._variablesView = null; michael@0: } michael@0: michael@0: if (aNode.parentNode) { michael@0: aNode.parentNode.removeChild(aNode); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Given a category and message body, creates a DOM node to represent an michael@0: * incoming message. The timestamp is automatically added. michael@0: * michael@0: * @param number aCategory michael@0: * The category of the message: one of the CATEGORY_* constants. michael@0: * @param number aSeverity michael@0: * The severity of the message: one of the SEVERITY_* constants; michael@0: * @param string|nsIDOMNode aBody michael@0: * The body of the message, either a simple string or a DOM node. michael@0: * @param string aSourceURL [optional] michael@0: * The URL of the source file that emitted the error. michael@0: * @param number aSourceLine [optional] michael@0: * The line number on which the error occurred. If zero or omitted, michael@0: * there is no line number associated with this message. michael@0: * @param string aClipboardText [optional] michael@0: * The text that should be copied to the clipboard when this node is michael@0: * copied. If omitted, defaults to the body text. If `aBody` is not michael@0: * a string, then the clipboard text must be supplied. michael@0: * @param number aLevel [optional] michael@0: * The level of the console API message. michael@0: * @param number aTimeStamp [optional] michael@0: * The timestamp to use for this message node. If omitted, the current michael@0: * date and time is used. michael@0: * @return nsIDOMNode michael@0: * The message node: a DIV ready to be inserted into the Web Console michael@0: * output node. michael@0: */ michael@0: createMessageNode: michael@0: function WCF_createMessageNode(aCategory, aSeverity, aBody, aSourceURL, michael@0: aSourceLine, aClipboardText, aLevel, aTimeStamp) michael@0: { michael@0: if (typeof aBody != "string" && aClipboardText == null && aBody.innerText) { michael@0: aClipboardText = aBody.innerText; michael@0: } michael@0: michael@0: let indentNode = this.document.createElementNS(XHTML_NS, "span"); michael@0: indentNode.className = "indent"; michael@0: michael@0: // Apply the current group by indenting appropriately. michael@0: let indent = this.groupDepth * GROUP_INDENT; michael@0: indentNode.style.width = indent + "px"; michael@0: michael@0: // Make the icon container, which is a vertical box. Its purpose is to michael@0: // ensure that the icon stays anchored at the top of the message even for michael@0: // long multi-line messages. michael@0: let iconContainer = this.document.createElementNS(XHTML_NS, "span"); michael@0: iconContainer.className = "icon"; michael@0: michael@0: // Create the message body, which contains the actual text of the message. michael@0: let bodyNode = this.document.createElementNS(XHTML_NS, "span"); michael@0: bodyNode.className = "message-body-wrapper message-body devtools-monospace"; michael@0: michael@0: // Store the body text, since it is needed later for the variables view. michael@0: let body = aBody; michael@0: // If a string was supplied for the body, turn it into a DOM node and an michael@0: // associated clipboard string now. michael@0: aClipboardText = aClipboardText || michael@0: (aBody + (aSourceURL ? " @ " + aSourceURL : "") + michael@0: (aSourceLine ? ":" + aSourceLine : "")); michael@0: michael@0: let timestamp = aTimeStamp || Date.now(); michael@0: michael@0: // Create the containing node and append all its elements to it. michael@0: let node = this.document.createElementNS(XHTML_NS, "div"); michael@0: node.id = "console-msg-" + gSequenceId(); michael@0: node.className = "message"; michael@0: node.clipboardText = aClipboardText; michael@0: node.timestamp = timestamp; michael@0: this.setMessageType(node, aCategory, aSeverity); michael@0: michael@0: if (aBody instanceof Ci.nsIDOMNode) { michael@0: bodyNode.appendChild(aBody); michael@0: } michael@0: else { michael@0: let str = undefined; michael@0: if (aLevel == "dir") { michael@0: str = VariablesView.getString(aBody.arguments[0]); michael@0: } michael@0: else { michael@0: str = aBody; michael@0: } michael@0: michael@0: if (str !== undefined) { michael@0: aBody = this.document.createTextNode(str); michael@0: bodyNode.appendChild(aBody); michael@0: } michael@0: } michael@0: michael@0: // Add the message repeats node only when needed. michael@0: let repeatNode = null; michael@0: if (aCategory != CATEGORY_INPUT && michael@0: aCategory != CATEGORY_OUTPUT && michael@0: aCategory != CATEGORY_NETWORK && michael@0: !(aCategory == CATEGORY_CSS && aSeverity == SEVERITY_LOG)) { michael@0: repeatNode = this.document.createElementNS(XHTML_NS, "span"); michael@0: repeatNode.setAttribute("value", "1"); michael@0: repeatNode.className = "message-repeats"; michael@0: repeatNode.textContent = 1; michael@0: repeatNode._uid = [bodyNode.textContent, aCategory, aSeverity, aLevel, michael@0: aSourceURL, aSourceLine].join(":"); michael@0: } michael@0: michael@0: // Create the timestamp. michael@0: let timestampNode = this.document.createElementNS(XHTML_NS, "span"); michael@0: timestampNode.className = "timestamp devtools-monospace"; michael@0: michael@0: let timestampString = l10n.timestampString(timestamp); michael@0: timestampNode.textContent = timestampString + " "; michael@0: michael@0: // Create the source location (e.g. www.example.com:6) that sits on the michael@0: // right side of the message, if applicable. michael@0: let locationNode; michael@0: if (aSourceURL && IGNORED_SOURCE_URLS.indexOf(aSourceURL) == -1) { michael@0: locationNode = this.createLocationNode(aSourceURL, aSourceLine); michael@0: } michael@0: michael@0: node.appendChild(timestampNode); michael@0: node.appendChild(indentNode); michael@0: node.appendChild(iconContainer); michael@0: michael@0: // Display the variables view after the message node. michael@0: if (aLevel == "dir") { michael@0: bodyNode.style.height = (this.window.innerHeight * michael@0: CONSOLE_DIR_VIEW_HEIGHT) + "px"; michael@0: michael@0: let options = { michael@0: objectActor: body.arguments[0], michael@0: targetElement: bodyNode, michael@0: hideFilterInput: true, michael@0: }; michael@0: this.jsterm.openVariablesView(options).then((aView) => { michael@0: node._variablesView = aView; michael@0: if (node.classList.contains("hidden-message")) { michael@0: node.classList.remove("hidden-message"); michael@0: } michael@0: }); michael@0: michael@0: node.classList.add("inlined-variables-view"); michael@0: } michael@0: michael@0: node.appendChild(bodyNode); michael@0: if (repeatNode) { michael@0: node.appendChild(repeatNode); michael@0: } michael@0: if (locationNode) { michael@0: node.appendChild(locationNode); michael@0: } michael@0: node.appendChild(this.document.createTextNode("\n")); michael@0: michael@0: return node; michael@0: }, michael@0: michael@0: /** michael@0: * Creates the anchor that displays the textual location of an incoming michael@0: * message. michael@0: * michael@0: * @param string aSourceURL michael@0: * The URL of the source file responsible for the error. michael@0: * @param number aSourceLine [optional] michael@0: * The line number on which the error occurred. If zero or omitted, michael@0: * there is no line number associated with this message. michael@0: * @param string aTarget [optional] michael@0: * Tells which tool to open the link with, on click. Supported tools: michael@0: * jsdebugger, styleeditor, scratchpad. michael@0: * @return nsIDOMNode michael@0: * The new anchor element, ready to be added to the message node. michael@0: */ michael@0: createLocationNode: michael@0: function WCF_createLocationNode(aSourceURL, aSourceLine, aTarget) michael@0: { michael@0: if (!aSourceURL) { michael@0: aSourceURL = ""; michael@0: } michael@0: let locationNode = this.document.createElementNS(XHTML_NS, "a"); michael@0: let filenameNode = this.document.createElementNS(XHTML_NS, "span"); michael@0: michael@0: // Create the text, which consists of an abbreviated version of the URL michael@0: // Scratchpad URLs should not be abbreviated. michael@0: let filename; michael@0: let fullURL; michael@0: let isScratchpad = false; michael@0: michael@0: if (/^Scratchpad\/\d+$/.test(aSourceURL)) { michael@0: filename = aSourceURL; michael@0: fullURL = aSourceURL; michael@0: isScratchpad = true; michael@0: } michael@0: else { michael@0: fullURL = aSourceURL.split(" -> ").pop(); michael@0: filename = WebConsoleUtils.abbreviateSourceURL(fullURL); michael@0: } michael@0: michael@0: filenameNode.className = "filename"; michael@0: filenameNode.textContent = " " + (filename || l10n.getStr("unknownLocation")); michael@0: locationNode.appendChild(filenameNode); michael@0: michael@0: locationNode.href = isScratchpad || !fullURL ? "#" : fullURL; michael@0: locationNode.draggable = false; michael@0: if (aTarget) { michael@0: locationNode.target = aTarget; michael@0: } michael@0: locationNode.setAttribute("title", aSourceURL); michael@0: locationNode.className = "message-location theme-link devtools-monospace"; michael@0: michael@0: // Make the location clickable. michael@0: let onClick = () => { michael@0: let target = locationNode.target; michael@0: if (target == "scratchpad" || isScratchpad) { michael@0: this.owner.viewSourceInScratchpad(aSourceURL); michael@0: return; michael@0: } michael@0: michael@0: let category = locationNode.parentNode.category; michael@0: if (target == "styleeditor" || category == CATEGORY_CSS) { michael@0: this.owner.viewSourceInStyleEditor(fullURL, aSourceLine); michael@0: } michael@0: else if (target == "jsdebugger" || michael@0: category == CATEGORY_JS || category == CATEGORY_WEBDEV) { michael@0: this.owner.viewSourceInDebugger(fullURL, aSourceLine); michael@0: } michael@0: else { michael@0: this.owner.viewSource(fullURL, aSourceLine); michael@0: } michael@0: }; michael@0: michael@0: if (fullURL) { michael@0: this._addMessageLinkCallback(locationNode, onClick); michael@0: } michael@0: michael@0: if (aSourceLine) { michael@0: let lineNumberNode = this.document.createElementNS(XHTML_NS, "span"); michael@0: lineNumberNode.className = "line-number"; michael@0: lineNumberNode.textContent = ":" + aSourceLine; michael@0: locationNode.appendChild(lineNumberNode); michael@0: locationNode.sourceLine = aSourceLine; michael@0: } michael@0: michael@0: return locationNode; michael@0: }, michael@0: michael@0: /** michael@0: * Adjusts the category and severity of the given message. michael@0: * michael@0: * @param nsIDOMNode aMessageNode michael@0: * The message node to alter. michael@0: * @param number aCategory michael@0: * The category for the message; one of the CATEGORY_ constants. michael@0: * @param number aSeverity michael@0: * The severity for the message; one of the SEVERITY_ constants. michael@0: * @return void michael@0: */ michael@0: setMessageType: michael@0: function WCF_setMessageType(aMessageNode, aCategory, aSeverity) michael@0: { michael@0: aMessageNode.category = aCategory; michael@0: aMessageNode.severity = aSeverity; michael@0: aMessageNode.setAttribute("category", CATEGORY_CLASS_FRAGMENTS[aCategory]); michael@0: aMessageNode.setAttribute("severity", SEVERITY_CLASS_FRAGMENTS[aSeverity]); michael@0: aMessageNode.setAttribute("filter", MESSAGE_PREFERENCE_KEYS[aCategory][aSeverity]); michael@0: }, michael@0: michael@0: /** michael@0: * Add the mouse event handlers needed to make a link. michael@0: * michael@0: * @private michael@0: * @param nsIDOMNode aNode michael@0: * The node for which you want to add the event handlers. michael@0: * @param function aCallback michael@0: * The function you want to invoke on click. michael@0: */ michael@0: _addMessageLinkCallback: function WCF__addMessageLinkCallback(aNode, aCallback) michael@0: { michael@0: aNode.addEventListener("mousedown", (aEvent) => { michael@0: this._mousedown = true; michael@0: this._startX = aEvent.clientX; michael@0: this._startY = aEvent.clientY; michael@0: }, false); michael@0: michael@0: aNode.addEventListener("click", (aEvent) => { michael@0: let mousedown = this._mousedown; michael@0: this._mousedown = false; michael@0: michael@0: aEvent.preventDefault(); michael@0: michael@0: // Do not allow middle/right-click or 2+ clicks. michael@0: if (aEvent.detail != 1 || aEvent.button != 0) { michael@0: return; michael@0: } michael@0: michael@0: // If this event started with a mousedown event and it ends at a different michael@0: // location, we consider this text selection. michael@0: if (mousedown && michael@0: (this._startX != aEvent.clientX) && michael@0: (this._startY != aEvent.clientY)) michael@0: { michael@0: this._startX = this._startY = undefined; michael@0: return; michael@0: } michael@0: michael@0: this._startX = this._startY = undefined; michael@0: michael@0: aCallback.call(this, aEvent); michael@0: }, false); michael@0: }, michael@0: michael@0: _addFocusCallback: function WCF__addFocusCallback(aNode, aCallback) michael@0: { michael@0: aNode.addEventListener("mousedown", (aEvent) => { michael@0: this._mousedown = true; michael@0: this._startX = aEvent.clientX; michael@0: this._startY = aEvent.clientY; michael@0: }, false); michael@0: michael@0: aNode.addEventListener("click", (aEvent) => { michael@0: let mousedown = this._mousedown; michael@0: this._mousedown = false; michael@0: michael@0: // Do not allow middle/right-click or 2+ clicks. michael@0: if (aEvent.detail != 1 || aEvent.button != 0) { michael@0: return; michael@0: } michael@0: michael@0: // If this event started with a mousedown event and it ends at a different michael@0: // location, we consider this text selection. michael@0: // Add a fuzz modifier of two pixels in any direction to account for sloppy michael@0: // clicking. michael@0: if (mousedown && michael@0: (Math.abs(aEvent.clientX - this._startX) >= 2) && michael@0: (Math.abs(aEvent.clientY - this._startY) >= 1)) michael@0: { michael@0: this._startX = this._startY = undefined; michael@0: return; michael@0: } michael@0: michael@0: this._startX = this._startY = undefined; michael@0: michael@0: aCallback.call(this, aEvent); michael@0: }, false); michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the pref-changed event coming from the toolbox. michael@0: * Currently this function only handles the timestamps preferences. michael@0: * michael@0: * @private michael@0: * @param object aEvent michael@0: * This parameter is a string that holds the event name michael@0: * pref-changed in this case. michael@0: * @param object aData michael@0: * This is the pref-changed data object. michael@0: */ michael@0: _onToolboxPrefChanged: function WCF__onToolboxPrefChanged(aEvent, aData) michael@0: { michael@0: if (aData.pref == PREF_MESSAGE_TIMESTAMP) { michael@0: if (aData.newValue) { michael@0: this.outputNode.classList.remove("hideTimestamps"); michael@0: } michael@0: else { michael@0: this.outputNode.classList.add("hideTimestamps"); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Copies the selected items to the system clipboard. michael@0: * michael@0: * @param object aOptions michael@0: * - linkOnly: michael@0: * An optional flag to copy only URL without timestamp and michael@0: * other meta-information. Default is false. michael@0: */ michael@0: copySelectedItems: function WCF_copySelectedItems(aOptions) michael@0: { michael@0: aOptions = aOptions || { linkOnly: false, contextmenu: false }; michael@0: michael@0: // Gather up the selected items and concatenate their clipboard text. michael@0: let strings = []; michael@0: michael@0: let children = this.output.getSelectedMessages(); michael@0: if (!children.length && aOptions.contextmenu) { michael@0: children = [this._contextMenuHandler.lastClickedMessage]; michael@0: } michael@0: michael@0: for (let item of children) { michael@0: // Ensure the selected item hasn't been filtered by type or string. michael@0: if (!item.classList.contains("filtered-by-type") && michael@0: !item.classList.contains("filtered-by-string")) { michael@0: let timestampString = l10n.timestampString(item.timestamp); michael@0: if (aOptions.linkOnly) { michael@0: strings.push(item.url); michael@0: } michael@0: else { michael@0: strings.push("[" + timestampString + "] " + item.clipboardText); michael@0: } michael@0: } michael@0: } michael@0: michael@0: clipboardHelper.copyString(strings.join("\n"), this.document); michael@0: }, michael@0: michael@0: /** michael@0: * Object properties provider. This function gives you the properties of the michael@0: * remote object you want. michael@0: * michael@0: * @param string aActor michael@0: * The object actor ID from which you want the properties. michael@0: * @param function aCallback michael@0: * Function you want invoked once the properties are received. michael@0: */ michael@0: objectPropertiesProvider: michael@0: function WCF_objectPropertiesProvider(aActor, aCallback) michael@0: { michael@0: this.webConsoleClient.inspectObjectProperties(aActor, michael@0: function(aResponse) { michael@0: if (aResponse.error) { michael@0: Cu.reportError("Failed to retrieve the object properties from the " + michael@0: "server. Error: " + aResponse.error); michael@0: return; michael@0: } michael@0: aCallback(aResponse.properties); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Release an actor. michael@0: * michael@0: * @private michael@0: * @param string aActor michael@0: * The actor ID you want to release. michael@0: */ michael@0: _releaseObject: function WCF__releaseObject(aActor) michael@0: { michael@0: if (this.proxy) { michael@0: this.proxy.releaseActor(aActor); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Open the selected item's URL in a new tab. michael@0: */ michael@0: openSelectedItemInTab: function WCF_openSelectedItemInTab() michael@0: { michael@0: let item = this.output.getSelectedMessages(1)[0] || michael@0: this._contextMenuHandler.lastClickedMessage; michael@0: michael@0: if (!item || !item.url) { michael@0: return; michael@0: } michael@0: michael@0: this.owner.openLink(item.url); michael@0: }, michael@0: michael@0: /** michael@0: * Destroy the WebConsoleFrame object. Call this method to avoid memory leaks michael@0: * when the Web Console is closed. michael@0: * michael@0: * @return object michael@0: * A promise that is resolved when the WebConsoleFrame instance is michael@0: * destroyed. michael@0: */ michael@0: destroy: function WCF_destroy() michael@0: { michael@0: if (this._destroyer) { michael@0: return this._destroyer.promise; michael@0: } michael@0: michael@0: this._destroyer = promise.defer(); michael@0: michael@0: let toolbox = gDevTools.getToolbox(this.owner.target); michael@0: if (toolbox) { michael@0: toolbox.off("webconsole-selected", this._onPanelSelected); michael@0: } michael@0: michael@0: gDevTools.off("pref-changed", this._onToolboxPrefChanged); michael@0: michael@0: this._repeatNodes = {}; michael@0: this._outputQueue = []; michael@0: this._pruneCategoriesQueue = {}; michael@0: this._networkRequests = {}; michael@0: michael@0: if (this._outputTimerInitialized) { michael@0: this._outputTimerInitialized = false; michael@0: this._outputTimer.cancel(); michael@0: } michael@0: this._outputTimer = null; michael@0: michael@0: if (this.jsterm) { michael@0: this.jsterm.destroy(); michael@0: this.jsterm = null; michael@0: } michael@0: this.output.destroy(); michael@0: this.output = null; michael@0: michael@0: if (this._contextMenuHandler) { michael@0: this._contextMenuHandler.destroy(); michael@0: this._contextMenuHandler = null; michael@0: } michael@0: michael@0: this._commandController = null; michael@0: michael@0: let onDestroy = function() { michael@0: this._destroyer.resolve(null); michael@0: }.bind(this); michael@0: michael@0: if (this.proxy) { michael@0: this.proxy.disconnect().then(onDestroy); michael@0: this.proxy = null; michael@0: } michael@0: else { michael@0: onDestroy(); michael@0: } michael@0: michael@0: return this._destroyer.promise; michael@0: }, michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * @see VariablesView.simpleValueEvalMacro michael@0: */ michael@0: function simpleValueEvalMacro(aItem, aCurrentString) michael@0: { michael@0: return VariablesView.simpleValueEvalMacro(aItem, aCurrentString, "_self"); michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * @see VariablesView.overrideValueEvalMacro michael@0: */ michael@0: function overrideValueEvalMacro(aItem, aCurrentString) michael@0: { michael@0: return VariablesView.overrideValueEvalMacro(aItem, aCurrentString, "_self"); michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * @see VariablesView.getterOrSetterEvalMacro michael@0: */ michael@0: function getterOrSetterEvalMacro(aItem, aCurrentString) michael@0: { michael@0: return VariablesView.getterOrSetterEvalMacro(aItem, aCurrentString, "_self"); michael@0: } michael@0: michael@0: michael@0: michael@0: /** michael@0: * Create a JSTerminal (a JavaScript command line). This is attached to an michael@0: * existing HeadsUpDisplay (a Web Console instance). This code is responsible michael@0: * with handling command line input, code evaluation and result output. michael@0: * michael@0: * @constructor michael@0: * @param object aWebConsoleFrame michael@0: * The WebConsoleFrame object that owns this JSTerm instance. michael@0: */ michael@0: function JSTerm(aWebConsoleFrame) michael@0: { michael@0: this.hud = aWebConsoleFrame; michael@0: this.hudId = this.hud.hudId; michael@0: michael@0: this.lastCompletion = { value: null }; michael@0: this.history = []; michael@0: michael@0: // Holds the number of entries in history. This value is incremented in michael@0: // this.execute(). michael@0: this.historyIndex = 0; // incremented on this.execute() michael@0: michael@0: // Holds the index of the history entry that the user is currently viewing. michael@0: // This is reset to this.history.length when this.execute() is invoked. michael@0: this.historyPlaceHolder = 0; michael@0: this._objectActorsInVariablesViews = new Map(); michael@0: michael@0: this._keyPress = this._keyPress.bind(this); michael@0: this._inputEventHandler = this._inputEventHandler.bind(this); michael@0: this._focusEventHandler = this._focusEventHandler.bind(this); michael@0: this._onKeypressInVariablesView = this._onKeypressInVariablesView.bind(this); michael@0: this._blurEventHandler = this._blurEventHandler.bind(this); michael@0: michael@0: EventEmitter.decorate(this); michael@0: } michael@0: michael@0: JSTerm.prototype = { michael@0: SELECTED_FRAME: -1, michael@0: michael@0: /** michael@0: * Stores the data for the last completion. michael@0: * @type object michael@0: */ michael@0: lastCompletion: null, michael@0: michael@0: /** michael@0: * Array that caches the user input suggestions received from the server. michael@0: * @private michael@0: * @type array michael@0: */ michael@0: _autocompleteCache: null, michael@0: michael@0: /** michael@0: * The input that caused the last request to the server, whose response is michael@0: * cached in the _autocompleteCache array. michael@0: * @private michael@0: * @type string michael@0: */ michael@0: _autocompleteQuery: null, michael@0: michael@0: /** michael@0: * The frameActorId used in the last autocomplete query. Whenever this changes michael@0: * the autocomplete cache must be invalidated. michael@0: * @private michael@0: * @type string michael@0: */ michael@0: _lastFrameActorId: null, michael@0: michael@0: /** michael@0: * The Web Console sidebar. michael@0: * @see this._createSidebar() michael@0: * @see Sidebar.jsm michael@0: */ michael@0: sidebar: null, michael@0: michael@0: /** michael@0: * The Variables View instance shown in the sidebar. michael@0: * @private michael@0: * @type object michael@0: */ michael@0: _variablesView: null, michael@0: michael@0: /** michael@0: * Tells if you want the variables view UI updates to be lazy or not. Tests michael@0: * disable lazy updates. michael@0: * michael@0: * @private michael@0: * @type boolean michael@0: */ michael@0: _lazyVariablesView: true, michael@0: michael@0: /** michael@0: * Holds a map between VariablesView instances and sets of ObjectActor IDs michael@0: * that have been retrieved from the server. This allows us to release the michael@0: * objects when needed. michael@0: * michael@0: * @private michael@0: * @type Map michael@0: */ michael@0: _objectActorsInVariablesViews: null, michael@0: michael@0: /** michael@0: * Last input value. michael@0: * @type string michael@0: */ michael@0: lastInputValue: "", michael@0: michael@0: /** michael@0: * Tells if the input node changed since the last focus. michael@0: * michael@0: * @private michael@0: * @type boolean michael@0: */ michael@0: _inputChanged: false, michael@0: michael@0: /** michael@0: * Tells if the autocomplete popup was navigated since the last open. michael@0: * michael@0: * @private michael@0: * @type boolean michael@0: */ michael@0: _autocompletePopupNavigated: false, michael@0: michael@0: /** michael@0: * History of code that was executed. michael@0: * @type array michael@0: */ michael@0: history: null, michael@0: autocompletePopup: null, michael@0: inputNode: null, michael@0: completeNode: null, michael@0: michael@0: /** michael@0: * Getter for the element that holds the messages we display. michael@0: * @type nsIDOMElement michael@0: */ michael@0: get outputNode() this.hud.outputNode, michael@0: michael@0: /** michael@0: * Getter for the debugger WebConsoleClient. michael@0: * @type object michael@0: */ michael@0: get webConsoleClient() this.hud.webConsoleClient, michael@0: michael@0: COMPLETE_FORWARD: 0, michael@0: COMPLETE_BACKWARD: 1, michael@0: COMPLETE_HINT_ONLY: 2, michael@0: COMPLETE_PAGEUP: 3, michael@0: COMPLETE_PAGEDOWN: 4, michael@0: michael@0: /** michael@0: * Initialize the JSTerminal UI. michael@0: */ michael@0: init: function JST_init() michael@0: { michael@0: let autocompleteOptions = { michael@0: onSelect: this.onAutocompleteSelect.bind(this), michael@0: onClick: this.acceptProposedCompletion.bind(this), michael@0: panelId: "webConsole_autocompletePopup", michael@0: listBoxId: "webConsole_autocompletePopupListBox", michael@0: position: "before_start", michael@0: theme: "auto", michael@0: direction: "ltr", michael@0: autoSelect: true michael@0: }; michael@0: this.autocompletePopup = new AutocompletePopup(this.hud.document, michael@0: autocompleteOptions); michael@0: michael@0: let doc = this.hud.document; michael@0: let inputContainer = doc.querySelector(".jsterm-input-container"); michael@0: this.completeNode = doc.querySelector(".jsterm-complete-node"); michael@0: this.inputNode = doc.querySelector(".jsterm-input-node"); michael@0: michael@0: if (this.hud.owner._browserConsole && michael@0: !Services.prefs.getBoolPref("devtools.chrome.enabled")) { michael@0: inputContainer.style.display = "none"; michael@0: } michael@0: else { michael@0: this.inputNode.addEventListener("keypress", this._keyPress, false); michael@0: this.inputNode.addEventListener("input", this._inputEventHandler, false); michael@0: this.inputNode.addEventListener("keyup", this._inputEventHandler, false); michael@0: this.inputNode.addEventListener("focus", this._focusEventHandler, false); michael@0: } michael@0: michael@0: this.hud.window.addEventListener("blur", this._blurEventHandler, false); michael@0: this.lastInputValue && this.setInputValue(this.lastInputValue); michael@0: }, michael@0: michael@0: /** michael@0: * The JavaScript evaluation response handler. michael@0: * michael@0: * @private michael@0: * @param object [aAfterMessage] michael@0: * Optional message after which the evaluation result will be michael@0: * inserted. michael@0: * @param function [aCallback] michael@0: * Optional function to invoke when the evaluation result is added to michael@0: * the output. michael@0: * @param object aResponse michael@0: * The message received from the server. michael@0: */ michael@0: _executeResultCallback: michael@0: function JST__executeResultCallback(aAfterMessage, aCallback, aResponse) michael@0: { michael@0: if (!this.hud) { michael@0: return; michael@0: } michael@0: if (aResponse.error) { michael@0: Cu.reportError("Evaluation error " + aResponse.error + ": " + michael@0: aResponse.message); michael@0: return; michael@0: } michael@0: let errorMessage = aResponse.exceptionMessage; michael@0: let result = aResponse.result; michael@0: let helperResult = aResponse.helperResult; michael@0: let helperHasRawOutput = !!(helperResult || {}).rawOutput; michael@0: michael@0: if (helperResult && helperResult.type) { michael@0: switch (helperResult.type) { michael@0: case "clearOutput": michael@0: this.clearOutput(); michael@0: break; michael@0: case "inspectObject": michael@0: if (aAfterMessage) { michael@0: if (!aAfterMessage._objectActors) { michael@0: aAfterMessage._objectActors = new Set(); michael@0: } michael@0: aAfterMessage._objectActors.add(helperResult.object.actor); michael@0: } michael@0: this.openVariablesView({ michael@0: label: VariablesView.getString(helperResult.object, { concise: true }), michael@0: objectActor: helperResult.object, michael@0: }); michael@0: break; michael@0: case "error": michael@0: try { michael@0: errorMessage = l10n.getStr(helperResult.message); michael@0: } michael@0: catch (ex) { michael@0: errorMessage = helperResult.message; michael@0: } michael@0: break; michael@0: case "help": michael@0: this.hud.owner.openLink(HELP_URL); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: // Hide undefined results coming from JSTerm helper functions. michael@0: if (!errorMessage && result && typeof result == "object" && michael@0: result.type == "undefined" && michael@0: helperResult && !helperHasRawOutput) { michael@0: aCallback && aCallback(); michael@0: return; michael@0: } michael@0: michael@0: let msg = new Messages.JavaScriptEvalOutput(aResponse, errorMessage); michael@0: this.hud.output.addMessage(msg); michael@0: michael@0: if (aCallback) { michael@0: let oldFlushCallback = this.hud._flushCallback; michael@0: this.hud._flushCallback = () => { michael@0: aCallback(msg.element); michael@0: if (oldFlushCallback) { michael@0: oldFlushCallback(); michael@0: this.hud._flushCallback = oldFlushCallback; michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: }; michael@0: } michael@0: michael@0: msg._afterMessage = aAfterMessage; michael@0: msg._objectActors = new Set(); michael@0: michael@0: if (WebConsoleUtils.isActorGrip(aResponse.exception)) { michael@0: msg._objectActors.add(aResponse.exception.actor); michael@0: } michael@0: michael@0: if (WebConsoleUtils.isActorGrip(result)) { michael@0: msg._objectActors.add(result.actor); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Execute a string. Execution happens asynchronously in the content process. michael@0: * michael@0: * @param string [aExecuteString] michael@0: * The string you want to execute. If this is not provided, the current michael@0: * user input is used - taken from |this.inputNode.value|. michael@0: * @param function [aCallback] michael@0: * Optional function to invoke when the result is displayed. michael@0: */ michael@0: execute: function JST_execute(aExecuteString, aCallback) michael@0: { michael@0: // attempt to execute the content of the inputNode michael@0: aExecuteString = aExecuteString || this.inputNode.value; michael@0: if (!aExecuteString) { michael@0: return; michael@0: } michael@0: michael@0: let message = new Messages.Simple(aExecuteString, { michael@0: category: "input", michael@0: severity: "log", michael@0: }); michael@0: this.hud.output.addMessage(message); michael@0: let onResult = this._executeResultCallback.bind(this, message, aCallback); michael@0: michael@0: let options = { frame: this.SELECTED_FRAME }; michael@0: this.requestEvaluation(aExecuteString, options).then(onResult, onResult); michael@0: michael@0: // Append a new value in the history of executed code, or overwrite the most michael@0: // recent entry. The most recent entry may contain the last edited input michael@0: // value that was not evaluated yet. michael@0: this.history[this.historyIndex++] = aExecuteString; michael@0: this.historyPlaceHolder = this.history.length; michael@0: this.setInputValue(""); michael@0: this.clearCompletion(); michael@0: }, michael@0: michael@0: /** michael@0: * Request a JavaScript string evaluation from the server. michael@0: * michael@0: * @param string aString michael@0: * String to execute. michael@0: * @param object [aOptions] michael@0: * Options for evaluation: michael@0: * - bindObjectActor: tells the ObjectActor ID for which you want to do michael@0: * the evaluation. The Debugger.Object of the OA will be bound to michael@0: * |_self| during evaluation, such that it's usable in the string you michael@0: * execute. michael@0: * - frame: tells the stackframe depth to evaluate the string in. If michael@0: * the jsdebugger is paused, you can pick the stackframe to be used for michael@0: * evaluation. Use |this.SELECTED_FRAME| to always pick the michael@0: * user-selected stackframe. michael@0: * If you do not provide a |frame| the string will be evaluated in the michael@0: * global content window. michael@0: * @return object michael@0: * A promise object that is resolved when the server response is michael@0: * received. michael@0: */ michael@0: requestEvaluation: function JST_requestEvaluation(aString, aOptions = {}) michael@0: { michael@0: let deferred = promise.defer(); michael@0: michael@0: function onResult(aResponse) { michael@0: if (!aResponse.error) { michael@0: deferred.resolve(aResponse); michael@0: } michael@0: else { michael@0: deferred.reject(aResponse); michael@0: } michael@0: } michael@0: michael@0: let frameActor = null; michael@0: if ("frame" in aOptions) { michael@0: frameActor = this.getFrameActor(aOptions.frame); michael@0: } michael@0: michael@0: let evalOptions = { michael@0: bindObjectActor: aOptions.bindObjectActor, michael@0: frameActor: frameActor, michael@0: }; michael@0: michael@0: this.webConsoleClient.evaluateJS(aString, onResult, evalOptions); michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the FrameActor ID given a frame depth. michael@0: * michael@0: * @param number aFrame michael@0: * Frame depth. michael@0: * @return string|null michael@0: * The FrameActor ID for the given frame depth. michael@0: */ michael@0: getFrameActor: function JST_getFrameActor(aFrame) michael@0: { michael@0: let state = this.hud.owner.getDebuggerFrames(); michael@0: if (!state) { michael@0: return null; michael@0: } michael@0: michael@0: let grip; michael@0: if (aFrame == this.SELECTED_FRAME) { michael@0: grip = state.frames[state.selected]; michael@0: } michael@0: else { michael@0: grip = state.frames[aFrame]; michael@0: } michael@0: michael@0: return grip ? grip.actor : null; michael@0: }, michael@0: michael@0: /** michael@0: * Opens a new variables view that allows the inspection of the given object. michael@0: * michael@0: * @param object aOptions michael@0: * Options for the variables view: michael@0: * - objectActor: grip of the ObjectActor you want to show in the michael@0: * variables view. michael@0: * - rawObject: the raw object you want to show in the variables view. michael@0: * - label: label to display in the variables view for inspected michael@0: * object. michael@0: * - hideFilterInput: optional boolean, |true| if you want to hide the michael@0: * variables view filter input. michael@0: * - targetElement: optional nsIDOMElement to append the variables view michael@0: * to. An iframe element is used as a container for the view. If this michael@0: * option is not used, then the variables view opens in the sidebar. michael@0: * - autofocus: optional boolean, |true| if you want to give focus to michael@0: * the variables view window after open, |false| otherwise. michael@0: * @return object michael@0: * A promise object that is resolved when the variables view has michael@0: * opened. The new variables view instance is given to the callbacks. michael@0: */ michael@0: openVariablesView: function JST_openVariablesView(aOptions) michael@0: { michael@0: let onContainerReady = (aWindow) => { michael@0: let container = aWindow.document.querySelector("#variables"); michael@0: let view = this._variablesView; michael@0: if (!view || aOptions.targetElement) { michael@0: let viewOptions = { michael@0: container: container, michael@0: hideFilterInput: aOptions.hideFilterInput, michael@0: }; michael@0: view = this._createVariablesView(viewOptions); michael@0: if (!aOptions.targetElement) { michael@0: this._variablesView = view; michael@0: aWindow.addEventListener("keypress", this._onKeypressInVariablesView); michael@0: } michael@0: } michael@0: aOptions.view = view; michael@0: this._updateVariablesView(aOptions); michael@0: michael@0: if (!aOptions.targetElement && aOptions.autofocus) { michael@0: aWindow.focus(); michael@0: } michael@0: michael@0: this.emit("variablesview-open", view, aOptions); michael@0: return view; michael@0: }; michael@0: michael@0: let openPromise; michael@0: if (aOptions.targetElement) { michael@0: let deferred = promise.defer(); michael@0: openPromise = deferred.promise; michael@0: let document = aOptions.targetElement.ownerDocument; michael@0: let iframe = document.createElementNS(XHTML_NS, "iframe"); michael@0: michael@0: iframe.addEventListener("load", function onIframeLoad(aEvent) { michael@0: iframe.removeEventListener("load", onIframeLoad, true); michael@0: iframe.style.visibility = "visible"; michael@0: deferred.resolve(iframe.contentWindow); michael@0: }, true); michael@0: michael@0: iframe.flex = 1; michael@0: iframe.style.visibility = "hidden"; michael@0: iframe.setAttribute("src", VARIABLES_VIEW_URL); michael@0: aOptions.targetElement.appendChild(iframe); michael@0: } michael@0: else { michael@0: if (!this.sidebar) { michael@0: this._createSidebar(); michael@0: } michael@0: openPromise = this._addVariablesViewSidebarTab(); michael@0: } michael@0: michael@0: return openPromise.then(onContainerReady); michael@0: }, michael@0: michael@0: /** michael@0: * Create the Web Console sidebar. michael@0: * michael@0: * @see devtools/framework/sidebar.js michael@0: * @private michael@0: */ michael@0: _createSidebar: function JST__createSidebar() michael@0: { michael@0: let tabbox = this.hud.document.querySelector("#webconsole-sidebar"); michael@0: this.sidebar = new ToolSidebar(tabbox, this, "webconsole"); michael@0: this.sidebar.show(); michael@0: }, michael@0: michael@0: /** michael@0: * Add the variables view tab to the sidebar. michael@0: * michael@0: * @private michael@0: * @return object michael@0: * A promise object for the adding of the new tab. michael@0: */ michael@0: _addVariablesViewSidebarTab: function JST__addVariablesViewSidebarTab() michael@0: { michael@0: let deferred = promise.defer(); michael@0: michael@0: let onTabReady = () => { michael@0: let window = this.sidebar.getWindowForTab("variablesview"); michael@0: deferred.resolve(window); michael@0: }; michael@0: michael@0: let tab = this.sidebar.getTab("variablesview"); michael@0: if (tab) { michael@0: if (this.sidebar.getCurrentTabID() == "variablesview") { michael@0: onTabReady(); michael@0: } michael@0: else { michael@0: this.sidebar.once("variablesview-selected", onTabReady); michael@0: this.sidebar.select("variablesview"); michael@0: } michael@0: } michael@0: else { michael@0: this.sidebar.once("variablesview-ready", onTabReady); michael@0: this.sidebar.addTab("variablesview", VARIABLES_VIEW_URL, true); michael@0: } michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * The keypress event handler for the Variables View sidebar. Currently this michael@0: * is used for removing the sidebar when Escape is pressed. michael@0: * michael@0: * @private michael@0: * @param nsIDOMEvent aEvent michael@0: * The keypress DOM event object. michael@0: */ michael@0: _onKeypressInVariablesView: function JST__onKeypressInVariablesView(aEvent) michael@0: { michael@0: let tag = aEvent.target.nodeName; michael@0: if (aEvent.keyCode != Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE || aEvent.shiftKey || michael@0: aEvent.altKey || aEvent.ctrlKey || aEvent.metaKey || michael@0: ["input", "textarea", "select", "textbox"].indexOf(tag) > -1) { michael@0: return; michael@0: } michael@0: michael@0: this._sidebarDestroy(); michael@0: this.inputNode.focus(); michael@0: aEvent.stopPropagation(); michael@0: }, michael@0: michael@0: /** michael@0: * Create a variables view instance. michael@0: * michael@0: * @private michael@0: * @param object aOptions michael@0: * Options for the new Variables View instance: michael@0: * - container: the DOM element where the variables view is inserted. michael@0: * - hideFilterInput: boolean, if true the variables filter input is michael@0: * hidden. michael@0: * @return object michael@0: * The new Variables View instance. michael@0: */ michael@0: _createVariablesView: function JST__createVariablesView(aOptions) michael@0: { michael@0: let view = new VariablesView(aOptions.container); michael@0: view.toolbox = gDevTools.getToolbox(this.hud.owner.target); michael@0: view.searchPlaceholder = l10n.getStr("propertiesFilterPlaceholder"); michael@0: view.emptyText = l10n.getStr("emptyPropertiesList"); michael@0: view.searchEnabled = !aOptions.hideFilterInput; michael@0: view.lazyEmpty = this._lazyVariablesView; michael@0: michael@0: VariablesViewController.attach(view, { michael@0: getEnvironmentClient: aGrip => { michael@0: return new EnvironmentClient(this.hud.proxy.client, aGrip); michael@0: }, michael@0: getObjectClient: aGrip => { michael@0: return new ObjectClient(this.hud.proxy.client, aGrip); michael@0: }, michael@0: getLongStringClient: aGrip => { michael@0: return this.webConsoleClient.longString(aGrip); michael@0: }, michael@0: releaseActor: aActor => { michael@0: this.hud._releaseObject(aActor); michael@0: }, michael@0: simpleValueEvalMacro: simpleValueEvalMacro, michael@0: overrideValueEvalMacro: overrideValueEvalMacro, michael@0: getterOrSetterEvalMacro: getterOrSetterEvalMacro, michael@0: }); michael@0: michael@0: // Relay events from the VariablesView. michael@0: view.on("fetched", (aEvent, aType, aVar) => { michael@0: this.emit("variablesview-fetched", aVar); michael@0: }); michael@0: michael@0: return view; michael@0: }, michael@0: michael@0: /** michael@0: * Update the variables view. michael@0: * michael@0: * @private michael@0: * @param object aOptions michael@0: * Options for updating the variables view: michael@0: * - view: the view you want to update. michael@0: * - objectActor: the grip of the new ObjectActor you want to show in michael@0: * the view. michael@0: * - rawObject: the new raw object you want to show. michael@0: * - label: the new label for the inspected object. michael@0: */ michael@0: _updateVariablesView: function JST__updateVariablesView(aOptions) michael@0: { michael@0: let view = aOptions.view; michael@0: view.empty(); michael@0: michael@0: // We need to avoid pruning the object inspection starting point. michael@0: // That one is pruned when the console message is removed. michael@0: view.controller.releaseActors(aActor => { michael@0: return view._consoleLastObjectActor != aActor; michael@0: }); michael@0: michael@0: if (aOptions.objectActor && michael@0: (!this.hud.owner._browserConsole || michael@0: Services.prefs.getBoolPref("devtools.chrome.enabled"))) { michael@0: // Make sure eval works in the correct context. michael@0: view.eval = this._variablesViewEvaluate.bind(this, aOptions); michael@0: view.switch = this._variablesViewSwitch.bind(this, aOptions); michael@0: view.delete = this._variablesViewDelete.bind(this, aOptions); michael@0: } michael@0: else { michael@0: view.eval = null; michael@0: view.switch = null; michael@0: view.delete = null; michael@0: } michael@0: michael@0: let { variable, expanded } = view.controller.setSingleVariable(aOptions); michael@0: variable.evaluationMacro = simpleValueEvalMacro; michael@0: michael@0: if (aOptions.objectActor) { michael@0: view._consoleLastObjectActor = aOptions.objectActor.actor; michael@0: } michael@0: else if (aOptions.rawObject) { michael@0: view._consoleLastObjectActor = null; michael@0: } michael@0: else { michael@0: throw new Error("Variables View cannot open without giving it an object " + michael@0: "display."); michael@0: } michael@0: michael@0: expanded.then(() => { michael@0: this.emit("variablesview-updated", view, aOptions); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * The evaluation function used by the variables view when editing a property michael@0: * value. michael@0: * michael@0: * @private michael@0: * @param object aOptions michael@0: * The options used for |this._updateVariablesView()|. michael@0: * @param object aVar michael@0: * The Variable object instance for the edited property. michael@0: * @param string aValue michael@0: * The value the edited property was changed to. michael@0: */ michael@0: _variablesViewEvaluate: michael@0: function JST__variablesViewEvaluate(aOptions, aVar, aValue) michael@0: { michael@0: let updater = this._updateVariablesView.bind(this, aOptions); michael@0: let onEval = this._silentEvalCallback.bind(this, updater); michael@0: let string = aVar.evaluationMacro(aVar, aValue); michael@0: michael@0: let evalOptions = { michael@0: frame: this.SELECTED_FRAME, michael@0: bindObjectActor: aOptions.objectActor.actor, michael@0: }; michael@0: michael@0: this.requestEvaluation(string, evalOptions).then(onEval, onEval); michael@0: }, michael@0: michael@0: /** michael@0: * The property deletion function used by the variables view when a property michael@0: * is deleted. michael@0: * michael@0: * @private michael@0: * @param object aOptions michael@0: * The options used for |this._updateVariablesView()|. michael@0: * @param object aVar michael@0: * The Variable object instance for the deleted property. michael@0: */ michael@0: _variablesViewDelete: function JST__variablesViewDelete(aOptions, aVar) michael@0: { michael@0: let onEval = this._silentEvalCallback.bind(this, null); michael@0: michael@0: let evalOptions = { michael@0: frame: this.SELECTED_FRAME, michael@0: bindObjectActor: aOptions.objectActor.actor, michael@0: }; michael@0: michael@0: this.requestEvaluation("delete _self" + aVar.symbolicName, evalOptions) michael@0: .then(onEval, onEval); michael@0: }, michael@0: michael@0: /** michael@0: * The property rename function used by the variables view when a property michael@0: * is renamed. michael@0: * michael@0: * @private michael@0: * @param object aOptions michael@0: * The options used for |this._updateVariablesView()|. michael@0: * @param object aVar michael@0: * The Variable object instance for the renamed property. michael@0: * @param string aNewName michael@0: * The new name for the property. michael@0: */ michael@0: _variablesViewSwitch: michael@0: function JST__variablesViewSwitch(aOptions, aVar, aNewName) michael@0: { michael@0: let updater = this._updateVariablesView.bind(this, aOptions); michael@0: let onEval = this._silentEvalCallback.bind(this, updater); michael@0: michael@0: let evalOptions = { michael@0: frame: this.SELECTED_FRAME, michael@0: bindObjectActor: aOptions.objectActor.actor, michael@0: }; michael@0: michael@0: let newSymbolicName = aVar.ownerView.symbolicName + '["' + aNewName + '"]'; michael@0: if (newSymbolicName == aVar.symbolicName) { michael@0: return; michael@0: } michael@0: michael@0: let code = "_self" + newSymbolicName + " = _self" + aVar.symbolicName + ";" + michael@0: "delete _self" + aVar.symbolicName; michael@0: michael@0: this.requestEvaluation(code, evalOptions).then(onEval, onEval); michael@0: }, michael@0: michael@0: /** michael@0: * A noop callback for JavaScript evaluation. This method releases any michael@0: * result ObjectActors that come from the server for evaluation requests. This michael@0: * is used for editing, renaming and deleting properties in the variables michael@0: * view. michael@0: * michael@0: * Exceptions are displayed in the output. michael@0: * michael@0: * @private michael@0: * @param function aCallback michael@0: * Function to invoke once the response is received. michael@0: * @param object aResponse michael@0: * The response packet received from the server. michael@0: */ michael@0: _silentEvalCallback: function JST__silentEvalCallback(aCallback, aResponse) michael@0: { michael@0: if (aResponse.error) { michael@0: Cu.reportError("Web Console evaluation failed. " + aResponse.error + ":" + michael@0: aResponse.message); michael@0: michael@0: aCallback && aCallback(aResponse); michael@0: return; michael@0: } michael@0: michael@0: if (aResponse.exceptionMessage) { michael@0: let message = new Messages.Simple(aResponse.exceptionMessage, { michael@0: category: "output", michael@0: severity: "error", michael@0: timestamp: aResponse.timestamp, michael@0: }); michael@0: this.hud.output.addMessage(message); michael@0: message._objectActors = new Set(); michael@0: if (WebConsoleUtils.isActorGrip(aResponse.exception)) { michael@0: message._objectActors.add(aResponse.exception.actor); michael@0: } michael@0: } michael@0: michael@0: let helper = aResponse.helperResult || { type: null }; michael@0: let helperGrip = null; michael@0: if (helper.type == "inspectObject") { michael@0: helperGrip = helper.object; michael@0: } michael@0: michael@0: let grips = [aResponse.result, helperGrip]; michael@0: for (let grip of grips) { michael@0: if (WebConsoleUtils.isActorGrip(grip)) { michael@0: this.hud._releaseObject(grip.actor); michael@0: } michael@0: } michael@0: michael@0: aCallback && aCallback(aResponse); michael@0: }, michael@0: michael@0: michael@0: /** michael@0: * Clear the Web Console output. michael@0: * michael@0: * This method emits the "messages-cleared" notification. michael@0: * michael@0: * @param boolean aClearStorage michael@0: * True if you want to clear the console messages storage associated to michael@0: * this Web Console. michael@0: */ michael@0: clearOutput: function JST_clearOutput(aClearStorage) michael@0: { michael@0: let hud = this.hud; michael@0: let outputNode = hud.outputNode; michael@0: let node; michael@0: while ((node = outputNode.firstChild)) { michael@0: hud.removeOutputMessage(node); michael@0: } michael@0: michael@0: hud.groupDepth = 0; michael@0: hud._outputQueue.forEach(hud._pruneItemFromQueue, hud); michael@0: hud._outputQueue = []; michael@0: hud._networkRequests = {}; michael@0: hud._repeatNodes = {}; michael@0: michael@0: if (aClearStorage) { michael@0: this.webConsoleClient.clearMessagesCache(); michael@0: } michael@0: michael@0: this.emit("messages-cleared"); michael@0: }, michael@0: michael@0: /** michael@0: * Remove all of the private messages from the Web Console output. michael@0: * michael@0: * This method emits the "private-messages-cleared" notification. michael@0: */ michael@0: clearPrivateMessages: function JST_clearPrivateMessages() michael@0: { michael@0: let nodes = this.hud.outputNode.querySelectorAll(".message[private]"); michael@0: for (let node of nodes) { michael@0: this.hud.removeOutputMessage(node); michael@0: } michael@0: this.emit("private-messages-cleared"); michael@0: }, michael@0: michael@0: /** michael@0: * Updates the size of the input field (command line) to fit its contents. michael@0: * michael@0: * @returns void michael@0: */ michael@0: resizeInput: function JST_resizeInput() michael@0: { michael@0: let inputNode = this.inputNode; michael@0: michael@0: // Reset the height so that scrollHeight will reflect the natural height of michael@0: // the contents of the input field. michael@0: inputNode.style.height = "auto"; michael@0: michael@0: // Now resize the input field to fit its contents. michael@0: let scrollHeight = inputNode.inputField.scrollHeight; michael@0: if (scrollHeight > 0) { michael@0: inputNode.style.height = scrollHeight + "px"; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Sets the value of the input field (command line), and resizes the field to michael@0: * fit its contents. This method is preferred over setting "inputNode.value" michael@0: * directly, because it correctly resizes the field. michael@0: * michael@0: * @param string aNewValue michael@0: * The new value to set. michael@0: * @returns void michael@0: */ michael@0: setInputValue: function JST_setInputValue(aNewValue) michael@0: { michael@0: this.inputNode.value = aNewValue; michael@0: this.lastInputValue = aNewValue; michael@0: this.completeNode.value = ""; michael@0: this.resizeInput(); michael@0: this._inputChanged = true; michael@0: }, michael@0: michael@0: /** michael@0: * The inputNode "input" and "keyup" event handler. michael@0: * @private michael@0: */ michael@0: _inputEventHandler: function JST__inputEventHandler() michael@0: { michael@0: if (this.lastInputValue != this.inputNode.value) { michael@0: this.resizeInput(); michael@0: this.complete(this.COMPLETE_HINT_ONLY); michael@0: this.lastInputValue = this.inputNode.value; michael@0: this._inputChanged = true; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The window "blur" event handler. michael@0: * @private michael@0: */ michael@0: _blurEventHandler: function JST__blurEventHandler() michael@0: { michael@0: if (this.autocompletePopup) { michael@0: this.clearCompletion(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The inputNode "keypress" event handler. michael@0: * michael@0: * @private michael@0: * @param nsIDOMEvent aEvent michael@0: */ michael@0: _keyPress: function JST__keyPress(aEvent) michael@0: { michael@0: let inputNode = this.inputNode; michael@0: let inputUpdated = false; michael@0: michael@0: if (aEvent.ctrlKey) { michael@0: switch (aEvent.charCode) { michael@0: case 101: michael@0: // control-e michael@0: if (Services.appinfo.OS == "WINNT") { michael@0: break; michael@0: } michael@0: let lineEndPos = inputNode.value.length; michael@0: if (this.hasMultilineInput()) { michael@0: // find index of closest newline >= cursor michael@0: for (let i = inputNode.selectionEnd; i -1) { michael@0: this.acceptProposedCompletion(); michael@0: } michael@0: else { michael@0: this.execute(); michael@0: this._inputChanged = false; michael@0: } michael@0: aEvent.preventDefault(); michael@0: break; michael@0: michael@0: case Ci.nsIDOMKeyEvent.DOM_VK_UP: michael@0: if (this.autocompletePopup.isOpen) { michael@0: inputUpdated = this.complete(this.COMPLETE_BACKWARD); michael@0: if (inputUpdated) { michael@0: this._autocompletePopupNavigated = true; michael@0: } michael@0: } michael@0: else if (this.canCaretGoPrevious()) { michael@0: inputUpdated = this.historyPeruse(HISTORY_BACK); michael@0: } michael@0: if (inputUpdated) { michael@0: aEvent.preventDefault(); michael@0: } michael@0: break; michael@0: michael@0: case Ci.nsIDOMKeyEvent.DOM_VK_DOWN: michael@0: if (this.autocompletePopup.isOpen) { michael@0: inputUpdated = this.complete(this.COMPLETE_FORWARD); michael@0: if (inputUpdated) { michael@0: this._autocompletePopupNavigated = true; michael@0: } michael@0: } michael@0: else if (this.canCaretGoNext()) { michael@0: inputUpdated = this.historyPeruse(HISTORY_FORWARD); michael@0: } michael@0: if (inputUpdated) { michael@0: aEvent.preventDefault(); michael@0: } michael@0: break; michael@0: michael@0: case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP: michael@0: if (this.autocompletePopup.isOpen) { michael@0: inputUpdated = this.complete(this.COMPLETE_PAGEUP); michael@0: if (inputUpdated) { michael@0: this._autocompletePopupNavigated = true; michael@0: } michael@0: } michael@0: else { michael@0: this.hud.outputNode.parentNode.scrollTop = michael@0: Math.max(0, michael@0: this.hud.outputNode.parentNode.scrollTop - michael@0: this.hud.outputNode.parentNode.clientHeight michael@0: ); michael@0: } michael@0: aEvent.preventDefault(); michael@0: break; michael@0: michael@0: case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN: michael@0: if (this.autocompletePopup.isOpen) { michael@0: inputUpdated = this.complete(this.COMPLETE_PAGEDOWN); michael@0: if (inputUpdated) { michael@0: this._autocompletePopupNavigated = true; michael@0: } michael@0: } michael@0: else { michael@0: this.hud.outputNode.parentNode.scrollTop = michael@0: Math.min(this.hud.outputNode.parentNode.scrollHeight, michael@0: this.hud.outputNode.parentNode.scrollTop + michael@0: this.hud.outputNode.parentNode.clientHeight michael@0: ); michael@0: } michael@0: aEvent.preventDefault(); michael@0: break; michael@0: michael@0: case Ci.nsIDOMKeyEvent.DOM_VK_HOME: michael@0: case Ci.nsIDOMKeyEvent.DOM_VK_END: michael@0: case Ci.nsIDOMKeyEvent.DOM_VK_LEFT: michael@0: if (this.autocompletePopup.isOpen || this.lastCompletion.value) { michael@0: this.clearCompletion(); michael@0: } michael@0: break; michael@0: michael@0: case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT: { michael@0: let cursorAtTheEnd = this.inputNode.selectionStart == michael@0: this.inputNode.selectionEnd && michael@0: this.inputNode.selectionStart == michael@0: this.inputNode.value.length; michael@0: let haveSuggestion = this.autocompletePopup.isOpen || michael@0: this.lastCompletion.value; michael@0: let useCompletion = cursorAtTheEnd || this._autocompletePopupNavigated; michael@0: if (haveSuggestion && useCompletion && michael@0: this.complete(this.COMPLETE_HINT_ONLY) && michael@0: this.lastCompletion.value && michael@0: this.acceptProposedCompletion()) { michael@0: aEvent.preventDefault(); michael@0: } michael@0: if (this.autocompletePopup.isOpen) { michael@0: this.clearCompletion(); michael@0: } michael@0: break; michael@0: } michael@0: case Ci.nsIDOMKeyEvent.DOM_VK_TAB: michael@0: // Generate a completion and accept the first proposed value. michael@0: if (this.complete(this.COMPLETE_HINT_ONLY) && michael@0: this.lastCompletion && michael@0: this.acceptProposedCompletion()) { michael@0: aEvent.preventDefault(); michael@0: } michael@0: else if (this._inputChanged) { michael@0: this.updateCompleteNode(l10n.getStr("Autocomplete.blank")); michael@0: aEvent.preventDefault(); michael@0: } michael@0: break; michael@0: default: michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The inputNode "focus" event handler. michael@0: * @private michael@0: */ michael@0: _focusEventHandler: function JST__focusEventHandler() michael@0: { michael@0: this._inputChanged = false; michael@0: }, michael@0: michael@0: /** michael@0: * Go up/down the history stack of input values. michael@0: * michael@0: * @param number aDirection michael@0: * History navigation direction: HISTORY_BACK or HISTORY_FORWARD. michael@0: * michael@0: * @returns boolean michael@0: * True if the input value changed, false otherwise. michael@0: */ michael@0: historyPeruse: function JST_historyPeruse(aDirection) michael@0: { michael@0: if (!this.history.length) { michael@0: return false; michael@0: } michael@0: michael@0: // Up Arrow key michael@0: if (aDirection == HISTORY_BACK) { michael@0: if (this.historyPlaceHolder <= 0) { michael@0: return false; michael@0: } michael@0: let inputVal = this.history[--this.historyPlaceHolder]; michael@0: michael@0: // Save the current input value as the latest entry in history, only if michael@0: // the user is already at the last entry. michael@0: // Note: this code does not store changes to items that are already in michael@0: // history. michael@0: if (this.historyPlaceHolder+1 == this.historyIndex) { michael@0: this.history[this.historyIndex] = this.inputNode.value || ""; michael@0: } michael@0: michael@0: this.setInputValue(inputVal); michael@0: } michael@0: // Down Arrow key michael@0: else if (aDirection == HISTORY_FORWARD) { michael@0: if (this.historyPlaceHolder >= (this.history.length-1)) { michael@0: return false; michael@0: } michael@0: michael@0: let inputVal = this.history[++this.historyPlaceHolder]; michael@0: this.setInputValue(inputVal); michael@0: } michael@0: else { michael@0: throw new Error("Invalid argument 0"); michael@0: } michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Test for multiline input. michael@0: * michael@0: * @return boolean michael@0: * True if CR or LF found in node value; else false. michael@0: */ michael@0: hasMultilineInput: function JST_hasMultilineInput() michael@0: { michael@0: return /[\r\n]/.test(this.inputNode.value); michael@0: }, michael@0: michael@0: /** michael@0: * Check if the caret is at a location that allows selecting the previous item michael@0: * in history when the user presses the Up arrow key. michael@0: * michael@0: * @return boolean michael@0: * True if the caret is at a location that allows selecting the michael@0: * previous item in history when the user presses the Up arrow key, michael@0: * otherwise false. michael@0: */ michael@0: canCaretGoPrevious: function JST_canCaretGoPrevious() michael@0: { michael@0: let node = this.inputNode; michael@0: if (node.selectionStart != node.selectionEnd) { michael@0: return false; michael@0: } michael@0: michael@0: let multiline = /[\r\n]/.test(node.value); michael@0: return node.selectionStart == 0 ? true : michael@0: node.selectionStart == node.value.length && !multiline; michael@0: }, michael@0: michael@0: /** michael@0: * Check if the caret is at a location that allows selecting the next item in michael@0: * history when the user presses the Down arrow key. michael@0: * michael@0: * @return boolean michael@0: * True if the caret is at a location that allows selecting the next michael@0: * item in history when the user presses the Down arrow key, otherwise michael@0: * false. michael@0: */ michael@0: canCaretGoNext: function JST_canCaretGoNext() michael@0: { michael@0: let node = this.inputNode; michael@0: if (node.selectionStart != node.selectionEnd) { michael@0: return false; michael@0: } michael@0: michael@0: let multiline = /[\r\n]/.test(node.value); michael@0: return node.selectionStart == node.value.length ? true : michael@0: node.selectionStart == 0 && !multiline; michael@0: }, michael@0: michael@0: /** michael@0: * Completes the current typed text in the inputNode. Completion is performed michael@0: * only if the selection/cursor is at the end of the string. If no completion michael@0: * is found, the current inputNode value and cursor/selection stay. michael@0: * michael@0: * @param int aType possible values are michael@0: * - this.COMPLETE_FORWARD: If there is more than one possible completion michael@0: * and the input value stayed the same compared to the last time this michael@0: * function was called, then the next completion of all possible michael@0: * completions is used. If the value changed, then the first possible michael@0: * completion is used and the selection is set from the current michael@0: * cursor position to the end of the completed text. michael@0: * If there is only one possible completion, then this completion michael@0: * value is used and the cursor is put at the end of the completion. michael@0: * - this.COMPLETE_BACKWARD: Same as this.COMPLETE_FORWARD but if the michael@0: * value stayed the same as the last time the function was called, michael@0: * then the previous completion of all possible completions is used. michael@0: * - this.COMPLETE_PAGEUP: Scroll up one page if available or select the first michael@0: * item. michael@0: * - this.COMPLETE_PAGEDOWN: Scroll down one page if available or select the michael@0: * last item. michael@0: * - this.COMPLETE_HINT_ONLY: If there is more than one possible michael@0: * completion and the input value stayed the same compared to the michael@0: * last time this function was called, then the same completion is michael@0: * used again. If there is only one possible completion, then michael@0: * the inputNode.value is set to this value and the selection is set michael@0: * from the current cursor position to the end of the completed text. michael@0: * @param function aCallback michael@0: * Optional function invoked when the autocomplete properties are michael@0: * updated. michael@0: * @returns boolean true if there existed a completion for the current input, michael@0: * or false otherwise. michael@0: */ michael@0: complete: function JSTF_complete(aType, aCallback) michael@0: { michael@0: let inputNode = this.inputNode; michael@0: let inputValue = inputNode.value; michael@0: let frameActor = this.getFrameActor(this.SELECTED_FRAME); michael@0: michael@0: // If the inputNode has no value, then don't try to complete on it. michael@0: if (!inputValue) { michael@0: this.clearCompletion(); michael@0: aCallback && aCallback(this); michael@0: this.emit("autocomplete-updated"); michael@0: return false; michael@0: } michael@0: michael@0: // Only complete if the selection is empty. michael@0: if (inputNode.selectionStart != inputNode.selectionEnd) { michael@0: this.clearCompletion(); michael@0: aCallback && aCallback(this); michael@0: this.emit("autocomplete-updated"); michael@0: return false; michael@0: } michael@0: michael@0: // Update the completion results. michael@0: if (this.lastCompletion.value != inputValue || frameActor != this._lastFrameActorId) { michael@0: this._updateCompletionResult(aType, aCallback); michael@0: return false; michael@0: } michael@0: michael@0: let popup = this.autocompletePopup; michael@0: let accepted = false; michael@0: michael@0: if (aType != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) { michael@0: this.acceptProposedCompletion(); michael@0: accepted = true; michael@0: } michael@0: else if (aType == this.COMPLETE_BACKWARD) { michael@0: popup.selectPreviousItem(); michael@0: } michael@0: else if (aType == this.COMPLETE_FORWARD) { michael@0: popup.selectNextItem(); michael@0: } michael@0: else if (aType == this.COMPLETE_PAGEUP) { michael@0: popup.selectPreviousPageItem(); michael@0: } michael@0: else if (aType == this.COMPLETE_PAGEDOWN) { michael@0: popup.selectNextPageItem(); michael@0: } michael@0: michael@0: aCallback && aCallback(this); michael@0: this.emit("autocomplete-updated"); michael@0: return accepted || popup.itemCount > 0; michael@0: }, michael@0: michael@0: /** michael@0: * Update the completion result. This operation is performed asynchronously by michael@0: * fetching updated results from the content process. michael@0: * michael@0: * @private michael@0: * @param int aType michael@0: * Completion type. See this.complete() for details. michael@0: * @param function [aCallback] michael@0: * Optional, function to invoke when completion results are received. michael@0: */ michael@0: _updateCompletionResult: michael@0: function JST__updateCompletionResult(aType, aCallback) michael@0: { michael@0: let frameActor = this.getFrameActor(this.SELECTED_FRAME); michael@0: if (this.lastCompletion.value == this.inputNode.value && frameActor == this._lastFrameActorId) { michael@0: return; michael@0: } michael@0: michael@0: let requestId = gSequenceId(); michael@0: let cursor = this.inputNode.selectionStart; michael@0: let input = this.inputNode.value.substring(0, cursor); michael@0: let cache = this._autocompleteCache; michael@0: michael@0: // If the current input starts with the previous input, then we already michael@0: // have a list of suggestions and we just need to filter the cached michael@0: // suggestions. When the current input ends with a non-alphanumeric michael@0: // character we ask the server again for suggestions. michael@0: michael@0: // Check if last character is non-alphanumeric michael@0: if (!/[a-zA-Z0-9]$/.test(input) || frameActor != this._lastFrameActorId) { michael@0: this._autocompleteQuery = null; michael@0: this._autocompleteCache = null; michael@0: } michael@0: michael@0: if (this._autocompleteQuery && input.startsWith(this._autocompleteQuery)) { michael@0: let filterBy = input; michael@0: // Find the last non-alphanumeric if exists. michael@0: let lastNonAlpha = input.match(/[^a-zA-Z0-9][a-zA-Z0-9]*$/); michael@0: // If input contains non-alphanumerics, use the part after the last one michael@0: // to filter the cache michael@0: if (lastNonAlpha) { michael@0: filterBy = input.substring(input.lastIndexOf(lastNonAlpha) + 1); michael@0: } michael@0: michael@0: let newList = cache.sort().filter(function(l) { michael@0: return l.startsWith(filterBy); michael@0: }); michael@0: michael@0: this.lastCompletion = { michael@0: requestId: null, michael@0: completionType: aType, michael@0: value: null, michael@0: }; michael@0: michael@0: let response = { matches: newList, matchProp: filterBy }; michael@0: this._receiveAutocompleteProperties(null, aCallback, response); michael@0: return; michael@0: } michael@0: michael@0: this._lastFrameActorId = frameActor; michael@0: michael@0: this.lastCompletion = { michael@0: requestId: requestId, michael@0: completionType: aType, michael@0: value: null, michael@0: }; michael@0: michael@0: let callback = this._receiveAutocompleteProperties.bind(this, requestId, michael@0: aCallback); michael@0: michael@0: this.webConsoleClient.autocomplete(input, cursor, callback, frameActor); michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the autocompletion results. This method takes michael@0: * the completion result received from the server and updates the UI michael@0: * accordingly. michael@0: * michael@0: * @param number aRequestId michael@0: * Request ID. michael@0: * @param function [aCallback=null] michael@0: * Optional, function to invoke when the completion result is received. michael@0: * @param object aMessage michael@0: * The JSON message which holds the completion results received from michael@0: * the content process. michael@0: */ michael@0: _receiveAutocompleteProperties: michael@0: function JST__receiveAutocompleteProperties(aRequestId, aCallback, aMessage) michael@0: { michael@0: let inputNode = this.inputNode; michael@0: let inputValue = inputNode.value; michael@0: if (this.lastCompletion.value == inputValue || michael@0: aRequestId != this.lastCompletion.requestId) { michael@0: return; michael@0: } michael@0: // Cache whatever came from the server if the last char is alphanumeric or '.' michael@0: let cursor = inputNode.selectionStart; michael@0: let inputUntilCursor = inputValue.substring(0, cursor); michael@0: michael@0: if (aRequestId != null && /[a-zA-Z0-9.]$/.test(inputUntilCursor)) { michael@0: this._autocompleteCache = aMessage.matches; michael@0: this._autocompleteQuery = inputUntilCursor; michael@0: } michael@0: michael@0: let matches = aMessage.matches; michael@0: let lastPart = aMessage.matchProp; michael@0: if (!matches.length) { michael@0: this.clearCompletion(); michael@0: aCallback && aCallback(this); michael@0: this.emit("autocomplete-updated"); michael@0: return; michael@0: } michael@0: michael@0: let items = matches.reverse().map(function(aMatch) { michael@0: return { preLabel: lastPart, label: aMatch }; michael@0: }); michael@0: michael@0: let popup = this.autocompletePopup; michael@0: popup.setItems(items); michael@0: michael@0: let completionType = this.lastCompletion.completionType; michael@0: this.lastCompletion = { michael@0: value: inputValue, michael@0: matchProp: lastPart, michael@0: }; michael@0: michael@0: if (items.length > 1 && !popup.isOpen) { michael@0: let str = this.inputNode.value.substr(0, this.inputNode.selectionStart); michael@0: let offset = str.length - (str.lastIndexOf("\n") + 1) - lastPart.length; michael@0: let x = offset * this.hud._inputCharWidth; michael@0: popup.openPopup(inputNode, x + this.hud._chevronWidth); michael@0: this._autocompletePopupNavigated = false; michael@0: } michael@0: else if (items.length < 2 && popup.isOpen) { michael@0: popup.hidePopup(); michael@0: this._autocompletePopupNavigated = false; michael@0: } michael@0: michael@0: if (items.length == 1) { michael@0: popup.selectedIndex = 0; michael@0: } michael@0: michael@0: this.onAutocompleteSelect(); michael@0: michael@0: if (completionType != this.COMPLETE_HINT_ONLY && popup.itemCount == 1) { michael@0: this.acceptProposedCompletion(); michael@0: } michael@0: else if (completionType == this.COMPLETE_BACKWARD) { michael@0: popup.selectPreviousItem(); michael@0: } michael@0: else if (completionType == this.COMPLETE_FORWARD) { michael@0: popup.selectNextItem(); michael@0: } michael@0: michael@0: aCallback && aCallback(this); michael@0: this.emit("autocomplete-updated"); michael@0: }, michael@0: michael@0: onAutocompleteSelect: function JSTF_onAutocompleteSelect() michael@0: { michael@0: // Render the suggestion only if the cursor is at the end of the input. michael@0: if (this.inputNode.selectionStart != this.inputNode.value.length) { michael@0: return; michael@0: } michael@0: michael@0: let currentItem = this.autocompletePopup.selectedItem; michael@0: if (currentItem && this.lastCompletion.value) { michael@0: let suffix = currentItem.label.substring(this.lastCompletion. michael@0: matchProp.length); michael@0: this.updateCompleteNode(suffix); michael@0: } michael@0: else { michael@0: this.updateCompleteNode(""); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Clear the current completion information and close the autocomplete popup, michael@0: * if needed. michael@0: */ michael@0: clearCompletion: function JSTF_clearCompletion() michael@0: { michael@0: this.autocompletePopup.clearItems(); michael@0: this.lastCompletion = { value: null }; michael@0: this.updateCompleteNode(""); michael@0: if (this.autocompletePopup.isOpen) { michael@0: this.autocompletePopup.hidePopup(); michael@0: this._autocompletePopupNavigated = false; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Accept the proposed input completion. michael@0: * michael@0: * @return boolean michael@0: * True if there was a selected completion item and the input value michael@0: * was updated, false otherwise. michael@0: */ michael@0: acceptProposedCompletion: function JSTF_acceptProposedCompletion() michael@0: { michael@0: let updated = false; michael@0: michael@0: let currentItem = this.autocompletePopup.selectedItem; michael@0: if (currentItem && this.lastCompletion.value) { michael@0: let suffix = currentItem.label.substring(this.lastCompletion. michael@0: matchProp.length); michael@0: let cursor = this.inputNode.selectionStart; michael@0: let value = this.inputNode.value; michael@0: this.setInputValue(value.substr(0, cursor) + suffix + value.substr(cursor)); michael@0: let newCursor = cursor + suffix.length; michael@0: this.inputNode.selectionStart = this.inputNode.selectionEnd = newCursor; michael@0: updated = true; michael@0: } michael@0: michael@0: this.clearCompletion(); michael@0: michael@0: return updated; michael@0: }, michael@0: michael@0: /** michael@0: * Update the node that displays the currently selected autocomplete proposal. michael@0: * michael@0: * @param string aSuffix michael@0: * The proposed suffix for the inputNode value. michael@0: */ michael@0: updateCompleteNode: function JSTF_updateCompleteNode(aSuffix) michael@0: { michael@0: // completion prefix = input, with non-control chars replaced by spaces michael@0: let prefix = aSuffix ? this.inputNode.value.replace(/[\S]/g, " ") : ""; michael@0: this.completeNode.value = prefix + aSuffix; michael@0: }, michael@0: michael@0: michael@0: /** michael@0: * Destroy the sidebar. michael@0: * @private michael@0: */ michael@0: _sidebarDestroy: function JST__sidebarDestroy() michael@0: { michael@0: if (this._variablesView) { michael@0: this._variablesView.controller.releaseActors(); michael@0: this._variablesView = null; michael@0: } michael@0: michael@0: if (this.sidebar) { michael@0: this.sidebar.hide(); michael@0: this.sidebar.destroy(); michael@0: this.sidebar = null; michael@0: } michael@0: michael@0: this.emit("sidebar-closed"); michael@0: }, michael@0: michael@0: /** michael@0: * Destroy the JSTerm object. Call this method to avoid memory leaks. michael@0: */ michael@0: destroy: function JST_destroy() michael@0: { michael@0: this._sidebarDestroy(); michael@0: michael@0: this.clearCompletion(); michael@0: this.clearOutput(); michael@0: michael@0: this.autocompletePopup.destroy(); michael@0: this.autocompletePopup = null; michael@0: michael@0: let popup = this.hud.owner.chromeWindow.document michael@0: .getElementById("webConsole_autocompletePopup"); michael@0: if (popup) { michael@0: popup.parentNode.removeChild(popup); michael@0: } michael@0: michael@0: this.inputNode.removeEventListener("keypress", this._keyPress, false); michael@0: this.inputNode.removeEventListener("input", this._inputEventHandler, false); michael@0: this.inputNode.removeEventListener("keyup", this._inputEventHandler, false); michael@0: this.inputNode.removeEventListener("focus", this._focusEventHandler, false); michael@0: this.hud.window.removeEventListener("blur", this._blurEventHandler, false); michael@0: michael@0: this.hud = null; michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * Utils: a collection of globally used functions. michael@0: */ michael@0: var Utils = { michael@0: /** michael@0: * Scrolls a node so that it's visible in its containing element. michael@0: * michael@0: * @param nsIDOMNode aNode michael@0: * The node to make visible. michael@0: * @returns void michael@0: */ michael@0: scrollToVisible: function Utils_scrollToVisible(aNode) michael@0: { michael@0: aNode.scrollIntoView(false); michael@0: }, michael@0: michael@0: /** michael@0: * Check if the given output node is scrolled to the bottom. michael@0: * michael@0: * @param nsIDOMNode aOutputNode michael@0: * @return boolean michael@0: * True if the output node is scrolled to the bottom, or false michael@0: * otherwise. michael@0: */ michael@0: isOutputScrolledToBottom: function Utils_isOutputScrolledToBottom(aOutputNode) michael@0: { michael@0: let lastNodeHeight = aOutputNode.lastChild ? michael@0: aOutputNode.lastChild.clientHeight : 0; michael@0: let scrollNode = aOutputNode.parentNode; michael@0: return scrollNode.scrollTop + scrollNode.clientHeight >= michael@0: scrollNode.scrollHeight - lastNodeHeight / 2; michael@0: }, michael@0: michael@0: /** michael@0: * Determine the category of a given nsIScriptError. michael@0: * michael@0: * @param nsIScriptError aScriptError michael@0: * The script error you want to determine the category for. michael@0: * @return CATEGORY_JS|CATEGORY_CSS|CATEGORY_SECURITY michael@0: * Depending on the script error CATEGORY_JS, CATEGORY_CSS, or michael@0: * CATEGORY_SECURITY can be returned. michael@0: */ michael@0: categoryForScriptError: function Utils_categoryForScriptError(aScriptError) michael@0: { michael@0: let category = aScriptError.category; michael@0: michael@0: if (/^(?:CSS|Layout)\b/.test(category)) { michael@0: return CATEGORY_CSS; michael@0: } michael@0: michael@0: switch (category) { michael@0: case "Mixed Content Blocker": michael@0: case "Mixed Content Message": michael@0: case "CSP": michael@0: case "Invalid HSTS Headers": michael@0: case "Insecure Password Field": michael@0: case "SSL": michael@0: case "CORS": michael@0: return CATEGORY_SECURITY; michael@0: michael@0: default: michael@0: return CATEGORY_JS; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Retrieve the limit of messages for a specific category. michael@0: * michael@0: * @param number aCategory michael@0: * The category of messages you want to retrieve the limit for. See the michael@0: * CATEGORY_* constants. michael@0: * @return number michael@0: * The number of messages allowed for the specific category. michael@0: */ michael@0: logLimitForCategory: function Utils_logLimitForCategory(aCategory) michael@0: { michael@0: let logLimit = DEFAULT_LOG_LIMIT; michael@0: michael@0: try { michael@0: let prefName = CATEGORY_CLASS_FRAGMENTS[aCategory]; michael@0: logLimit = Services.prefs.getIntPref("devtools.hud.loglimit." + prefName); michael@0: logLimit = Math.max(logLimit, 1); michael@0: } michael@0: catch (e) { } michael@0: michael@0: return logLimit; michael@0: }, michael@0: }; michael@0: michael@0: /////////////////////////////////////////////////////////////////////////////// michael@0: // CommandController michael@0: /////////////////////////////////////////////////////////////////////////////// michael@0: michael@0: /** michael@0: * A controller (an instance of nsIController) that makes editing actions michael@0: * behave appropriately in the context of the Web Console. michael@0: */ michael@0: function CommandController(aWebConsole) michael@0: { michael@0: this.owner = aWebConsole; michael@0: } michael@0: michael@0: CommandController.prototype = { michael@0: /** michael@0: * Selects all the text in the HUD output. michael@0: */ michael@0: selectAll: function CommandController_selectAll() michael@0: { michael@0: this.owner.output.selectAllMessages(); michael@0: }, michael@0: michael@0: /** michael@0: * Open the URL of the selected message in a new tab. michael@0: */ michael@0: openURL: function CommandController_openURL() michael@0: { michael@0: this.owner.openSelectedItemInTab(); michael@0: }, michael@0: michael@0: copyURL: function CommandController_copyURL() michael@0: { michael@0: this.owner.copySelectedItems({ linkOnly: true, contextmenu: true }); michael@0: }, michael@0: michael@0: supportsCommand: function CommandController_supportsCommand(aCommand) michael@0: { michael@0: if (!this.owner || !this.owner.output) { michael@0: return false; michael@0: } michael@0: return this.isCommandEnabled(aCommand); michael@0: }, michael@0: michael@0: isCommandEnabled: function CommandController_isCommandEnabled(aCommand) michael@0: { michael@0: switch (aCommand) { michael@0: case "consoleCmd_openURL": michael@0: case "consoleCmd_copyURL": { michael@0: // Only enable URL-related actions if node is Net Activity. michael@0: let selectedItem = this.owner.output.getSelectedMessages(1)[0] || michael@0: this.owner._contextMenuHandler.lastClickedMessage; michael@0: return selectedItem && "url" in selectedItem; michael@0: } michael@0: case "consoleCmd_clearOutput": michael@0: case "cmd_selectAll": michael@0: case "cmd_find": michael@0: return true; michael@0: case "cmd_fontSizeEnlarge": michael@0: case "cmd_fontSizeReduce": michael@0: case "cmd_fontSizeReset": michael@0: case "cmd_close": michael@0: return this.owner.owner._browserConsole; michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: doCommand: function CommandController_doCommand(aCommand) michael@0: { michael@0: switch (aCommand) { michael@0: case "consoleCmd_openURL": michael@0: this.openURL(); michael@0: break; michael@0: case "consoleCmd_copyURL": michael@0: this.copyURL(); michael@0: break; michael@0: case "consoleCmd_clearOutput": michael@0: this.owner.jsterm.clearOutput(true); michael@0: break; michael@0: case "cmd_find": michael@0: this.owner.filterBox.focus(); michael@0: break; michael@0: case "cmd_selectAll": michael@0: this.selectAll(); michael@0: break; michael@0: case "cmd_fontSizeEnlarge": michael@0: this.owner.changeFontSize("+"); michael@0: break; michael@0: case "cmd_fontSizeReduce": michael@0: this.owner.changeFontSize("-"); michael@0: break; michael@0: case "cmd_fontSizeReset": michael@0: this.owner.changeFontSize(""); michael@0: break; michael@0: case "cmd_close": michael@0: this.owner.window.close(); michael@0: break; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /////////////////////////////////////////////////////////////////////////////// michael@0: // Web Console connection proxy michael@0: /////////////////////////////////////////////////////////////////////////////// michael@0: michael@0: /** michael@0: * The WebConsoleConnectionProxy handles the connection between the Web Console michael@0: * and the application we connect to through the remote debug protocol. michael@0: * michael@0: * @constructor michael@0: * @param object aWebConsole michael@0: * The Web Console instance that owns this connection proxy. michael@0: * @param RemoteTarget aTarget michael@0: * The target that the console will connect to. michael@0: */ michael@0: function WebConsoleConnectionProxy(aWebConsole, aTarget) michael@0: { michael@0: this.owner = aWebConsole; michael@0: this.target = aTarget; michael@0: michael@0: this._onPageError = this._onPageError.bind(this); michael@0: this._onLogMessage = this._onLogMessage.bind(this); michael@0: this._onConsoleAPICall = this._onConsoleAPICall.bind(this); michael@0: this._onNetworkEvent = this._onNetworkEvent.bind(this); michael@0: this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this); michael@0: this._onFileActivity = this._onFileActivity.bind(this); michael@0: this._onReflowActivity = this._onReflowActivity.bind(this); michael@0: this._onTabNavigated = this._onTabNavigated.bind(this); michael@0: this._onAttachConsole = this._onAttachConsole.bind(this); michael@0: this._onCachedMessages = this._onCachedMessages.bind(this); michael@0: this._connectionTimeout = this._connectionTimeout.bind(this); michael@0: this._onLastPrivateContextExited = this._onLastPrivateContextExited.bind(this); michael@0: } michael@0: michael@0: WebConsoleConnectionProxy.prototype = { michael@0: /** michael@0: * The owning Web Console instance. michael@0: * michael@0: * @see WebConsoleFrame michael@0: * @type object michael@0: */ michael@0: owner: null, michael@0: michael@0: /** michael@0: * The target that the console connects to. michael@0: * @type RemoteTarget michael@0: */ michael@0: target: null, michael@0: michael@0: /** michael@0: * The DebuggerClient object. michael@0: * michael@0: * @see DebuggerClient michael@0: * @type object michael@0: */ michael@0: client: null, michael@0: michael@0: /** michael@0: * The WebConsoleClient object. michael@0: * michael@0: * @see WebConsoleClient michael@0: * @type object michael@0: */ michael@0: webConsoleClient: null, michael@0: michael@0: /** michael@0: * Tells if the connection is established. michael@0: * @type boolean michael@0: */ michael@0: connected: false, michael@0: michael@0: /** michael@0: * Timer used for the connection. michael@0: * @private michael@0: * @type object michael@0: */ michael@0: _connectTimer: null, michael@0: michael@0: _connectDefer: null, michael@0: _disconnecter: null, michael@0: michael@0: /** michael@0: * The WebConsoleActor ID. michael@0: * michael@0: * @private michael@0: * @type string michael@0: */ michael@0: _consoleActor: null, michael@0: michael@0: /** michael@0: * Tells if the window.console object of the remote web page is the native michael@0: * object or not. michael@0: * @private michael@0: * @type boolean michael@0: */ michael@0: _hasNativeConsoleAPI: false, michael@0: michael@0: /** michael@0: * Initialize a debugger client and connect it to the debugger server. michael@0: * michael@0: * @return object michael@0: * A promise object that is resolved/rejected based on the success of michael@0: * the connection initialization. michael@0: */ michael@0: connect: function WCCP_connect() michael@0: { michael@0: if (this._connectDefer) { michael@0: return this._connectDefer.promise; michael@0: } michael@0: michael@0: this._connectDefer = promise.defer(); michael@0: michael@0: let timeout = Services.prefs.getIntPref(PREF_CONNECTION_TIMEOUT); michael@0: this._connectTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: this._connectTimer.initWithCallback(this._connectionTimeout, michael@0: timeout, Ci.nsITimer.TYPE_ONE_SHOT); michael@0: michael@0: let connPromise = this._connectDefer.promise; michael@0: connPromise.then(function _onSucess() { michael@0: this._connectTimer.cancel(); michael@0: this._connectTimer = null; michael@0: }.bind(this), function _onFailure() { michael@0: this._connectTimer = null; michael@0: }.bind(this)); michael@0: michael@0: let client = this.client = this.target.client; michael@0: michael@0: client.addListener("logMessage", this._onLogMessage); michael@0: client.addListener("pageError", this._onPageError); michael@0: client.addListener("consoleAPICall", this._onConsoleAPICall); michael@0: client.addListener("networkEvent", this._onNetworkEvent); michael@0: client.addListener("networkEventUpdate", this._onNetworkEventUpdate); michael@0: client.addListener("fileActivity", this._onFileActivity); michael@0: client.addListener("reflowActivity", this._onReflowActivity); michael@0: client.addListener("lastPrivateContextExited", this._onLastPrivateContextExited); michael@0: this.target.on("will-navigate", this._onTabNavigated); michael@0: this.target.on("navigate", this._onTabNavigated); michael@0: michael@0: this._consoleActor = this.target.form.consoleActor; michael@0: if (!this.target.chrome) { michael@0: let tab = this.target.form; michael@0: this.owner.onLocationChange(tab.url, tab.title); michael@0: } michael@0: this._attachConsole(); michael@0: michael@0: return connPromise; michael@0: }, michael@0: michael@0: /** michael@0: * Connection timeout handler. michael@0: * @private michael@0: */ michael@0: _connectionTimeout: function WCCP__connectionTimeout() michael@0: { michael@0: let error = { michael@0: error: "timeout", michael@0: message: l10n.getStr("connectionTimeout"), michael@0: }; michael@0: michael@0: this._connectDefer.reject(error); michael@0: }, michael@0: michael@0: /** michael@0: * Attach to the Web Console actor. michael@0: * @private michael@0: */ michael@0: _attachConsole: function WCCP__attachConsole() michael@0: { michael@0: let listeners = ["PageError", "ConsoleAPI", "NetworkActivity", michael@0: "FileActivity"]; michael@0: this.client.attachConsole(this._consoleActor, listeners, michael@0: this._onAttachConsole); michael@0: }, michael@0: michael@0: /** michael@0: * The "attachConsole" response handler. michael@0: * michael@0: * @private michael@0: * @param object aResponse michael@0: * The JSON response object received from the server. michael@0: * @param object aWebConsoleClient michael@0: * The WebConsoleClient instance for the attached console, for the michael@0: * specific tab we work with. michael@0: */ michael@0: _onAttachConsole: function WCCP__onAttachConsole(aResponse, aWebConsoleClient) michael@0: { michael@0: if (aResponse.error) { michael@0: Cu.reportError("attachConsole failed: " + aResponse.error + " " + michael@0: aResponse.message); michael@0: this._connectDefer.reject(aResponse); michael@0: return; michael@0: } michael@0: michael@0: this.webConsoleClient = aWebConsoleClient; michael@0: michael@0: this._hasNativeConsoleAPI = aResponse.nativeConsoleAPI; michael@0: michael@0: let msgs = ["PageError", "ConsoleAPI"]; michael@0: this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages); michael@0: michael@0: this.owner._updateReflowActivityListener(); michael@0: }, michael@0: michael@0: /** michael@0: * The "cachedMessages" response handler. michael@0: * michael@0: * @private michael@0: * @param object aResponse michael@0: * The JSON response object received from the server. michael@0: */ michael@0: _onCachedMessages: function WCCP__onCachedMessages(aResponse) michael@0: { michael@0: if (aResponse.error) { michael@0: Cu.reportError("Web Console getCachedMessages error: " + aResponse.error + michael@0: " " + aResponse.message); michael@0: this._connectDefer.reject(aResponse); michael@0: return; michael@0: } michael@0: michael@0: if (!this._connectTimer) { michael@0: // This happens if the promise is rejected (eg. a timeout), but the michael@0: // connection attempt is successful, nonetheless. michael@0: Cu.reportError("Web Console getCachedMessages error: invalid state."); michael@0: } michael@0: michael@0: this.owner.displayCachedMessages(aResponse.messages); michael@0: michael@0: if (!this._hasNativeConsoleAPI) { michael@0: this.owner.logWarningAboutReplacedAPI(); michael@0: } michael@0: michael@0: this.connected = true; michael@0: this._connectDefer.resolve(this); michael@0: }, michael@0: michael@0: /** michael@0: * The "pageError" message type handler. We redirect any page errors to the UI michael@0: * for displaying. michael@0: * michael@0: * @private michael@0: * @param string aType michael@0: * Message type. michael@0: * @param object aPacket michael@0: * The message received from the server. michael@0: */ michael@0: _onPageError: function WCCP__onPageError(aType, aPacket) michael@0: { michael@0: if (this.owner && aPacket.from == this._consoleActor) { michael@0: this.owner.handlePageError(aPacket.pageError); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The "logMessage" message type handler. We redirect any message to the UI michael@0: * for displaying. michael@0: * michael@0: * @private michael@0: * @param string aType michael@0: * Message type. michael@0: * @param object aPacket michael@0: * The message received from the server. michael@0: */ michael@0: _onLogMessage: function WCCP__onLogMessage(aType, aPacket) michael@0: { michael@0: if (this.owner && aPacket.from == this._consoleActor) { michael@0: this.owner.handleLogMessage(aPacket); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The "consoleAPICall" message type handler. We redirect any message to michael@0: * the UI for displaying. michael@0: * michael@0: * @private michael@0: * @param string aType michael@0: * Message type. michael@0: * @param object aPacket michael@0: * The message received from the server. michael@0: */ michael@0: _onConsoleAPICall: function WCCP__onConsoleAPICall(aType, aPacket) michael@0: { michael@0: if (this.owner && aPacket.from == this._consoleActor) { michael@0: this.owner.handleConsoleAPICall(aPacket.message); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The "networkEvent" message type handler. We redirect any message to michael@0: * the UI for displaying. michael@0: * michael@0: * @private michael@0: * @param string aType michael@0: * Message type. michael@0: * @param object aPacket michael@0: * The message received from the server. michael@0: */ michael@0: _onNetworkEvent: function WCCP__onNetworkEvent(aType, aPacket) michael@0: { michael@0: if (this.owner && aPacket.from == this._consoleActor) { michael@0: this.owner.handleNetworkEvent(aPacket.eventActor); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The "networkEventUpdate" message type handler. We redirect any message to michael@0: * the UI for displaying. michael@0: * michael@0: * @private michael@0: * @param string aType michael@0: * Message type. michael@0: * @param object aPacket michael@0: * The message received from the server. michael@0: */ michael@0: _onNetworkEventUpdate: function WCCP__onNetworkEvenUpdatet(aType, aPacket) michael@0: { michael@0: if (this.owner) { michael@0: this.owner.handleNetworkEventUpdate(aPacket.from, aPacket.updateType, michael@0: aPacket); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The "fileActivity" message type handler. We redirect any message to michael@0: * the UI for displaying. michael@0: * michael@0: * @private michael@0: * @param string aType michael@0: * Message type. michael@0: * @param object aPacket michael@0: * The message received from the server. michael@0: */ michael@0: _onFileActivity: function WCCP__onFileActivity(aType, aPacket) michael@0: { michael@0: if (this.owner && aPacket.from == this._consoleActor) { michael@0: this.owner.handleFileActivity(aPacket.uri); michael@0: } michael@0: }, michael@0: michael@0: _onReflowActivity: function WCCP__onReflowActivity(aType, aPacket) michael@0: { michael@0: if (this.owner && aPacket.from == this._consoleActor) { michael@0: this.owner.handleReflowActivity(aPacket); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The "lastPrivateContextExited" message type handler. When this message is michael@0: * received the Web Console UI is cleared. michael@0: * michael@0: * @private michael@0: * @param string aType michael@0: * Message type. michael@0: * @param object aPacket michael@0: * The message received from the server. michael@0: */ michael@0: _onLastPrivateContextExited: michael@0: function WCCP__onLastPrivateContextExited(aType, aPacket) michael@0: { michael@0: if (this.owner && aPacket.from == this._consoleActor) { michael@0: this.owner.jsterm.clearPrivateMessages(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * The "will-navigate" and "navigate" event handlers. We redirect any message michael@0: * to the UI for displaying. michael@0: * michael@0: * @private michael@0: * @param string aEvent michael@0: * Event type. michael@0: * @param object aPacket michael@0: * The message received from the server. michael@0: */ michael@0: _onTabNavigated: function WCCP__onTabNavigated(aEvent, aPacket) michael@0: { michael@0: if (!this.owner) { michael@0: return; michael@0: } michael@0: michael@0: this.owner.handleTabNavigated(aEvent, aPacket); michael@0: }, michael@0: michael@0: /** michael@0: * Release an object actor. michael@0: * michael@0: * @param string aActor michael@0: * The actor ID to send the request to. michael@0: */ michael@0: releaseActor: function WCCP_releaseActor(aActor) michael@0: { michael@0: if (this.client) { michael@0: this.client.release(aActor); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Disconnect the Web Console from the remote server. michael@0: * michael@0: * @return object michael@0: * A promise object that is resolved when disconnect completes. michael@0: */ michael@0: disconnect: function WCCP_disconnect() michael@0: { michael@0: if (this._disconnecter) { michael@0: return this._disconnecter.promise; michael@0: } michael@0: michael@0: this._disconnecter = promise.defer(); michael@0: michael@0: if (!this.client) { michael@0: this._disconnecter.resolve(null); michael@0: return this._disconnecter.promise; michael@0: } michael@0: michael@0: this.client.removeListener("logMessage", this._onLogMessage); michael@0: this.client.removeListener("pageError", this._onPageError); michael@0: this.client.removeListener("consoleAPICall", this._onConsoleAPICall); michael@0: this.client.removeListener("networkEvent", this._onNetworkEvent); michael@0: this.client.removeListener("networkEventUpdate", this._onNetworkEventUpdate); michael@0: this.client.removeListener("fileActivity", this._onFileActivity); michael@0: this.client.removeListener("reflowActivity", this._onReflowActivity); michael@0: this.client.removeListener("lastPrivateContextExited", this._onLastPrivateContextExited); michael@0: this.target.off("will-navigate", this._onTabNavigated); michael@0: this.target.off("navigate", this._onTabNavigated); michael@0: michael@0: this.client = null; michael@0: this.webConsoleClient = null; michael@0: this.target = null; michael@0: this.connected = false; michael@0: this.owner = null; michael@0: this._disconnecter.resolve(null); michael@0: michael@0: return this._disconnecter.promise; michael@0: }, michael@0: }; michael@0: michael@0: function gSequenceId() michael@0: { michael@0: return gSequenceId.n++; michael@0: } michael@0: gSequenceId.n = 0; michael@0: michael@0: /////////////////////////////////////////////////////////////////////////////// michael@0: // Context Menu michael@0: /////////////////////////////////////////////////////////////////////////////// michael@0: michael@0: /* michael@0: * ConsoleContextMenu this used to handle the visibility of context menu items. michael@0: * michael@0: * @constructor michael@0: * @param object aOwner michael@0: * The WebConsoleFrame instance that owns this object. michael@0: */ michael@0: function ConsoleContextMenu(aOwner) michael@0: { michael@0: this.owner = aOwner; michael@0: this.popup = this.owner.document.getElementById("output-contextmenu"); michael@0: this.build = this.build.bind(this); michael@0: this.popup.addEventListener("popupshowing", this.build); michael@0: } michael@0: michael@0: ConsoleContextMenu.prototype = { michael@0: lastClickedMessage: null, michael@0: michael@0: /* michael@0: * Handle to show/hide context menu item. michael@0: */ michael@0: build: function CCM_build(aEvent) michael@0: { michael@0: let metadata = this.getSelectionMetadata(aEvent.rangeParent); michael@0: for (let element of this.popup.children) { michael@0: element.hidden = this.shouldHideMenuItem(element, metadata); michael@0: } michael@0: }, michael@0: michael@0: /* michael@0: * Get selection information from the view. michael@0: * michael@0: * @param nsIDOMElement aClickElement michael@0: * The DOM element the user clicked on. michael@0: * @return object michael@0: * Selection metadata. michael@0: */ michael@0: getSelectionMetadata: function CCM_getSelectionMetadata(aClickElement) michael@0: { michael@0: let metadata = { michael@0: selectionType: "", michael@0: selection: new Set(), michael@0: }; michael@0: let selectedItems = this.owner.output.getSelectedMessages(); michael@0: if (!selectedItems.length) { michael@0: let clickedItem = this.owner.output.getMessageForElement(aClickElement); michael@0: if (clickedItem) { michael@0: this.lastClickedMessage = clickedItem; michael@0: selectedItems = [clickedItem]; michael@0: } michael@0: } michael@0: michael@0: metadata.selectionType = selectedItems.length > 1 ? "multiple" : "single"; michael@0: michael@0: let selection = metadata.selection; michael@0: for (let item of selectedItems) { michael@0: switch (item.category) { michael@0: case CATEGORY_NETWORK: michael@0: selection.add("network"); michael@0: break; michael@0: case CATEGORY_CSS: michael@0: selection.add("css"); michael@0: break; michael@0: case CATEGORY_JS: michael@0: selection.add("js"); michael@0: break; michael@0: case CATEGORY_WEBDEV: michael@0: selection.add("webdev"); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: return metadata; michael@0: }, michael@0: michael@0: /* michael@0: * Determine if an item should be hidden. michael@0: * michael@0: * @param nsIDOMElement aMenuItem michael@0: * @param object aMetadata michael@0: * @return boolean michael@0: * Whether the given item should be hidden or not. michael@0: */ michael@0: shouldHideMenuItem: function CCM_shouldHideMenuItem(aMenuItem, aMetadata) michael@0: { michael@0: let selectionType = aMenuItem.getAttribute("selectiontype"); michael@0: if (selectionType && !aMetadata.selectionType == selectionType) { michael@0: return true; michael@0: } michael@0: michael@0: let selection = aMenuItem.getAttribute("selection"); michael@0: if (!selection) { michael@0: return false; michael@0: } michael@0: michael@0: let shouldHide = true; michael@0: let itemData = selection.split("|"); michael@0: for (let type of aMetadata.selection) { michael@0: // check whether this menu item should show or not. michael@0: if (itemData.indexOf(type) !== -1) { michael@0: shouldHide = false; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: return shouldHide; michael@0: }, michael@0: michael@0: /** michael@0: * Destroy the ConsoleContextMenu object instance. michael@0: */ michael@0: destroy: function CCM_destroy() michael@0: { michael@0: this.popup.removeEventListener("popupshowing", this.build); michael@0: this.popup = null; michael@0: this.owner = null; michael@0: this.lastClickedMessage = null; michael@0: }, michael@0: }; michael@0: