/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); var EXPORTED_SYMBOLS = ["WebConsoleUtils", "JSPropertyProvider", "PageErrorListener"]; const TYPES = { OBJECT: 0, FUNCTION: 1, ARRAY: 2, OTHER: 3, ITERATOR: 4, GETTER: 5, GENERATOR: 6, STRING: 7 }; var gObjectId = 0; var WebConsoleUtils = { TYPES: TYPES, /** * Convenience function to unwrap a wrapped object. * * @param aObject the object to unwrap. * @return aObject unwrapped. */ unwrap: function WCU_unwrap(aObject) { try { return XPCNativeWrapper.unwrap(aObject); } catch (ex) { return aObject; } }, /** * Wrap a string in an nsISupportsString object. * * @param string aString * @return nsISupportsString */ supportsString: function WCU_supportsString(aString) { let str = Cc["@mozilla.org/supports-string;1"]. createInstance(Ci.nsISupportsString); str.data = aString; return str; }, /** * Clone an object. * * @param object aObject * The object you want cloned. * @param boolean aRecursive * Tells if you want to dig deeper into the object, to clone * recursively. * @param function [aFilter] * Optional, filter function, called for every property. Three * arguments are passed: key, value and object. Return true if the * property should be added to the cloned object. Return false to skip * the property. * @return object * The cloned object. */ cloneObject: function WCU_cloneObject(aObject, aRecursive, aFilter) { if (typeof aObject != "object") { return aObject; } let temp; if (Array.isArray(aObject)) { temp = []; Array.forEach(aObject, function(aValue, aIndex) { if (!aFilter || aFilter(aIndex, aValue, aObject)) { temp.push(aRecursive ? WCU_cloneObject(aValue) : aValue); } }); } else { temp = {}; for (let key in aObject) { let value = aObject[key]; if (aObject.hasOwnProperty(key) && (!aFilter || aFilter(key, value, aObject))) { temp[key] = aRecursive ? WCU_cloneObject(value) : value; } } } return temp; }, /** * Gets the ID of the inner window of this DOM window. * * @param nsIDOMWindow aWindow * @return integer * Inner ID for the given aWindow. */ getInnerWindowId: function WCU_getInnerWindowId(aWindow) { return aWindow.QueryInterface(Ci.nsIInterfaceRequestor). getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; }, /** * Gets the ID of the outer window of this DOM window. * * @param nsIDOMWindow aWindow * @return integer * Outer ID for the given aWindow. */ getOuterWindowId: function WCU_getOuterWindowId(aWindow) { return aWindow.QueryInterface(Ci.nsIInterfaceRequestor). getInterface(Ci.nsIDOMWindowUtils).outerWindowID; }, /** * Gets the window that has the given outer ID. * * @param integer aOuterId * @param nsIDOMWindow [aHintWindow] * Optional, the window object used to QueryInterface to * nsIDOMWindowUtils. If this is not given, * Services.wm.getMostRecentWindow() is used. * @return nsIDOMWindow|null * The window object with the given outer ID. */ getWindowByOuterId: function WCU_getWindowByOuterId(aOuterId, aHintWindow) { let someWindow = aHintWindow || Services.wm.getMostRecentWindow(null); let content = null; if (someWindow) { let windowUtils = someWindow.QueryInterface(Ci.nsIInterfaceRequestor). getInterface(Ci.nsIDOMWindowUtils); content = windowUtils.getOuterWindowWithId(aOuterId); } return content; }, /** * Abbreviates the given source URL so that it can be displayed flush-right * without being too distracting. * * @param string aSourceURL * The source URL to shorten. * @return string * The abbreviated form of the source URL. */ abbreviateSourceURL: function WCU_abbreviateSourceURL(aSourceURL) { // Remove any query parameters. let hookIndex = aSourceURL.indexOf("?"); if (hookIndex > -1) { aSourceURL = aSourceURL.substring(0, hookIndex); } // Remove a trailing "/". if (aSourceURL[aSourceURL.length - 1] == "/") { aSourceURL = aSourceURL.substring(0, aSourceURL.length - 1); } // Remove all but the last path component. let slashIndex = aSourceURL.lastIndexOf("/"); if (slashIndex > -1) { aSourceURL = aSourceURL.substring(slashIndex + 1); } return aSourceURL; }, /** * Format the jsterm execution result based on its type. * * @param mixed aResult * The evaluation result object you want displayed. * @return string * The string that can be displayed. */ formatResult: function WCU_formatResult(aResult) { let output = ""; let type = this.getResultType(aResult); switch (type) { case "string": output = this.formatResultString(aResult); break; case "boolean": case "date": case "error": case "number": case "regexp": output = aResult.toString(); break; case "null": case "undefined": output = type; break; default: try { if (aResult.toSource) { output = aResult.toSource(); } if (!output || output == "({})") { output = aResult + ""; } } catch (ex) { output = ex; } break; } return output + ""; }, /** * Format a string for output. * * @param string aString * The string you want to display. * @return string * The string that can be displayed. */ formatResultString: function WCU_formatResultString(aString) { function isControlCode(c) { // See http://en.wikipedia.org/wiki/C0_and_C1_control_codes // C0 is 0x00-0x1F, C1 is 0x80-0x9F (inclusive). // We also include DEL (U+007F) and NBSP (U+00A0), which are not strictly // in C1 but border it. return (c <= 0x1F) || (0x7F <= c && c <= 0xA0); } function replaceFn(aMatch, aType, aHex) { // Leave control codes escaped, but unescape the rest of the characters. let c = parseInt(aHex, 16); return isControlCode(c) ? aMatch : String.fromCharCode(c); } let output = uneval(aString).replace(/\\(x)([0-9a-fA-F]{2})/g, replaceFn) .replace(/\\(u)([0-9a-fA-F]{4})/g, replaceFn); return output; }, /** * Determine if an object can be inspected or not. * * @param mixed aObject * The object you want to check if it can be inspected. * @return boolean * True if the object is inspectable or false otherwise. */ isObjectInspectable: function WCU_isObjectInspectable(aObject) { let isEnumerable = false; // Skip Iterators and Generators. if (this.isIteratorOrGenerator(aObject)) { return false; } try { for (let p in aObject) { isEnumerable = true; break; } } catch (ex) { // Proxy objects can lack an enumerable method. } return isEnumerable && typeof(aObject) != "string"; }, /** * Determine the type of the jsterm execution result. * * @param mixed aResult * The evaluation result object you want to check. * @return string * Constructor name or type: string, number, boolean, regexp, date, * function, object, null, undefined... */ getResultType: function WCU_getResultType(aResult) { let type = aResult === null ? "null" : typeof aResult; if (type == "object" && aResult.constructor && aResult.constructor.name) { type = aResult.constructor.name; } return type.toLowerCase(); }, /** * Figures out the type of aObject and the string to display as the object * value. * * @see TYPES * @param object aObject * The object to operate on. * @return object * An object of the form: * { * type: TYPES.OBJECT || TYPES.FUNCTION || ... * display: string for displaying the object * } */ presentableValueFor: function WCU_presentableValueFor(aObject) { let type = this.getResultType(aObject); let presentable; switch (type) { case "undefined": case "null": return { type: TYPES.OTHER, display: type }; case "array": return { type: TYPES.ARRAY, display: "Array" }; case "string": return { type: TYPES.STRING, display: "\"" + aObject + "\"" }; case "date": case "regexp": case "number": case "boolean": return { type: TYPES.OTHER, display: aObject.toString() }; case "iterator": return { type: TYPES.ITERATOR, display: "Iterator" }; case "function": presentable = aObject.toString(); return { type: TYPES.FUNCTION, display: presentable.substring(0, presentable.indexOf(')') + 1) }; default: presentable = String(aObject); let m = /^\[object (\S+)\]/.exec(presentable); try { if (typeof aObject == "object" && typeof aObject.next == "function" && m && m[1] == "Generator") { return { type: TYPES.GENERATOR, display: m[1] }; } } catch (ex) { // window.history.next throws in the typeof check above. return { type: TYPES.OBJECT, display: m ? m[1] : "Object" }; } if (typeof aObject == "object" && typeof aObject.__iterator__ == "function") { return { type: TYPES.ITERATOR, display: "Iterator" }; } return { type: TYPES.OBJECT, display: m ? m[1] : "Object" }; } }, /** * Tells if the given function is native or not. * * @param function aFunction * The function you want to check if it is native or not. * @return boolean * True if the given function is native, false otherwise. */ isNativeFunction: function WCU_isNativeFunction(aFunction) { return typeof aFunction == "function" && !("prototype" in aFunction); }, /** * Tells if the given property of the provided object is a non-native getter or * not. * * @param object aObject * The object that contains the property. * @param string aProp * The property you want to check if it is a getter or not. * @return boolean * True if the given property is a getter, false otherwise. */ isNonNativeGetter: function WCU_isNonNativeGetter(aObject, aProp) { if (typeof aObject != "object") { return false; } let desc; while (aObject) { try { if (desc = Object.getOwnPropertyDescriptor(aObject, aProp)) { break; } } catch (ex) { // Native getters throw here. See bug 520882. if (ex.name == "NS_ERROR_XPC_BAD_CONVERT_JS" || ex.name == "NS_ERROR_XPC_BAD_OP_ON_WN_PROTO") { return false; } throw ex; } aObject = Object.getPrototypeOf(aObject); } if (desc && desc.get && !this.isNativeFunction(desc.get)) { return true; } return false; }, /** * Get an array that describes the properties of the given object. * * @param object aObject * The object to get the properties from. * @param object aObjectCache * Optional object cache where to store references to properties of * aObject that are inspectable. See this.isObjectInspectable(). * @return array * An array that describes each property from the given object. Each * array element is an object (a property descriptor). Each property * descriptor has the following properties: * - name - property name * - value - a presentable property value representation (see * this.presentableValueFor()) * - type - value type (see this.presentableValueFor()) * - inspectable - tells if the property value holds further * properties (see this.isObjectInspectable()). * - objectId - optional, available only if aObjectCache is given and * if |inspectable| is true. You can do * aObjectCache[propertyDescriptor.objectId] to get the actual object * referenced by the property of aObject. */ namesAndValuesOf: function WCU_namesAndValuesOf(aObject, aObjectCache) { let pairs = []; let value, presentable; let isDOMDocument = aObject instanceof Ci.nsIDOMDocument; let deprecated = ["width", "height", "inputEncoding"]; for (let propName in aObject) { // See bug 632275: skip deprecated properties. if (isDOMDocument && deprecated.indexOf(propName) > -1) { continue; } // Also skip non-native getters. if (this.isNonNativeGetter(aObject, propName)) { value = ""; presentable = {type: TYPES.GETTER, display: "Getter"}; } else { try { value = aObject[propName]; presentable = this.presentableValueFor(value); } catch (ex) { continue; } } let pair = {}; pair.name = propName; pair.value = presentable.display; pair.inspectable = false; pair.type = presentable.type; switch (presentable.type) { case TYPES.GETTER: case TYPES.ITERATOR: case TYPES.GENERATOR: case TYPES.STRING: break; default: try { for (let p in value) { pair.inspectable = true; break; } } catch (ex) { } break; } // Store the inspectable object. if (pair.inspectable && aObjectCache) { pair.objectId = ++gObjectId; aObjectCache[pair.objectId] = value; } pairs.push(pair); } pairs.sort(function(a, b) { // Convert the pair.name to a number for later sorting. let aNumber = parseFloat(a.name); let bNumber = parseFloat(b.name); // Sort numbers. if (!isNaN(aNumber) && isNaN(bNumber)) { return -1; } else if (isNaN(aNumber) && !isNaN(bNumber)) { return 1; } else if (!isNaN(aNumber) && !isNaN(bNumber)) { return aNumber - bNumber; } // Sort string. else if (a.name < b.name) { return -1; } else if (a.name > b.name) { return 1; } else { return 0; } }); return pairs; }, /** * Check if the given object is an iterator or a generator. * * @param object aObject * The object you want to check. * @return boolean * True if the given object is an iterator or a generator, otherwise * false is returned. */ isIteratorOrGenerator: function WCU_isIteratorOrGenerator(aObject) { if (aObject === null) { return false; } if (typeof aObject == "object") { if (typeof aObject.__iterator__ == "function" || aObject.constructor && aObject.constructor.name == "Iterator") { return true; } try { let str = aObject.toString(); if (typeof aObject.next == "function" && str.indexOf("[object Generator") == 0) { return true; } } catch (ex) { // window.history.next throws in the typeof check above. return false; } } return false; }, /** * Determine if the given request mixes HTTP with HTTPS content. * * @param string aRequest * Location of the requested content. * @param string aLocation * Location of the current page. * @return boolean * True if the content is mixed, false if not. */ isMixedHTTPSRequest: function WCU_isMixedHTTPSRequest(aRequest, aLocation) { try { let requestURI = Services.io.newURI(aRequest, null, null); let contentURI = Services.io.newURI(aLocation, null, null); return (contentURI.scheme == "https" && requestURI.scheme != "https"); } catch (ex) { return false; } }, }; ////////////////////////////////////////////////////////////////////////// // Localization ////////////////////////////////////////////////////////////////////////// WebConsoleUtils.l10n = function WCU_l10n(aBundleURI) { this._bundleUri = aBundleURI; }; WebConsoleUtils.l10n.prototype = { _stringBundle: null, get stringBundle() { if (!this._stringBundle) { this._stringBundle = Services.strings.createBundle(this._bundleUri); } return this._stringBundle; }, /** * Generates a formatted timestamp string for displaying in console messages. * * @param integer [aMilliseconds] * Optional, allows you to specify the timestamp in milliseconds since * the UNIX epoch. * @return string * The timestamp formatted for display. */ timestampString: function WCU_l10n_timestampString(aMilliseconds) { let d = new Date(aMilliseconds ? aMilliseconds : null); let hours = d.getHours(), minutes = d.getMinutes(); let seconds = d.getSeconds(), milliseconds = d.getMilliseconds(); let parameters = [hours, minutes, seconds, milliseconds]; return this.getFormatStr("timestampFormat", parameters); }, /** * Retrieve a localized string. * * @param string aName * The string name you want from the Web Console string bundle. * @return string * The localized string. */ getStr: function WCU_l10n_getStr(aName) { let result; try { result = this.stringBundle.GetStringFromName(aName); } catch (ex) { Cu.reportError("Failed to get string: " + aName); throw ex; } return result; }, /** * Retrieve a localized string formatted with values coming from the given * array. * * @param string aName * The string name you want from the Web Console string bundle. * @param array aArray * The array of values you want in the formatted string. * @return string * The formatted local string. */ getFormatStr: function WCU_l10n_getFormatStr(aName, aArray) { let result; try { result = this.stringBundle.formatStringFromName(aName, aArray, aArray.length); } catch (ex) { Cu.reportError("Failed to format string: " + aName); throw ex; } return result; }, }; ////////////////////////////////////////////////////////////////////////// // JS Completer ////////////////////////////////////////////////////////////////////////// var JSPropertyProvider = (function _JSPP(WCU) { const STATE_NORMAL = 0; const STATE_QUOTE = 2; const STATE_DQUOTE = 3; const OPEN_BODY = "{[(".split(""); const CLOSE_BODY = "}])".split(""); const OPEN_CLOSE_BODY = { "{": "}", "[": "]", "(": ")", }; const MAX_COMPLETIONS = 256; /** * Analyses a given string to find the last statement that is interesting for * later completion. * * @param string aStr * A string to analyse. * * @returns object * If there was an error in the string detected, then a object like * * { err: "ErrorMesssage" } * * is returned, otherwise a object like * * { * state: STATE_NORMAL|STATE_QUOTE|STATE_DQUOTE, * startPos: index of where the last statement begins * } */ function findCompletionBeginning(aStr) { let bodyStack = []; let state = STATE_NORMAL; let start = 0; let c; for (let i = 0; i < aStr.length; i++) { c = aStr[i]; switch (state) { // Normal JS state. case STATE_NORMAL: if (c == '"') { state = STATE_DQUOTE; } else if (c == "'") { state = STATE_QUOTE; } else if (c == ";") { start = i + 1; } else if (c == " ") { start = i + 1; } else if (OPEN_BODY.indexOf(c) != -1) { bodyStack.push({ token: c, start: start }); start = i + 1; } else if (CLOSE_BODY.indexOf(c) != -1) { var last = bodyStack.pop(); if (!last || OPEN_CLOSE_BODY[last.token] != c) { return { err: "syntax error" }; } if (c == "}") { start = i + 1; } else { start = last.start; } } break; // Double quote state > " < case STATE_DQUOTE: if (c == "\\") { i++; } else if (c == "\n") { return { err: "unterminated string literal" }; } else if (c == '"') { state = STATE_NORMAL; } break; // Single quote state > ' < case STATE_QUOTE: if (c == "\\") { i++; } else if (c == "\n") { return { err: "unterminated string literal" }; } else if (c == "'") { state = STATE_NORMAL; } break; } } return { state: state, startPos: start }; } /** * Provides a list of properties, that are possible matches based on the passed * scope and inputValue. * * @param object aScope * Scope to use for the completion. * * @param string aInputValue * Value that should be completed. * * @returns null or object * If no completion valued could be computed, null is returned, * otherwise a object with the following form is returned: * { * matches: [ string, string, string ], * matchProp: Last part of the inputValue that was used to find * the matches-strings. * } */ function JSPropertyProvider(aScope, aInputValue) { let obj = WCU.unwrap(aScope); // Analyse the aInputValue and find the beginning of the last part that // should be completed. let beginning = findCompletionBeginning(aInputValue); // There was an error analysing the string. if (beginning.err) { return null; } // If the current state is not STATE_NORMAL, then we are inside of an string // which means that no completion is possible. if (beginning.state != STATE_NORMAL) { return null; } let completionPart = aInputValue.substring(beginning.startPos); // Don't complete on just an empty string. if (completionPart.trim() == "") { return null; } let matches = null; let matchProp = ""; let lastDot = completionPart.lastIndexOf("."); if (lastDot > 0 && (completionPart[0] == "'" || completionPart[0] == '"') && completionPart[lastDot - 1] == completionPart[0]) { // We are completing a string literal. obj = obj.String.prototype; matchProp = completionPart.slice(lastDot + 1); } else { // We are completing a variable / a property lookup. let properties = completionPart.split("."); if (properties.length > 1) { matchProp = properties.pop().trimLeft(); for (let i = 0; i < properties.length; i++) { let prop = properties[i].trim(); if (!prop) { return null; } // If obj is undefined or null (which is what "== null" does), // then there is no chance to run completion on it. Exit here. if (obj == null) { return null; } // Check if prop is a getter function on obj. Functions can change other // stuff so we can't execute them to get the next object. Stop here. if (WCU.isNonNativeGetter(obj, prop)) { return null; } try { obj = obj[prop]; } catch (ex) { return null; } } } else { matchProp = properties[0].trimLeft(); } // If obj is undefined or null (which is what "== null" does), // then there is no chance to run completion on it. Exit here. if (obj == null) { return null; } // Skip Iterators and Generators. if (WCU.isIteratorOrGenerator(obj)) { return null; } } let matches = Object.keys(getMatchedProps(obj, {matchProp:matchProp})); return { matchProp: matchProp, matches: matches.sort(), }; } /** * Get all accessible properties on this JS value. * Filter those properties by name. * Take only a certain number of those. * * @param mixed aObj * JS value whose properties we want to collect. * * @param object aOptions * Options that the algorithm takes. * - matchProp (string): Filter for properties that match this one. * Defaults to the empty string (which always matches). * * @return object * Object whose keys are all accessible properties on the object. */ function getMatchedProps(aObj, aOptions = {matchProp: ""}) { // Argument defaults. aOptions.matchProp = aOptions.matchProp || ""; if (aObj == null) { return {}; } try { Object.getPrototypeOf(aObj); } catch(e) { aObj = aObj.constructor.prototype; } let c = MAX_COMPLETIONS; let names = {}; // Using an Object to avoid duplicates. // We need to go up the prototype chain. let ownNames = null; while (aObj !== null) { ownNames = Object.getOwnPropertyNames(aObj); for (let i = 0; i < ownNames.length; i++) { // Filtering happens here. // If we already have it in, no need to append it. if (ownNames[i].indexOf(aOptions.matchProp) != 0 || names[ownNames[i]] == true) { continue; } c--; if (c < 0) { return names; } // If it is an array index, we can't take it. // This uses a trick: converting a string to a number yields NaN if // the operation failed, and NaN is not equal to itself. if (+ownNames[i] != +ownNames[i]) { names[ownNames[i]] = true; } } aObj = Object.getPrototypeOf(aObj); } return names; } return JSPropertyProvider; })(WebConsoleUtils); /////////////////////////////////////////////////////////////////////////////// // The page errors listener /////////////////////////////////////////////////////////////////////////////// /** * The nsIConsoleService listener. This is used to send all the page errors * (JavaScript, CSS and more) to the remote Web Console instance. * * @constructor * @param nsIDOMWindow aWindow * The window object for which we are created. * @param object aListener * The listener object must have a method: onPageError. This method is * invoked with one argument, the nsIScriptError, whenever a relevant * page error is received. */ function PageErrorListener(aWindow, aListener) { this.window = aWindow; this.listener = aListener; } PageErrorListener.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleListener]), /** * The content window for which we listen to page errors. * @type nsIDOMWindow */ window: null, /** * The listener object which is notified of page errors. It must have * a onPageError method which is invoked with one argument: the nsIScriptError. * @type object */ listener: null, /** * Initialize the nsIConsoleService listener. */ init: function PEL_init() { Services.console.registerListener(this); }, /** * The nsIConsoleService observer. This method takes all the script error * messages belonging to the current window and sends them to the remote Web * Console instance. * * @param nsIScriptError aScriptError * The script error object coming from the nsIConsoleService. */ observe: function PEL_observe(aScriptError) { if (!this.window || !this.listener || !(aScriptError instanceof Ci.nsIScriptError) || !aScriptError.outerWindowID) { return; } if (!this.isCategoryAllowed(aScriptError.category)) { return; } let errorWindow = WebConsoleUtils.getWindowByOuterId(aScriptError.outerWindowID, this.window); if (!errorWindow || errorWindow.top != this.window) { return; } this.listener.onPageError(aScriptError); }, /** * Check if the given script error category is allowed to be tracked or not. * We ignore chrome-originating errors as we only care about content. * * @param string aCategory * The nsIScriptError category you want to check. * @return boolean * True if the category is allowed to be logged, false otherwise. */ isCategoryAllowed: function PEL_isCategoryAllowed(aCategory) { switch (aCategory) { case "XPConnect JavaScript": case "component javascript": case "chrome javascript": case "chrome registration": case "XBL": case "XBL Prototype Handler": case "XBL Content Sink": case "xbl javascript": return false; } return true; }, /** * Get the cached page errors for the current inner window. * * @return array * The array of cached messages. Each element is an nsIScriptError * with an added _type property so the remote Web Console instance can * tell the difference between various types of cached messages. */ getCachedMessages: function PEL_getCachedMessages() { let innerWindowId = WebConsoleUtils.getInnerWindowId(this.window); let result = []; let errors = {}; Services.console.getMessageArray(errors, {}); (errors.value || []).forEach(function(aError) { if (!(aError instanceof Ci.nsIScriptError) || aError.innerWindowID != innerWindowId || !this.isCategoryAllowed(aError.category)) { return; } let remoteMessage = WebConsoleUtils.cloneObject(aError); result.push(remoteMessage); }, this); return result; }, /** * Remove the nsIConsoleService listener. */ destroy: function PEL_destroy() { Services.console.unregisterListener(this); this.listener = this.window = null; }, };