/* -*- 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"); XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPIStorage", "resource://gre/modules/ConsoleAPIStorage.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetworkHelper", "resource://gre/modules/devtools/NetworkHelper.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "gActivityDistributor", "@mozilla.org/network/http-activity-distributor;1", "nsIHttpActivityDistributor"); XPCOMUtils.defineLazyModuleGetter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "TargetFactory", "resource:///modules/devtools/Target.jsm"); this.EXPORTED_SYMBOLS = ["WebConsoleUtils", "JSPropertyProvider", "JSTermHelpers", "PageErrorListener", "ConsoleAPIListener", "NetworkResponseListener", "NetworkMonitor", "ConsoleProgressListener"]; // Match the function name from the result of toString() or toSource(). // // Examples: // (function foobar(a, b) { ... // function foobar2(a) { ... // function() { ... const REGEX_MATCH_FUNCTION_NAME = /^\(?function\s+([^(\s]+)\s*\(/; // Match the function arguments from the result of toString() or toSource(). const REGEX_MATCH_FUNCTION_ARGS = /^\(?function\s*[^\s(]*\s*\((.+?)\)/; this.WebConsoleUtils = { /** * 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": try { output = aResult + ""; } catch (ex) { output = ex; } 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; try { if (type == "object" && aResult.constructor && aResult.constructor.name) { type = aResult.constructor.name + ""; } } catch (ex) { // Prevent potential exceptions in page-provided objects from taking down // the Web Console. If the constructor.name is a getter that throws, or // something else bad happens. } return type.toLowerCase(); }, /** * 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 = this.getPropertyDescriptor(aObject, aProp); return desc && desc.get && !this.isNativeFunction(desc.get); }, /** * Get the property descriptor for the given object. * * @param object aObject * The object that contains the property. * @param string aProp * The property you want to get the descriptor for. * @return object * Property descriptor. */ getPropertyDescriptor: function WCU_getPropertyDescriptor(aObject, aProp) { let desc = null; while (aObject) { try { if (desc = Object.getOwnPropertyDescriptor(aObject, aProp)) { break; } } catch (ex if (ex.name == "NS_ERROR_XPC_BAD_CONVERT_JS" || ex.name == "NS_ERROR_XPC_BAD_OP_ON_WN_PROTO" || ex.name == "TypeError")) { // Native getters throw here. See bug 520882. // null throws TypeError. } try { aObject = Object.getPrototypeOf(aObject); } catch (ex if (ex.name == "TypeError")) { return desc; } } return desc; }, /** * Sort function for object properties. * * @param object a * Property descriptor. * @param object b * Property descriptor. * @return integer * -1 if a.name < b.name, * 1 if a.name > b.name, * 0 otherwise. */ propertiesSort: function WCU_propertiesSort(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; } }, /** * Inspect the properties of the given object. For each property a descriptor * object is created. The descriptor gives you information about the property * name, value, type, getter and setter. When the property value references * another object you get a wrapper that holds information about that object. * * @see this.inspectObjectProperty * @param object aObject * The object you want to inspect. * @param function aObjectWrapper * The function that creates wrappers for property values which * reference other objects. This function must take one argument, the * object to wrap, and it must return an object grip that gives * information about the referenced object. * @return array * An array of property descriptors. */ inspectObject: function WCU_inspectObject(aObject, aObjectWrapper) { let properties = []; let isDOMDocument = aObject instanceof Ci.nsIDOMDocument; let deprecated = ["width", "height", "inputEncoding"]; for (let name in aObject) { // See bug 632275: skip deprecated properties. if (isDOMDocument && deprecated.indexOf(name) > -1) { continue; } properties.push(this.inspectObjectProperty(aObject, name, aObjectWrapper)); } return properties.sort(this.propertiesSort); }, /** * A helper method that creates a property descriptor for the provided object, * properly formatted for sending in a protocol response. * * The property value can reference other objects. Since actual objects cannot * be sent to the client, we need to send simple object grips - descriptors * for those objects. This is why you need to give an object wrapper function * that creates object grips. * * @param string aProperty * Property name for which we have the descriptor. * @param object aObject * The object that the descriptor is generated for. * @param function aObjectWrapper * This function is given the property value. Whatever the function * returns is used as the representation of the property value. * @return object * The property descriptor formatted for sending to the client. */ inspectObjectProperty: function WCU_inspectObjectProperty(aObject, aProperty, aObjectWrapper) { let descriptor = this.getPropertyDescriptor(aObject, aProperty) || {}; let result = { name: aProperty }; result.configurable = descriptor.configurable; result.enumerable = descriptor.enumerable; result.writable = descriptor.writable; if (descriptor.value !== undefined) { result.value = this.createValueGrip(descriptor.value, aObjectWrapper); } else if (descriptor.get) { let gotValue = false; if (this.isNativeFunction(descriptor.get)) { try { result.value = this.createValueGrip(aObject[aProperty], aObjectWrapper); gotValue = true; } catch (e) {} } if (!gotValue) { result.get = this.createValueGrip(descriptor.get, aObjectWrapper); result.set = this.createValueGrip(descriptor.set, aObjectWrapper); } } // There are cases with properties that have no value and no getter. For // example window.screen.width. if (result.value === undefined && result.get === undefined) { try { result.value = this.createValueGrip(aObject[aProperty], aObjectWrapper); } catch (ex) { // This can throw when security restrictions prevent us from reading // the value. } } return result; }, /** * Make an object grip for the given object. An object grip of the simplest * form with minimal information about the given object is returned. This * method is usually combined with other functions that add further state * information and object ID such that, later, the client is able to retrieve * more information about the object being represented by this grip. * * @param object aObject * The object you want to create a grip for. * @return object * The object grip. */ getObjectGrip: function WCU_getObjectGrip(aObject) { let className = null; let type = typeof aObject; let result = { "type": type, "className": this.getObjectClassName(aObject), "displayString": this.formatResult(aObject), "inspectable": this.isObjectInspectable(aObject), }; if (type == "function") { result.functionName = this.getFunctionName(aObject); result.functionArguments = this.getFunctionArguments(aObject); } return result; }, /** * Create a grip for the given value. If the value is an object, * an object wrapper will be created. * * @param mixed aValue * The value you want to create a grip for, before sending it to the * client. * @param function aObjectWrapper * If the value is an object then the aObjectWrapper function is * invoked to give us an object grip. See this.getObjectGrip(). * @return mixed * The value grip. */ createValueGrip: function WCU_createValueGrip(aValue, aObjectWrapper) { let type = typeof(aValue); switch (type) { case "boolean": case "number": return aValue; case "string": return aObjectWrapper(aValue); case "object": case "function": if (aValue) { return aObjectWrapper(aValue); } default: if (aValue === null) { return { type: "null" }; } if (aValue === undefined) { return { type: "undefined" }; } Cu.reportError("Failed to provide a grip for value of " + type + ": " + aValue); return null; } }, /** * 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; } }, /** * Make a string representation for an object actor grip. * * @param object aGrip * The object grip received from the server. * @param boolean [aFormatString=false] * Optional boolean that tells if you want strings to be unevaled or * not. * @return string * The object grip converted to a string. */ objectActorGripToString: function WCU_objectActorGripToString(aGrip, aFormatString) { // Primitives like strings and numbers are not sent as objects. // But null and undefined are sent as objects with the type property // telling which type of value we have. We also have long strings which are // sent using the LongStringActor. let type = typeof(aGrip); if (type == "string" || (aGrip && type == "object" && aGrip.type == "longString")) { let str = type == "string" ? aGrip : aGrip.initial; if (aFormatString) { return this.formatResultString(str); } return str; } if (aGrip && type == "object") { if (aGrip.displayString && typeof aGrip.displayString == "object" && aGrip.displayString.type == "longString") { return aGrip.displayString.initial; } return aGrip.displayString || aGrip.className || aGrip.type || type; } return aGrip + ""; }, /** * Helper function to deduce the name of the provided function. * * @param funtion aFunction * The function whose name will be returned. * @return string * Function name. */ getFunctionName: function WCF_getFunctionName(aFunction) { let name = null; if (aFunction.name) { name = aFunction.name; } else { let desc; try { desc = aFunction.getOwnPropertyDescriptor("displayName"); } catch (ex) { } if (desc && typeof desc.value == "string") { name = desc.value; } } if (!name) { try { let str = (aFunction.toString() || aFunction.toSource()) + ""; name = (str.match(REGEX_MATCH_FUNCTION_NAME) || [])[1]; } catch (ex) { } } return name; }, /** * Helper function to deduce the arguments of the provided function. * * @param funtion aFunction * The function whose name will be returned. * @return array * Function arguments. */ getFunctionArguments: function WCF_getFunctionArguments(aFunction) { let args = []; try { let str = (aFunction.toString() || aFunction.toSource()) + ""; let argsString = (str.match(REGEX_MATCH_FUNCTION_ARGS) || [])[1]; if (argsString) { args = argsString.split(/\s*,\s*/); } } catch (ex) { } return args; }, /** * Get the object class name. For example, the |window| object has the Window * class name (based on [object Window]). * * @param object aObject * The object you want to get the class name for. * @return string * The object class name. */ getObjectClassName: function WCF_getObjectClassName(aObject) { if (aObject === null) { return "null"; } if (aObject === undefined) { return "undefined"; } let type = typeof aObject; if (type != "object") { // Grip class names should start with an uppercase letter. return type.charAt(0).toUpperCase() + type.substr(1); } let className; try { className = ((aObject + "").match(/^\[object (\S+)\]$/) || [])[1]; if (!className) { className = ((aObject.constructor + "").match(/^\[object (\S+)\]$/) || [])[1]; } if (!className && typeof aObject.constructor == "function") { className = this.getFunctionName(aObject.constructor); } } catch (ex) { } return className; }, /** * Determine the string to display as a property value in the property panel. * * @param object aActor * Object actor grip. * @return string * Property value as suited for the property panel. */ getPropertyPanelValue: function WCU_getPropertyPanelValue(aActor) { if (aActor.get) { return "Getter"; } let val = aActor.value; if (typeof val == "string") { return this.formatResultString(val); } if (typeof val != "object" || !val) { return val; } if (val.type == "longString") { return this.formatResultString(val.initial) + "\u2026"; } if (val.type == "function" && val.functionName) { return "function " + val.functionName + "(" + val.functionArguments.join(", ") + ")"; } if (val.type == "object" && val.className) { return val.className; } if (val.displayString && typeof val.displayString == "object" && val.displayString.type == "longString") { return val.displayString.initial; } return val.displayString || val.type; }, }; ////////////////////////////////////////////////////////////////////////// // 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 ////////////////////////////////////////////////////////////////////////// this.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 = Object.create(null); // 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 || ownNames[i] in names) { 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] * Optional - the window object for which we are created. This is used * for filtering out messages that belong to other windows. * @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. */ this.PageErrorListener = 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.listener || !(aScriptError instanceof Ci.nsIScriptError)) { return; } if (this.window) { if (!aScriptError.outerWindowID || !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 = this.window ? WebConsoleUtils.getInnerWindowId(this.window) : null; let errors = Services.console.getMessageArray() || []; return errors.filter(function(aError) { return aError instanceof Ci.nsIScriptError && (!innerWindowId || (aError.innerWindowID == innerWindowId && this.isCategoryAllowed(aError.category))); }, this); }, /** * Remove the nsIConsoleService listener. */ destroy: function PEL_destroy() { Services.console.unregisterListener(this); this.listener = this.window = null; }, }; /////////////////////////////////////////////////////////////////////////////// // The window.console API observer /////////////////////////////////////////////////////////////////////////////// /** * The window.console API observer. This allows the window.console API messages * to be sent to the remote Web Console instance. * * @constructor * @param nsIDOMWindow aWindow * Optional - the window object for which we are created. This is used * for filtering out messages that belong to other windows. * @param object aOwner * The owner object must have the following methods: * - onConsoleAPICall(). This method is invoked with one argument, the * Console API message that comes from the observer service, whenever * a relevant console API call is received. */ this.ConsoleAPIListener = function ConsoleAPIListener(aWindow, aOwner) { this.window = aWindow; this.owner = aOwner; } ConsoleAPIListener.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), /** * The content window for which we listen to window.console API calls. * @type nsIDOMWindow */ window: null, /** * The owner object which is notified of window.console API calls. It must * have a onConsoleAPICall method which is invoked with one argument: the * console API call object that comes from the observer service. * * @type object * @see WebConsoleActor */ owner: null, /** * Initialize the window.console API observer. */ init: function CAL_init() { // Note that the observer is process-wide. We will filter the messages as // needed, see CAL_observe(). Services.obs.addObserver(this, "console-api-log-event", false); }, /** * The console API message observer. When messages are received from the * observer service we forward them to the remote Web Console instance. * * @param object aMessage * The message object receives from the observer service. * @param string aTopic * The message topic received from the observer service. */ observe: function CAL_observe(aMessage, aTopic) { if (!this.owner) { return; } let apiMessage = aMessage.wrappedJSObject; if (this.window) { let msgWindow = WebConsoleUtils.getWindowByOuterId(apiMessage.ID, this.window); if (!msgWindow || msgWindow.top != this.window) { // Not the same window! return; } } this.owner.onConsoleAPICall(apiMessage); }, /** * Get the cached messages for the current inner window. * * @return array * The array of cached messages. Each element is a Console API * prepared to be sent to the remote Web Console instance. */ getCachedMessages: function CAL_getCachedMessages() { let innerWindowId = this.window ? WebConsoleUtils.getInnerWindowId(this.window) : null; return ConsoleAPIStorage.getEvents(innerWindowId); }, /** * Destroy the console API listener. */ destroy: function CAL_destroy() { Services.obs.removeObserver(this, "console-api-log-event"); this.window = this.owner = null; }, }; /** * JSTerm helper functions. * * Defines a set of functions ("helper functions") that are available from the * Web Console but not from the web page. * * A list of helper functions used by Firebug can be found here: * http://getfirebug.com/wiki/index.php/Command_Line_API * * @param object aOwner * The owning object. */ this.JSTermHelpers = function JSTermHelpers(aOwner) { /** * Find a node by ID. * * @param string aId * The ID of the element you want. * @return nsIDOMNode or null * The result of calling document.querySelector(aSelector). */ aOwner.sandbox.$ = function JSTH_$(aSelector) { return aOwner.window.document.querySelector(aSelector); }; /** * Find the nodes matching a CSS selector. * * @param string aSelector * A string that is passed to window.document.querySelectorAll. * @return nsIDOMNodeList * Returns the result of document.querySelectorAll(aSelector). */ aOwner.sandbox.$$ = function JSTH_$$(aSelector) { return aOwner.window.document.querySelectorAll(aSelector); }; /** * Runs an xPath query and returns all matched nodes. * * @param string aXPath * xPath search query to execute. * @param [optional] nsIDOMNode aContext * Context to run the xPath query on. Uses window.document if not set. * @return array of nsIDOMNode */ aOwner.sandbox.$x = function JSTH_$x(aXPath, aContext) { let nodes = []; let doc = aOwner.window.document; let aContext = aContext || doc; try { let results = doc.evaluate(aXPath, aContext, null, Ci.nsIDOMXPathResult.ANY_TYPE, null); let node; while (node = results.iterateNext()) { nodes.push(node); } } catch (ex) { aOwner.window.console.error(ex.message); } return nodes; }; /** * Returns the currently selected object in the highlighter. * * TODO: this implementation crosses the client/server boundaries! This is not * usable within a remote browser. To implement this feature correctly we need * support for remote inspection capabilities within the Inspector as well. * See bug 787975. * * @return nsIDOMElement|null * The DOM element currently selected in the highlighter. */ Object.defineProperty(aOwner.sandbox, "$0", { get: function() { try { let window = aOwner.chromeWindow(); let target = TargetFactory.forTab(window.gBrowser.selectedTab); let toolbox = gDevTools.getToolbox(target); return toolbox == null ? undefined : toolbox.getPanel("inspector").selection.node; } catch (ex) { aOwner.window.console.error(ex.message); } }, enumerable: true, configurable: false }); /** * Clears the output of the JSTerm. */ aOwner.sandbox.clear = function JSTH_clear() { aOwner.helperResult = { type: "clearOutput", }; }; /** * Returns the result of Object.keys(aObject). * * @param object aObject * Object to return the property names from. * @return array of strings */ aOwner.sandbox.keys = function JSTH_keys(aObject) { return Object.keys(WebConsoleUtils.unwrap(aObject)); }; /** * Returns the values of all properties on aObject. * * @param object aObject * Object to display the values from. * @return array of string */ aOwner.sandbox.values = function JSTH_values(aObject) { let arrValues = []; let obj = WebConsoleUtils.unwrap(aObject); try { for (let prop in obj) { arrValues.push(obj[prop]); } } catch (ex) { aOwner.window.console.error(ex.message); } return arrValues; }; /** * Opens a help window in MDN. */ aOwner.sandbox.help = function JSTH_help() { aOwner.helperResult = { type: "help" }; }; /** * Inspects the passed aObject. This is done by opening the PropertyPanel. * * @param object aObject * Object to inspect. */ aOwner.sandbox.inspect = function JSTH_inspect(aObject) { let obj = WebConsoleUtils.unwrap(aObject); if (!WebConsoleUtils.isObjectInspectable(obj)) { return aObject; } aOwner.helperResult = { type: "inspectObject", input: aOwner.evalInput, object: aOwner.createValueGrip(obj), }; }; /** * Prints aObject to the output. * * @param object aObject * Object to print to the output. * @return string */ aOwner.sandbox.pprint = function JSTH_pprint(aObject) { if (aObject === null || aObject === undefined || aObject === true || aObject === false) { aOwner.helperResult = { type: "error", message: "helperFuncUnsupportedTypeError", }; return; } aOwner.helperResult = { rawOutput: true }; if (typeof aObject == "function") { return aObject + "\n"; } let output = []; let getObjectGrip = WebConsoleUtils.getObjectGrip.bind(WebConsoleUtils); let obj = WebConsoleUtils.unwrap(aObject); let props = WebConsoleUtils.inspectObject(obj, getObjectGrip); props.forEach(function(aProp) { output.push(aProp.name + ": " + WebConsoleUtils.getPropertyPanelValue(aProp)); }); return " " + output.join("\n "); }; /** * Print a string to the output, as-is. * * @param string aString * A string you want to output. * @return void */ aOwner.sandbox.print = function JSTH_print(aString) { aOwner.helperResult = { rawOutput: true }; return String(aString); }; }; (function(_global, WCU) { /////////////////////////////////////////////////////////////////////////////// // Network logging /////////////////////////////////////////////////////////////////////////////// // The maximum uint32 value. const PR_UINT32_MAX = 4294967295; // HTTP status codes. const HTTP_MOVED_PERMANENTLY = 301; const HTTP_FOUND = 302; const HTTP_SEE_OTHER = 303; const HTTP_TEMPORARY_REDIRECT = 307; // The maximum number of bytes a NetworkResponseListener can hold. const RESPONSE_BODY_LIMIT = 1048576; // 1 MB /** * The network response listener implements the nsIStreamListener and * nsIRequestObserver interfaces. This is used within the NetworkMonitor feature * to get the response body of the request. * * The code is mostly based on code listings from: * * http://www.softwareishard.com/blog/firebug/ * nsitraceablechannel-intercept-http-traffic/ * * @constructor * @param object aOwner * The response listener owner. This object needs to hold the * |openResponses| object. * @param object aHttpActivity * HttpActivity object associated with this request. See NetworkMonitor * for more information. */ function NetworkResponseListener(aOwner, aHttpActivity) { this.owner = aOwner; this.receivedData = ""; this.httpActivity = aHttpActivity; this.bodySize = 0; } NetworkResponseListener.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInputStreamCallback, Ci.nsIRequestObserver, Ci.nsISupports]), /** * This NetworkResponseListener tracks the NetworkMonitor.openResponses object * to find the associated uncached headers. * @private */ _foundOpenResponse: false, /** * The response listener owner. */ owner: null, /** * The response will be written into the outputStream of this nsIPipe. * Both ends of the pipe must be blocking. */ sink: null, /** * The HttpActivity object associated with this response. */ httpActivity: null, /** * Stores the received data as a string. */ receivedData: null, /** * The network response body size. */ bodySize: null, /** * The nsIRequest we are started for. */ request: null, /** * Set the async listener for the given nsIAsyncInputStream. This allows us to * wait asynchronously for any data coming from the stream. * * @param nsIAsyncInputStream aStream * The input stream from where we are waiting for data to come in. * @param nsIInputStreamCallback aListener * The input stream callback you want. This is an object that must have * the onInputStreamReady() method. If the argument is null, then the * current callback is removed. * @return void */ setAsyncListener: function NRL_setAsyncListener(aStream, aListener) { // Asynchronously wait for the stream to be readable or closed. aStream.asyncWait(aListener, 0, 0, Services.tm.mainThread); }, /** * Stores the received data, if request/response body logging is enabled. It * also does limit the number of stored bytes, based on the * RESPONSE_BODY_LIMIT constant. * * Learn more about nsIStreamListener at: * https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIStreamListener * * @param nsIRequest aRequest * @param nsISupports aContext * @param nsIInputStream aInputStream * @param unsigned long aOffset * @param unsigned long aCount */ onDataAvailable: function NRL_onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount) { this._findOpenResponse(); let data = NetUtil.readInputStreamToString(aInputStream, aCount); this.bodySize += aCount; if (!this.httpActivity.discardResponseBody && this.receivedData.length < RESPONSE_BODY_LIMIT) { this.receivedData += NetworkHelper. convertToUnicode(data, aRequest.contentCharset); } }, /** * See documentation at * https://developer.mozilla.org/En/NsIRequestObserver * * @param nsIRequest aRequest * @param nsISupports aContext */ onStartRequest: function NRL_onStartRequest(aRequest) { this.request = aRequest; this._findOpenResponse(); // Asynchronously wait for the data coming from the request. this.setAsyncListener(this.sink.inputStream, this); }, /** * Handle the onStopRequest by closing the sink output stream. * * For more documentation about nsIRequestObserver go to: * https://developer.mozilla.org/En/NsIRequestObserver */ onStopRequest: function NRL_onStopRequest() { this._findOpenResponse(); this.sink.outputStream.close(); }, /** * Find the open response object associated to the current request. The * NetworkMonitor._httpResponseExaminer() method saves the response headers in * NetworkMonitor.openResponses. This method takes the data from the open * response object and puts it into the HTTP activity object, then sends it to * the remote Web Console instance. * * @private */ _findOpenResponse: function NRL__findOpenResponse() { if (!this.owner || this._foundOpenResponse) { return; } let openResponse = null; for each (let item in this.owner.openResponses) { if (item.channel === this.httpActivity.channel) { openResponse = item; break; } } if (!openResponse) { return; } this._foundOpenResponse = true; delete this.owner.openResponses[openResponse.id]; this.httpActivity.owner.addResponseHeaders(openResponse.headers); this.httpActivity.owner.addResponseCookies(openResponse.cookies); }, /** * Clean up the response listener once the response input stream is closed. * This is called from onStopRequest() or from onInputStreamReady() when the * stream is closed. * @return void */ onStreamClose: function NRL_onStreamClose() { if (!this.httpActivity) { return; } // Remove our listener from the request input stream. this.setAsyncListener(this.sink.inputStream, null); this._findOpenResponse(); if (!this.httpActivity.discardResponseBody && this.receivedData.length) { this._onComplete(this.receivedData); } else if (!this.httpActivity.discardResponseBody && this.httpActivity.responseStatus == 304) { // Response is cached, so we load it from cache. let charset = this.request.contentCharset || this.httpActivity.charset; NetworkHelper.loadFromCache(this.httpActivity.url, charset, this._onComplete.bind(this)); } else { this._onComplete(); } }, /** * Handler for when the response completes. This function cleans up the * response listener. * * @param string [aData] * Optional, the received data coming from the response listener or * from the cache. */ _onComplete: function NRL__onComplete(aData) { let response = { mimeType: "", text: aData || "", }; response.size = response.text.length; try { response.mimeType = this.request.contentType; } catch (ex) { } if (!response.mimeType || !NetworkHelper.isTextMimeType(response.mimeType)) { response.encoding = "base64"; response.text = btoa(response.text); } if (response.mimeType && this.request.contentCharset) { response.mimeType += "; charset=" + this.request.contentCharset; } this.receivedData = ""; this.httpActivity.owner. addResponseContent(response, this.httpActivity.discardResponseBody); this.httpActivity.channel = null; this.httpActivity.owner = null; this.httpActivity = null; this.sink = null; this.inputStream = null; this.request = null; this.owner = null; }, /** * The nsIInputStreamCallback for when the request input stream is ready - * either it has more data or it is closed. * * @param nsIAsyncInputStream aStream * The sink input stream from which data is coming. * @returns void */ onInputStreamReady: function NRL_onInputStreamReady(aStream) { if (!(aStream instanceof Ci.nsIAsyncInputStream) || !this.httpActivity) { return; } let available = -1; try { // This may throw if the stream is closed normally or due to an error. available = aStream.available(); } catch (ex) { } if (available != -1) { if (available != 0) { // Note that passing 0 as the offset here is wrong, but the // onDataAvailable() method does not use the offset, so it does not // matter. this.onDataAvailable(this.request, null, aStream, 0, available); } this.setAsyncListener(aStream, this); } else { this.onStreamClose(); } }, }; /** * The network monitor uses the nsIHttpActivityDistributor to monitor network * requests. The nsIObserverService is also used for monitoring * http-on-examine-response notifications. All network request information is * routed to the remote Web Console. * * @constructor * @param nsIDOMWindow aWindow * Optional, the window that we monitor network requests for. If no * window is given, all browser network requests are logged. * @param object aOwner * The network monitor owner. This object needs to hold: * - onNetworkEvent(aRequestInfo). This method is invoked once for every * new network request and it is given one arguments: the initial network * request information. onNetworkEvent() must return an object which * holds several add*() methods which are used to add further network * request/response information. * - saveRequestAndResponseBodies property which tells if you want to log * request and response bodies. */ function NetworkMonitor(aWindow, aOwner) { this.window = aWindow; this.owner = aOwner; this.openRequests = {}; this.openResponses = {}; this._httpResponseExaminer = this._httpResponseExaminer.bind(this); } NetworkMonitor.prototype = { httpTransactionCodes: { 0x5001: "REQUEST_HEADER", 0x5002: "REQUEST_BODY_SENT", 0x5003: "RESPONSE_START", 0x5004: "RESPONSE_HEADER", 0x5005: "RESPONSE_COMPLETE", 0x5006: "TRANSACTION_CLOSE", 0x804b0003: "STATUS_RESOLVING", 0x804b000b: "STATUS_RESOLVED", 0x804b0007: "STATUS_CONNECTING_TO", 0x804b0004: "STATUS_CONNECTED_TO", 0x804b0005: "STATUS_SENDING_TO", 0x804b000a: "STATUS_WAITING_FOR", 0x804b0006: "STATUS_RECEIVING_FROM" }, // Network response bodies are piped through a buffer of the given size (in // bytes). responsePipeSegmentSize: null, owner: null, /** * Whether to save the bodies of network requests and responses. Disabled by * default to save memory. */ get saveRequestAndResponseBodies() this.owner && this.owner.saveRequestAndResponseBodies, /** * Object that holds the HTTP activity objects for ongoing requests. */ openRequests: null, /** * Object that holds response headers coming from this._httpResponseExaminer. */ openResponses: null, /** * The network monitor initializer. */ init: function NM_init() { this.responsePipeSegmentSize = Services.prefs .getIntPref("network.buffer.cache.size"); gActivityDistributor.addObserver(this); Services.obs.addObserver(this._httpResponseExaminer, "http-on-examine-response", false); }, /** * Observe notifications for the http-on-examine-response topic, coming from * the nsIObserverService. * * @private * @param nsIHttpChannel aSubject * @param string aTopic * @returns void */ _httpResponseExaminer: function NM__httpResponseExaminer(aSubject, aTopic) { // The httpResponseExaminer is used to retrieve the uncached response // headers. The data retrieved is stored in openResponses. The // NetworkResponseListener is responsible with updating the httpActivity // object with the data from the new object in openResponses. if (!this.owner || aTopic != "http-on-examine-response" || !(aSubject instanceof Ci.nsIHttpChannel)) { return; } let channel = aSubject.QueryInterface(Ci.nsIHttpChannel); if (this.window) { // Try to get the source window of the request. let win = NetworkHelper.getWindowForRequest(channel); if (!win || win.top !== this.window) { return; } } let response = { id: gSequenceId(), channel: channel, headers: [], cookies: [], }; let setCookieHeader = null; channel.visitResponseHeaders({ visitHeader: function NM__visitHeader(aName, aValue) { let lowerName = aName.toLowerCase(); if (lowerName == "set-cookie") { setCookieHeader = aValue; } response.headers.push({ name: aName, value: aValue }); } }); if (!response.headers.length) { return; // No need to continue. } if (setCookieHeader) { response.cookies = NetworkHelper.parseSetCookieHeader(setCookieHeader); } // Determine the HTTP version. let httpVersionMaj = {}; let httpVersionMin = {}; channel.QueryInterface(Ci.nsIHttpChannelInternal); channel.getResponseVersion(httpVersionMaj, httpVersionMin); response.status = channel.responseStatus; response.statusText = channel.responseStatusText; response.httpVersion = "HTTP/" + httpVersionMaj.value + "." + httpVersionMin.value; this.openResponses[response.id] = response; }, /** * Begin observing HTTP traffic that originates inside the current tab. * * @see https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIHttpActivityObserver * * @param nsIHttpChannel aChannel * @param number aActivityType * @param number aActivitySubtype * @param number aTimestamp * @param number aExtraSizeData * @param string aExtraStringData */ observeActivity: function NM_observeActivity(aChannel, aActivityType, aActivitySubtype, aTimestamp, aExtraSizeData, aExtraStringData) { if (!this.owner || aActivityType != gActivityDistributor.ACTIVITY_TYPE_HTTP_TRANSACTION && aActivityType != gActivityDistributor.ACTIVITY_TYPE_SOCKET_TRANSPORT) { return; } if (!(aChannel instanceof Ci.nsIHttpChannel)) { return; } aChannel = aChannel.QueryInterface(Ci.nsIHttpChannel); if (aActivitySubtype == gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_HEADER) { this._onRequestHeader(aChannel, aTimestamp, aExtraStringData); return; } // Iterate over all currently ongoing requests. If aChannel can't // be found within them, then exit this function. let httpActivity = null; for each (let item in this.openRequests) { if (item.channel === aChannel) { httpActivity = item; break; } } if (!httpActivity) { return; } let transCodes = this.httpTransactionCodes; // Store the time information for this activity subtype. if (aActivitySubtype in transCodes) { let stage = transCodes[aActivitySubtype]; if (stage in httpActivity.timings) { httpActivity.timings[stage].last = aTimestamp; } else { httpActivity.timings[stage] = { first: aTimestamp, last: aTimestamp, }; } } switch (aActivitySubtype) { case gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT: this._onRequestBodySent(httpActivity); break; case gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER: this._onResponseHeader(httpActivity, aExtraStringData); break; case gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE: this._onTransactionClose(httpActivity); break; default: break; } }, /** * Handler for ACTIVITY_SUBTYPE_REQUEST_HEADER. When a request starts the * headers are sent to the server. This method creates the |httpActivity| * object where we store the request and response information that is * collected through its lifetime. * * @private * @param nsIHttpChannel aChannel * @param number aTimestamp * @param string aExtraStringData * @return void */ _onRequestHeader: function NM__onRequestHeader(aChannel, aTimestamp, aExtraStringData) { let win = null; try { win = NetworkHelper.getWindowForRequest(aChannel); } catch (ex) { // getWindowForRequest() throws on b2g. } // Try to get the source window of the request. if (this.window && (!win || win.top !== this.window)) { return; } let httpActivity = this.createActivityObject(aChannel); // see NM__onRequestBodySent() httpActivity.charset = win ? win.document.characterSet : null; httpActivity.timings.REQUEST_HEADER = { first: aTimestamp, last: aTimestamp }; let httpVersionMaj = {}; let httpVersionMin = {}; let event = {}; event.startedDateTime = new Date(Math.round(aTimestamp / 1000)).toISOString(); event.headersSize = aExtraStringData.length; event.method = aChannel.requestMethod; event.url = aChannel.URI.spec; // Determine the HTTP version. aChannel.QueryInterface(Ci.nsIHttpChannelInternal); aChannel.getRequestVersion(httpVersionMaj, httpVersionMin); event.httpVersion = "HTTP/" + httpVersionMaj.value + "." + httpVersionMin.value; event.discardRequestBody = !this.saveRequestAndResponseBodies; event.discardResponseBody = !this.saveRequestAndResponseBodies; let headers = []; let cookies = []; let cookieHeader = null; // Copy the request header data. aChannel.visitRequestHeaders({ visitHeader: function NM__visitHeader(aName, aValue) { if (aName == "Cookie") { cookieHeader = aValue; } headers.push({ name: aName, value: aValue }); } }); if (cookieHeader) { cookies = NetworkHelper.parseCookieHeader(cookieHeader); } httpActivity.owner = this.owner.onNetworkEvent(event); this._setupResponseListener(httpActivity); this.openRequests[httpActivity.id] = httpActivity; httpActivity.owner.addRequestHeaders(headers); httpActivity.owner.addRequestCookies(cookies); }, /** * Create the empty HTTP activity object. This object is used for storing all * the request and response information. * * This is a HAR-like object. Conformance to the spec is not guaranteed at * this point. * * TODO: Bug 708717 - Add support for network log export to HAR * * @see http://www.softwareishard.com/blog/har-12-spec * @param nsIHttpChannel aChannel * The HTTP channel for which the HTTP activity object is created. * @return object * The new HTTP activity object. */ createActivityObject: function NM_createActivityObject(aChannel) { return { id: gSequenceId(), channel: aChannel, charset: null, // see NM__onRequestHeader() url: aChannel.URI.spec, discardRequestBody: !this.saveRequestAndResponseBodies, discardResponseBody: !this.saveRequestAndResponseBodies, timings: {}, // internal timing information, see NM_observeActivity() responseStatus: null, // see NM__onResponseHeader() owner: null, // the activity owner which is notified when changes happen }; }, /** * Setup the network response listener for the given HTTP activity. The * NetworkResponseListener is responsible for storing the response body. * * @private * @param object aHttpActivity * The HTTP activity object we are tracking. */ _setupResponseListener: function NM__setupResponseListener(aHttpActivity) { let channel = aHttpActivity.channel; channel.QueryInterface(Ci.nsITraceableChannel); // The response will be written into the outputStream of this pipe. // This allows us to buffer the data we are receiving and read it // asynchronously. // Both ends of the pipe must be blocking. let sink = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); // The streams need to be blocking because this is required by the // stream tee. sink.init(false, false, this.responsePipeSegmentSize, PR_UINT32_MAX, null); // Add listener for the response body. let newListener = new NetworkResponseListener(this, aHttpActivity); // Remember the input stream, so it isn't released by GC. newListener.inputStream = sink.inputStream; newListener.sink = sink; let tee = Cc["@mozilla.org/network/stream-listener-tee;1"]. createInstance(Ci.nsIStreamListenerTee); let originalListener = channel.setNewListener(tee); tee.init(originalListener, sink.outputStream, newListener); }, /** * Handler for ACTIVITY_SUBTYPE_REQUEST_BODY_SENT. The request body is logged * here. * * @private * @param object aHttpActivity * The HTTP activity object we are working with. */ _onRequestBodySent: function NM__onRequestBodySent(aHttpActivity) { if (aHttpActivity.discardRequestBody) { return; } let sentBody = NetworkHelper. readPostTextFromRequest(aHttpActivity.channel, aHttpActivity.charset); if (!sentBody && this.window && aHttpActivity.url == this.window.location.href) { // If the request URL is the same as the current page URL, then // we can try to get the posted text from the page directly. // This check is necessary as otherwise the // NetworkHelper.readPostTextFromPageViaWebNav() // function is called for image requests as well but these // are not web pages and as such don't store the posted text // in the cache of the webpage. let webNav = this.window.QueryInterface(Ci.nsIInterfaceRequestor). getInterface(Ci.nsIWebNavigation); sentBody = NetworkHelper. readPostTextFromPageViaWebNav(webNav, aHttpActivity.charset); } if (sentBody) { aHttpActivity.owner.addRequestPostData({ text: sentBody }); } }, /** * Handler for ACTIVITY_SUBTYPE_RESPONSE_HEADER. This method stores * information about the response headers. * * @private * @param object aHttpActivity * The HTTP activity object we are working with. * @param string aExtraStringData * The uncached response headers. */ _onResponseHeader: function NM__onResponseHeader(aHttpActivity, aExtraStringData) { // aExtraStringData contains the uncached response headers. The first line // contains the response status (e.g. HTTP/1.1 200 OK). // // Note: The response header is not saved here. Calling the // channel.visitResponseHeaders() methood at this point sometimes causes an // NS_ERROR_NOT_AVAILABLE exception. // // We could parse aExtraStringData to get the headers and their values, but // that is not trivial to do in an accurate manner. Hence, we save the // response headers in this._httpResponseExaminer(). let headers = aExtraStringData.split(/\r\n|\n|\r/); let statusLine = headers.shift(); let statusLineArray = statusLine.split(" "); let response = {}; response.httpVersion = statusLineArray.shift(); response.status = statusLineArray.shift(); response.statusText = statusLineArray.join(" "); response.headersSize = aExtraStringData.length; aHttpActivity.responseStatus = response.status; // Discard the response body for known response statuses. switch (parseInt(response.status)) { case HTTP_MOVED_PERMANENTLY: case HTTP_FOUND: case HTTP_SEE_OTHER: case HTTP_TEMPORARY_REDIRECT: aHttpActivity.discardResponseBody = true; break; } response.discardResponseBody = aHttpActivity.discardResponseBody; aHttpActivity.owner.addResponseStart(response); }, /** * Handler for ACTIVITY_SUBTYPE_TRANSACTION_CLOSE. This method updates the HAR * timing information on the HTTP activity object and clears the request * from the list of known open requests. * * @private * @param object aHttpActivity * The HTTP activity object we work with. */ _onTransactionClose: function NM__onTransactionClose(aHttpActivity) { let result = this._setupHarTimings(aHttpActivity); aHttpActivity.owner.addEventTimings(result.total, result.timings); delete this.openRequests[aHttpActivity.id]; }, /** * Update the HTTP activity object to include timing information as in the HAR * spec. The HTTP activity object holds the raw timing information in * |timings| - these are timings stored for each activity notification. The * HAR timing information is constructed based on these lower level data. * * @param object aHttpActivity * The HTTP activity object we are working with. * @return object * This object holds two properties: * - total - the total time for all of the request and response. * - timings - the HAR timings object. */ _setupHarTimings: function NM__setupHarTimings(aHttpActivity) { let timings = aHttpActivity.timings; let harTimings = {}; // Not clear how we can determine "blocked" time. harTimings.blocked = -1; // DNS timing information is available only in when the DNS record is not // cached. harTimings.dns = timings.STATUS_RESOLVING && timings.STATUS_RESOLVED ? timings.STATUS_RESOLVED.last - timings.STATUS_RESOLVING.first : -1; if (timings.STATUS_CONNECTING_TO && timings.STATUS_CONNECTED_TO) { harTimings.connect = timings.STATUS_CONNECTED_TO.last - timings.STATUS_CONNECTING_TO.first; } else if (timings.STATUS_SENDING_TO) { harTimings.connect = timings.STATUS_SENDING_TO.first - timings.REQUEST_HEADER.first; } else { harTimings.connect = -1; } if ((timings.STATUS_WAITING_FOR || timings.STATUS_RECEIVING_FROM) && (timings.STATUS_CONNECTED_TO || timings.STATUS_SENDING_TO)) { harTimings.send = (timings.STATUS_WAITING_FOR || timings.STATUS_RECEIVING_FROM).first - (timings.STATUS_CONNECTED_TO || timings.STATUS_SENDING_TO).last; } else { harTimings.send = -1; } if (timings.RESPONSE_START) { harTimings.wait = timings.RESPONSE_START.first - (timings.REQUEST_BODY_SENT || timings.STATUS_SENDING_TO).last; } else { harTimings.wait = -1; } if (timings.RESPONSE_START && timings.RESPONSE_COMPLETE) { harTimings.receive = timings.RESPONSE_COMPLETE.last - timings.RESPONSE_START.first; } else { harTimings.receive = -1; } let totalTime = 0; for (let timing in harTimings) { let time = Math.max(Math.round(harTimings[timing] / 1000), -1); harTimings[timing] = time; if (time > -1) { totalTime += time; } } return { total: totalTime, timings: harTimings, }; }, /** * Suspend Web Console activity. This is called when all Web Consoles are * closed. */ destroy: function NM_destroy() { Services.obs.removeObserver(this._httpResponseExaminer, "http-on-examine-response"); gActivityDistributor.removeObserver(this); this.openRequests = {}; this.openResponses = {}; this.owner = null; this.window = null; }, }; _global.NetworkMonitor = NetworkMonitor; _global.NetworkResponseListener = NetworkResponseListener; })(this, WebConsoleUtils); /** * A WebProgressListener that listens for location changes. * * This progress listener is used to track file loads and other kinds of * location changes. * * @constructor * @param object aWindow * The window for which we need to track location changes. * @param object aOwner * The listener owner which needs to implement two methods: * - onFileActivity(aFileURI) * - onLocationChange(aState, aTabURI, aPageTitle) */ this.ConsoleProgressListener = function ConsoleProgressListener(aWindow, aOwner) { this.window = aWindow; this.owner = aOwner; } ConsoleProgressListener.prototype = { /** * Constant used for startMonitor()/stopMonitor() that tells you want to * monitor file loads. */ MONITOR_FILE_ACTIVITY: 1, /** * Constant used for startMonitor()/stopMonitor() that tells you want to * monitor page location changes. */ MONITOR_LOCATION_CHANGE: 2, /** * Tells if you want to monitor file activity. * @private * @type boolean */ _fileActivity: false, /** * Tells if you want to monitor location changes. * @private * @type boolean */ _locationChange: false, /** * Tells if the console progress listener is initialized or not. * @private * @type boolean */ _initialized: false, _webProgress: null, QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), /** * Initialize the ConsoleProgressListener. * @private */ _init: function CPL__init() { if (this._initialized) { return; } this._webProgress = this.window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIWebProgress); this._webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_ALL); this._initialized = true; }, /** * Start a monitor/tracker related to the current nsIWebProgressListener * instance. * * @param number aMonitor * Tells what you want to track. Available constants: * - this.MONITOR_FILE_ACTIVITY * Track file loads. * - this.MONITOR_LOCATION_CHANGE * Track location changes for the top window. */ startMonitor: function CPL_startMonitor(aMonitor) { switch (aMonitor) { case this.MONITOR_FILE_ACTIVITY: this._fileActivity = true; break; case this.MONITOR_LOCATION_CHANGE: this._locationChange = true; break; default: throw new Error("ConsoleProgressListener: unknown monitor type " + aMonitor + "!"); } this._init(); }, /** * Stop a monitor. * * @param number aMonitor * Tells what you want to stop tracking. See this.startMonitor() for * the list of constants. */ stopMonitor: function CPL_stopMonitor(aMonitor) { switch (aMonitor) { case this.MONITOR_FILE_ACTIVITY: this._fileActivity = false; break; case this.MONITOR_LOCATION_CHANGE: this._locationChange = false; break; default: throw new Error("ConsoleProgressListener: unknown monitor type " + aMonitor + "!"); } if (!this._fileActivity && !this._locationChange) { this.destroy(); } }, onStateChange: function CPL_onStateChange(aProgress, aRequest, aState, aStatus) { if (!this.owner) { return; } if (this._fileActivity) { this._checkFileActivity(aProgress, aRequest, aState, aStatus); } if (this._locationChange) { this._checkLocationChange(aProgress, aRequest, aState, aStatus); } }, /** * Check if there is any file load, given the arguments of * nsIWebProgressListener.onStateChange. If the state change tells that a file * URI has been loaded, then the remote Web Console instance is notified. * @private */ _checkFileActivity: function CPL__checkFileActivity(aProgress, aRequest, aState, aStatus) { if (!(aState & Ci.nsIWebProgressListener.STATE_START)) { return; } let uri = null; if (aRequest instanceof Ci.imgIRequest) { let imgIRequest = aRequest.QueryInterface(Ci.imgIRequest); uri = imgIRequest.URI; } else if (aRequest instanceof Ci.nsIChannel) { let nsIChannel = aRequest.QueryInterface(Ci.nsIChannel); uri = nsIChannel.URI; } if (!uri || !uri.schemeIs("file") && !uri.schemeIs("ftp")) { return; } this.owner.onFileActivity(uri.spec); }, /** * Check if the current window.top location is changing, given the arguments * of nsIWebProgressListener.onStateChange. If that is the case, the remote * Web Console instance is notified. * @private */ _checkLocationChange: function CPL__checkLocationChange(aProgress, aRequest, aState, aStatus) { let isStart = aState & Ci.nsIWebProgressListener.STATE_START; let isStop = aState & Ci.nsIWebProgressListener.STATE_STOP; let isNetwork = aState & Ci.nsIWebProgressListener.STATE_IS_NETWORK; let isWindow = aState & Ci.nsIWebProgressListener.STATE_IS_WINDOW; // Skip non-interesting states. if (!isNetwork || !isWindow || aProgress.DOMWindow != this.window) { return; } if (isStart && aRequest instanceof Ci.nsIChannel) { this.owner.onLocationChange("start", aRequest.URI.spec, ""); } else if (isStop) { this.owner.onLocationChange("stop", this.window.location.href, this.window.document.title); } }, onLocationChange: function() {}, onStatusChange: function() {}, onProgressChange: function() {}, onSecurityChange: function() {}, /** * Destroy the ConsoleProgressListener. */ destroy: function CPL_destroy() { if (!this._initialized) { return; } this._initialized = false; this._fileActivity = false; this._locationChange = false; try { this._webProgress.removeProgressListener(this); } catch (ex) { // This can throw during browser shutdown. } this._webProgress = null; this.window = null; this.owner = null; }, }; function gSequenceId() { return gSequenceId.n++; } gSequenceId.n = 0;