diff options
Diffstat (limited to 'devtools/shared/webconsole')
55 files changed, 10831 insertions, 0 deletions
diff --git a/devtools/shared/webconsole/client.js b/devtools/shared/webconsole/client.js new file mode 100644 index 000000000..4cc5deedf --- /dev/null +++ b/devtools/shared/webconsole/client.js @@ -0,0 +1,652 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const EventEmitter = require("devtools/shared/event-emitter"); +const promise = require("promise"); +const defer = require("devtools/shared/defer"); +const {LongStringClient} = require("devtools/shared/client/main"); + +/** + * A WebConsoleClient is used as a front end for the WebConsoleActor that is + * created on the server, hiding implementation details. + * + * @param object debuggerClient + * The DebuggerClient instance we live for. + * @param object response + * The response packet received from the "startListeners" request sent to + * the WebConsoleActor. + */ +function WebConsoleClient(debuggerClient, response) { + this._actor = response.from; + this._client = debuggerClient; + this._longStrings = {}; + this.traits = response.traits || {}; + this.events = []; + this._networkRequests = new Map(); + + this.pendingEvaluationResults = new Map(); + this.onEvaluationResult = this.onEvaluationResult.bind(this); + this.onNetworkEvent = this._onNetworkEvent.bind(this); + this.onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this); + + this._client.addListener("evaluationResult", this.onEvaluationResult); + this._client.addListener("networkEvent", this.onNetworkEvent); + this._client.addListener("networkEventUpdate", this.onNetworkEventUpdate); + EventEmitter.decorate(this); +} + +exports.WebConsoleClient = WebConsoleClient; + +WebConsoleClient.prototype = { + _longStrings: null, + traits: null, + + /** + * Holds the network requests currently displayed by the Web Console. Each key + * represents the connection ID and the value is network request information. + * @private + * @type object + */ + _networkRequests: null, + + getNetworkRequest(actorId) { + return this._networkRequests.get(actorId); + }, + + hasNetworkRequest(actorId) { + return this._networkRequests.has(actorId); + }, + + removeNetworkRequest(actorId) { + this._networkRequests.delete(actorId); + }, + + getNetworkEvents() { + return this._networkRequests.values(); + }, + + get actor() { + return this._actor; + }, + + /** + * The "networkEvent" message type handler. We redirect any message to + * the UI for displaying. + * + * @private + * @param string type + * Message type. + * @param object packet + * The message received from the server. + */ + _onNetworkEvent: function (type, packet) { + if (packet.from == this._actor) { + let actor = packet.eventActor; + let networkInfo = { + _type: "NetworkEvent", + timeStamp: actor.timeStamp, + node: null, + actor: actor.actor, + discardRequestBody: true, + discardResponseBody: true, + startedDateTime: actor.startedDateTime, + request: { + url: actor.url, + method: actor.method, + }, + isXHR: actor.isXHR, + cause: actor.cause, + response: {}, + timings: {}, + // track the list of network event updates + updates: [], + private: actor.private, + fromCache: actor.fromCache, + fromServiceWorker: actor.fromServiceWorker + }; + this._networkRequests.set(actor.actor, networkInfo); + + this.emit("networkEvent", networkInfo); + } + }, + + /** + * The "networkEventUpdate" message type handler. We redirect any message to + * the UI for displaying. + * + * @private + * @param string type + * Message type. + * @param object packet + * The message received from the server. + */ + _onNetworkEventUpdate: function (type, packet) { + let networkInfo = this.getNetworkRequest(packet.from); + if (!networkInfo) { + return; + } + + networkInfo.updates.push(packet.updateType); + + switch (packet.updateType) { + case "requestHeaders": + networkInfo.request.headersSize = packet.headersSize; + break; + case "requestPostData": + networkInfo.discardRequestBody = packet.discardRequestBody; + networkInfo.request.bodySize = packet.dataSize; + break; + case "responseStart": + networkInfo.response.httpVersion = packet.response.httpVersion; + networkInfo.response.status = packet.response.status; + networkInfo.response.statusText = packet.response.statusText; + networkInfo.response.headersSize = packet.response.headersSize; + networkInfo.response.remoteAddress = packet.response.remoteAddress; + networkInfo.response.remotePort = packet.response.remotePort; + networkInfo.discardResponseBody = packet.response.discardResponseBody; + break; + case "responseContent": + networkInfo.response.content = { + mimeType: packet.mimeType, + }; + networkInfo.response.bodySize = packet.contentSize; + networkInfo.response.transferredSize = packet.transferredSize; + networkInfo.discardResponseBody = packet.discardResponseBody; + break; + case "eventTimings": + networkInfo.totalTime = packet.totalTime; + break; + case "securityInfo": + networkInfo.securityInfo = packet.state; + break; + } + + this.emit("networkEventUpdate", { + packet: packet, + networkInfo + }); + }, + + /** + * Retrieve the cached messages from the server. + * + * @see this.CACHED_MESSAGES + * @param array types + * The array of message types you want from the server. See + * this.CACHED_MESSAGES for known types. + * @param function onResponse + * The function invoked when the response is received. + */ + getCachedMessages: function (types, onResponse) { + let packet = { + to: this._actor, + type: "getCachedMessages", + messageTypes: types, + }; + this._client.request(packet, onResponse); + }, + + /** + * Inspect the properties of an object. + * + * @param string actor + * The WebConsoleObjectActor ID to send the request to. + * @param function onResponse + * The function invoked when the response is received. + */ + inspectObjectProperties: function (actor, onResponse) { + let packet = { + to: actor, + type: "inspectProperties", + }; + this._client.request(packet, onResponse); + }, + + /** + * Evaluate a JavaScript expression. + * + * @param string string + * The code you want to evaluate. + * @param function onResponse + * The function invoked when the response is received. + * @param object [options={}] + * Options for evaluation: + * + * - bindObjectActor: an ObjectActor ID. The OA holds a reference to + * a Debugger.Object that wraps a content object. This option allows + * you to bind |_self| to the D.O of the given OA, during string + * evaluation. + * + * See: Debugger.Object.executeInGlobalWithBindings() for information + * about bindings. + * + * Use case: the variable view needs to update objects and it does so + * by knowing the ObjectActor it inspects and binding |_self| to the + * D.O of the OA. As such, variable view sends strings like these for + * eval: + * _self["prop"] = value; + * + * - frameActor: a FrameActor ID. The FA holds a reference to + * a Debugger.Frame. This option allows you to evaluate the string in + * the frame of the given FA. + * + * - url: the url to evaluate the script as. Defaults to + * "debugger eval code". + * + * - selectedNodeActor: the NodeActor ID of the current + * selection in the Inspector, if such a selection + * exists. This is used by helper functions that can + * reference the currently selected node in the Inspector, + * like $0. + */ + evaluateJS: function (string, onResponse, options = {}) { + let packet = { + to: this._actor, + type: "evaluateJS", + text: string, + bindObjectActor: options.bindObjectActor, + frameActor: options.frameActor, + url: options.url, + selectedNodeActor: options.selectedNodeActor, + selectedObjectActor: options.selectedObjectActor, + }; + this._client.request(packet, onResponse); + }, + + /** + * Evaluate a JavaScript expression asynchronously. + * See evaluateJS for parameter and response information. + */ + evaluateJSAsync: function (string, onResponse, options = {}) { + // Pre-37 servers don't support async evaluation. + if (!this.traits.evaluateJSAsync) { + this.evaluateJS(string, onResponse, options); + return; + } + + let packet = { + to: this._actor, + type: "evaluateJSAsync", + text: string, + bindObjectActor: options.bindObjectActor, + frameActor: options.frameActor, + url: options.url, + selectedNodeActor: options.selectedNodeActor, + selectedObjectActor: options.selectedObjectActor, + }; + + this._client.request(packet, response => { + // Null check this in case the client has been detached while waiting + // for a response. + if (this.pendingEvaluationResults) { + this.pendingEvaluationResults.set(response.resultID, onResponse); + } + }); + }, + + /** + * Handler for the actors's unsolicited evaluationResult packet. + */ + onEvaluationResult: function (notification, packet) { + // The client on the main thread can receive notification packets from + // multiple webconsole actors: the one on the main thread and the ones + // on worker threads. So make sure we should be handling this request. + if (packet.from !== this._actor) { + return; + } + + // Find the associated callback based on this ID, and fire it. + // In a sync evaluation, this would have already been called in + // direct response to the client.request function. + let onResponse = this.pendingEvaluationResults.get(packet.resultID); + if (onResponse) { + onResponse(packet); + this.pendingEvaluationResults.delete(packet.resultID); + } else { + DevToolsUtils.reportException("onEvaluationResult", + "No response handler for an evaluateJSAsync result (resultID: " + + packet.resultID + ")"); + } + }, + + /** + * Autocomplete a JavaScript expression. + * + * @param string string + * The code you want to autocomplete. + * @param number cursor + * Cursor location inside the string. Index starts from 0. + * @param function onResponse + * The function invoked when the response is received. + * @param string frameActor + * The id of the frame actor that made the call. + */ + autocomplete: function (string, cursor, onResponse, frameActor) { + let packet = { + to: this._actor, + type: "autocomplete", + text: string, + cursor: cursor, + frameActor: frameActor, + }; + this._client.request(packet, onResponse); + }, + + /** + * Clear the cache of messages (page errors and console API calls). + */ + clearMessagesCache: function () { + let packet = { + to: this._actor, + type: "clearMessagesCache", + }; + this._client.request(packet); + }, + + /** + * Get Web Console-related preferences on the server. + * + * @param array preferences + * An array with the preferences you want to retrieve. + * @param function [onResponse] + * Optional function to invoke when the response is received. + */ + getPreferences: function (preferences, onResponse) { + let packet = { + to: this._actor, + type: "getPreferences", + preferences: preferences, + }; + this._client.request(packet, onResponse); + }, + + /** + * Set Web Console-related preferences on the server. + * + * @param object preferences + * An object with the preferences you want to change. + * @param function [onResponse] + * Optional function to invoke when the response is received. + */ + setPreferences: function (preferences, onResponse) { + let packet = { + to: this._actor, + type: "setPreferences", + preferences: preferences, + }; + this._client.request(packet, onResponse); + }, + + /** + * Retrieve the request headers from the given NetworkEventActor. + * + * @param string actor + * The NetworkEventActor ID. + * @param function onResponse + * The function invoked when the response is received. + */ + getRequestHeaders: function (actor, onResponse) { + let packet = { + to: actor, + type: "getRequestHeaders", + }; + this._client.request(packet, onResponse); + }, + + /** + * Retrieve the request cookies from the given NetworkEventActor. + * + * @param string actor + * The NetworkEventActor ID. + * @param function onResponse + * The function invoked when the response is received. + */ + getRequestCookies: function (actor, onResponse) { + let packet = { + to: actor, + type: "getRequestCookies", + }; + this._client.request(packet, onResponse); + }, + + /** + * Retrieve the request post data from the given NetworkEventActor. + * + * @param string actor + * The NetworkEventActor ID. + * @param function onResponse + * The function invoked when the response is received. + */ + getRequestPostData: function (actor, onResponse) { + let packet = { + to: actor, + type: "getRequestPostData", + }; + this._client.request(packet, onResponse); + }, + + /** + * Retrieve the response headers from the given NetworkEventActor. + * + * @param string actor + * The NetworkEventActor ID. + * @param function onResponse + * The function invoked when the response is received. + */ + getResponseHeaders: function (actor, onResponse) { + let packet = { + to: actor, + type: "getResponseHeaders", + }; + this._client.request(packet, onResponse); + }, + + /** + * Retrieve the response cookies from the given NetworkEventActor. + * + * @param string actor + * The NetworkEventActor ID. + * @param function onResponse + * The function invoked when the response is received. + */ + getResponseCookies: function (actor, onResponse) { + let packet = { + to: actor, + type: "getResponseCookies", + }; + this._client.request(packet, onResponse); + }, + + /** + * Retrieve the response content from the given NetworkEventActor. + * + * @param string actor + * The NetworkEventActor ID. + * @param function onResponse + * The function invoked when the response is received. + */ + getResponseContent: function (actor, onResponse) { + let packet = { + to: actor, + type: "getResponseContent", + }; + this._client.request(packet, onResponse); + }, + + /** + * Retrieve the timing information for the given NetworkEventActor. + * + * @param string actor + * The NetworkEventActor ID. + * @param function onResponse + * The function invoked when the response is received. + */ + getEventTimings: function (actor, onResponse) { + let packet = { + to: actor, + type: "getEventTimings", + }; + this._client.request(packet, onResponse); + }, + + /** + * Retrieve the security information for the given NetworkEventActor. + * + * @param string actor + * The NetworkEventActor ID. + * @param function onResponse + * The function invoked when the response is received. + */ + getSecurityInfo: function (actor, onResponse) { + let packet = { + to: actor, + type: "getSecurityInfo", + }; + this._client.request(packet, onResponse); + }, + + /** + * Send a HTTP request with the given data. + * + * @param string data + * The details of the HTTP request. + * @param function onResponse + * The function invoked when the response is received. + */ + sendHTTPRequest: function (data, onResponse) { + let packet = { + to: this._actor, + type: "sendHTTPRequest", + request: data + }; + this._client.request(packet, onResponse); + }, + + /** + * Start the given Web Console listeners. + * + * @see this.LISTENERS + * @param array listeners + * Array of listeners you want to start. See this.LISTENERS for + * known listeners. + * @param function onResponse + * Function to invoke when the server response is received. + */ + startListeners: function (listeners, onResponse) { + let packet = { + to: this._actor, + type: "startListeners", + listeners: listeners, + }; + this._client.request(packet, onResponse); + }, + + /** + * Stop the given Web Console listeners. + * + * @see this.LISTENERS + * @param array listeners + * Array of listeners you want to stop. See this.LISTENERS for + * known listeners. + * @param function onResponse + * Function to invoke when the server response is received. + */ + stopListeners: function (listeners, onResponse) { + let packet = { + to: this._actor, + type: "stopListeners", + listeners: listeners, + }; + this._client.request(packet, onResponse); + }, + + /** + * Return an instance of LongStringClient for the given long string grip. + * + * @param object grip + * The long string grip returned by the protocol. + * @return object + * The LongStringClient for the given long string grip. + */ + longString: function (grip) { + if (grip.actor in this._longStrings) { + return this._longStrings[grip.actor]; + } + + let client = new LongStringClient(this._client, grip); + this._longStrings[grip.actor] = client; + return client; + }, + + /** + * Close the WebConsoleClient. This stops all the listeners on the server and + * detaches from the console actor. + * + * @param function onResponse + * Function to invoke when the server response is received. + */ + detach: function (onResponse) { + this._client.removeListener("evaluationResult", this.onEvaluationResult); + this._client.removeListener("networkEvent", this.onNetworkEvent); + this._client.removeListener("networkEventUpdate", + this.onNetworkEventUpdate); + this.stopListeners(null, onResponse); + this._longStrings = null; + this._client = null; + this.pendingEvaluationResults.clear(); + this.pendingEvaluationResults = null; + this.clearNetworkRequests(); + this._networkRequests = null; + }, + + clearNetworkRequests: function () { + this._networkRequests.clear(); + }, + + /** + * Fetches the full text of a LongString. + * + * @param object | string stringGrip + * The long string grip containing the corresponding actor. + * If you pass in a plain string (by accident or because you're lazy), + * then a promise of the same string is simply returned. + * @return object Promise + * A promise that is resolved when the full string contents + * are available, or rejected if something goes wrong. + */ + getString: function (stringGrip) { + // Make sure this is a long string. + if (typeof stringGrip != "object" || stringGrip.type != "longString") { + // Go home string, you're drunk. + return promise.resolve(stringGrip); + } + + // Fetch the long string only once. + if (stringGrip._fullText) { + return stringGrip._fullText.promise; + } + + let deferred = stringGrip._fullText = defer(); + let { initial, length } = stringGrip; + let longStringClient = this.longString(stringGrip); + + longStringClient.substring(initial.length, length, response => { + if (response.error) { + DevToolsUtils.reportException("getString", + response.error + ": " + response.message); + + deferred.reject(response); + return; + } + deferred.resolve(initial + response.substring); + }); + + return deferred.promise; + } +}; diff --git a/devtools/shared/webconsole/js-property-provider.js b/devtools/shared/webconsole/js-property-provider.js new file mode 100644 index 000000000..9ada46732 --- /dev/null +++ b/devtools/shared/webconsole/js-property-provider.js @@ -0,0 +1,538 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 DevToolsUtils = require("devtools/shared/DevToolsUtils"); + +if (!isWorker) { + loader.lazyImporter(this, "Parser", "resource://devtools/shared/Parser.jsm"); +} + +// Provide an easy way to bail out of even attempting an autocompletion +// if an object has way too many properties. Protects against large objects +// with numeric values that wouldn't be tallied towards MAX_AUTOCOMPLETIONS. +const MAX_AUTOCOMPLETE_ATTEMPTS = exports.MAX_AUTOCOMPLETE_ATTEMPTS = 100000; +// Prevent iterating over too many properties during autocomplete suggestions. +const MAX_AUTOCOMPLETIONS = exports.MAX_AUTOCOMPLETIONS = 1500; + +const STATE_NORMAL = 0; +const STATE_QUOTE = 2; +const STATE_DQUOTE = 3; + +const OPEN_BODY = "{[(".split(""); +const CLOSE_BODY = "}])".split(""); +const OPEN_CLOSE_BODY = { + "{": "}", + "[": "]", + "(": ")", +}; + +function hasArrayIndex(str) { + return /\[\d+\]$/.test(str); +} + +/** + * Analyses a given string to find the last statement that is interesting for + * later completion. + * + * @param string str + * 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(str) { + let bodyStack = []; + + let state = STATE_NORMAL; + let start = 0; + let c; + for (let i = 0; i < str.length; i++) { + c = str[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) { + let 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 + * Debugger.Environment/Debugger.Object and inputValue. + * + * @param object dbgObject + * When the debugger is not paused this Debugger.Object wraps + * the scope for autocompletion. + * It is null if the debugger is paused. + * @param object anEnvironment + * When the debugger is paused this Debugger.Environment is the + * scope for autocompletion. + * It is null if the debugger is not paused. + * @param string inputValue + * Value that should be completed. + * @param number [cursor=inputValue.length] + * Optional offset in the input where the cursor is located. If this is + * omitted then the cursor is assumed to be at the end of the input + * value. + * @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(dbgObject, anEnvironment, inputValue, cursor) { + if (cursor === undefined) { + cursor = inputValue.length; + } + + inputValue = inputValue.substring(0, cursor); + + // Analyse the inputValue and find the beginning of the last part that + // should be completed. + let beginning = findCompletionBeginning(inputValue); + + // 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 = inputValue.substring(beginning.startPos); + let lastDot = completionPart.lastIndexOf("."); + + // Don't complete on just an empty string. + if (completionPart.trim() == "") { + return null; + } + + // Catch literals like [1,2,3] or "foo" and return the matches from + // their prototypes. + // Don't run this is a worker, migrating to acorn should allow this + // to run in a worker - Bug 1217198. + if (!isWorker && lastDot > 0) { + let parser = new Parser(); + parser.logExceptions = false; + let syntaxTree = parser.get(completionPart.slice(0, lastDot)); + let lastTree = syntaxTree.getLastSyntaxTree(); + let lastBody = lastTree && lastTree.AST.body[lastTree.AST.body.length - 1]; + + // Finding the last expression since we've sliced up until the dot. + // If there were parse errors this won't exist. + if (lastBody) { + let expression = lastBody.expression; + let matchProp = completionPart.slice(lastDot + 1); + if (expression.type === "ArrayExpression") { + return getMatchedProps(Array.prototype, matchProp); + } else if (expression.type === "Literal" && + (typeof expression.value === "string")) { + return getMatchedProps(String.prototype, matchProp); + } + } + } + + // We are completing a variable / a property lookup. + let properties = completionPart.split("."); + let matchProp = properties.pop().trimLeft(); + let obj = dbgObject; + + // The first property must be found in the environment of the paused debugger + // or of the global lexical scope. + let env = anEnvironment || obj.asEnvironment(); + + if (properties.length === 0) { + return getMatchedPropsInEnvironment(env, matchProp); + } + + let firstProp = properties.shift().trim(); + if (firstProp === "this") { + // Special case for 'this' - try to get the Object from the Environment. + // No problem if it throws, we will just not autocomplete. + try { + obj = env.object; + } catch (e) { + // Ignore. + } + } else if (hasArrayIndex(firstProp)) { + obj = getArrayMemberProperty(null, env, firstProp); + } else { + obj = getVariableInEnvironment(env, firstProp); + } + + if (!isObjectUsable(obj)) { + return null; + } + + // We get the rest of the properties recursively starting from the + // Debugger.Object that wraps the first property + for (let i = 0; i < properties.length; i++) { + let prop = properties[i].trim(); + if (!prop) { + return null; + } + + if (hasArrayIndex(prop)) { + // The property to autocomplete is a member of array. For example + // list[i][j]..[n]. Traverse the array to get the actual element. + obj = getArrayMemberProperty(obj, null, prop); + } else { + obj = DevToolsUtils.getProperty(obj, prop); + } + + if (!isObjectUsable(obj)) { + return null; + } + } + + // If the final property is a primitive + if (typeof obj != "object") { + return getMatchedProps(obj, matchProp); + } + + return getMatchedPropsInDbgObject(obj, matchProp); +} + +/** + * Get the array member of obj for the given prop. For example, given + * prop='list[0][1]' the element at [0][1] of obj.list is returned. + * + * @param object obj + * The object to operate on. Should be null if env is passed. + * @param object env + * The Environment to operate in. Should be null if obj is passed. + * @param string prop + * The property to return. + * @return null or Object + * Returns null if the property couldn't be located. Otherwise the array + * member identified by prop. + */ +function getArrayMemberProperty(obj, env, prop) { + // First get the array. + let propWithoutIndices = prop.substr(0, prop.indexOf("[")); + + if (env) { + obj = getVariableInEnvironment(env, propWithoutIndices); + } else { + obj = DevToolsUtils.getProperty(obj, propWithoutIndices); + } + + if (!isObjectUsable(obj)) { + return null; + } + + // Then traverse the list of indices to get the actual element. + let result; + let arrayIndicesRegex = /\[[^\]]*\]/g; + while ((result = arrayIndicesRegex.exec(prop)) !== null) { + let indexWithBrackets = result[0]; + let indexAsText = indexWithBrackets.substr(1, indexWithBrackets.length - 2); + let index = parseInt(indexAsText, 10); + + if (isNaN(index)) { + return null; + } + + obj = DevToolsUtils.getProperty(obj, index); + + if (!isObjectUsable(obj)) { + return null; + } + } + + return obj; +} + +/** + * Check if the given Debugger.Object can be used for autocomplete. + * + * @param Debugger.Object object + * The Debugger.Object to check. + * @return boolean + * True if further inspection into the object is possible, or false + * otherwise. + */ +function isObjectUsable(object) { + if (object == null) { + return false; + } + + if (typeof object == "object" && object.class == "DeadObject") { + return false; + } + + return true; +} + +/** + * @see getExactMatchImpl() + */ +function getVariableInEnvironment(anEnvironment, name) { + return getExactMatchImpl(anEnvironment, name, DebuggerEnvironmentSupport); +} + +/** + * @see getMatchedPropsImpl() + */ +function getMatchedPropsInEnvironment(anEnvironment, match) { + return getMatchedPropsImpl(anEnvironment, match, DebuggerEnvironmentSupport); +} + +/** + * @see getMatchedPropsImpl() + */ +function getMatchedPropsInDbgObject(dbgObject, match) { + return getMatchedPropsImpl(dbgObject, match, DebuggerObjectSupport); +} + +/** + * @see getMatchedPropsImpl() + */ +function getMatchedProps(obj, match) { + if (typeof obj != "object") { + obj = obj.constructor.prototype; + } + return getMatchedPropsImpl(obj, match, JSObjectSupport); +} + +/** + * Get all properties in the given object (and its parent prototype chain) that + * match a given prefix. + * + * @param mixed obj + * Object whose properties we want to filter. + * @param string match + * Filter for properties that match this string. + * @return object + * Object that contains the matchProp and the list of names. + */ +function getMatchedPropsImpl(obj, match, {chainIterator, getProperties}) { + let matches = new Set(); + let numProps = 0; + + // We need to go up the prototype chain. + let iter = chainIterator(obj); + for (obj of iter) { + let props = getProperties(obj); + numProps += props.length; + + // If there are too many properties to event attempt autocompletion, + // or if we have already added the max number, then stop looping + // and return the partial set that has already been discovered. + if (numProps >= MAX_AUTOCOMPLETE_ATTEMPTS || + matches.size >= MAX_AUTOCOMPLETIONS) { + break; + } + + for (let i = 0; i < props.length; i++) { + let prop = props[i]; + if (prop.indexOf(match) != 0) { + continue; + } + if (prop.indexOf("-") > -1) { + continue; + } + // 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 (+prop != +prop) { + matches.add(prop); + } + + if (matches.size >= MAX_AUTOCOMPLETIONS) { + break; + } + } + } + + return { + matchProp: match, + matches: [...matches], + }; +} + +/** + * Returns a property value based on its name from the given object, by + * recursively checking the object's prototype. + * + * @param object obj + * An object to look the property into. + * @param string name + * The property that is looked up. + * @returns object|undefined + * A Debugger.Object if the property exists in the object's prototype + * chain, undefined otherwise. + */ +function getExactMatchImpl(obj, name, {chainIterator, getProperty}) { + // We need to go up the prototype chain. + let iter = chainIterator(obj); + for (obj of iter) { + let prop = getProperty(obj, name, obj); + if (prop) { + return prop.value; + } + } + return undefined; +} + +var JSObjectSupport = { + chainIterator: function* (obj) { + while (obj) { + yield obj; + obj = Object.getPrototypeOf(obj); + } + }, + + getProperties: function (obj) { + return Object.getOwnPropertyNames(obj); + }, + + getProperty: function () { + // getProperty is unsafe with raw JS objects. + throw new Error("Unimplemented!"); + }, +}; + +var DebuggerObjectSupport = { + chainIterator: function* (obj) { + while (obj) { + yield obj; + obj = obj.proto; + } + }, + + getProperties: function (obj) { + return obj.getOwnPropertyNames(); + }, + + getProperty: function (obj, name, rootObj) { + // This is left unimplemented in favor to DevToolsUtils.getProperty(). + throw new Error("Unimplemented!"); + }, +}; + +var DebuggerEnvironmentSupport = { + chainIterator: function* (obj) { + while (obj) { + yield obj; + obj = obj.parent; + } + }, + + getProperties: function (obj) { + let names = obj.names(); + + // Include 'this' in results (in sorted order) + for (let i = 0; i < names.length; i++) { + if (i === names.length - 1 || names[i + 1] > "this") { + names.splice(i + 1, 0, "this"); + break; + } + } + + return names; + }, + + getProperty: function (obj, name) { + let result; + // Try/catch since name can be anything, and getVariable throws if + // it's not a valid ECMAScript identifier name + try { + // TODO: we should use getVariableDescriptor() here - bug 725815. + result = obj.getVariable(name); + } catch (e) { + // Ignore. + } + + // FIXME: Need actual UI, bug 941287. + if (result === undefined || result.optimizedOut || + result.missingArguments) { + return null; + } + return { value: result }; + }, +}; + +exports.JSPropertyProvider = DevToolsUtils.makeInfallible(JSPropertyProvider); + +// Export a version that will throw (for tests) +exports.FallibleJSPropertyProvider = JSPropertyProvider; diff --git a/devtools/shared/webconsole/moz.build b/devtools/shared/webconsole/moz.build new file mode 100644 index 000000000..2ff6ed57f --- /dev/null +++ b/devtools/shared/webconsole/moz.build @@ -0,0 +1,19 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +if CONFIG['OS_TARGET'] != 'Android': + MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini'] + XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini'] + +DevToolsModules( + 'client.js', + 'js-property-provider.js', + 'network-helper.js', + 'network-monitor.js', + 'server-logger-monitor.js', + 'server-logger.js', + 'throttle.js', +) diff --git a/devtools/shared/webconsole/network-helper.js b/devtools/shared/webconsole/network-helper.js new file mode 100644 index 000000000..af6a2e55b --- /dev/null +++ b/devtools/shared/webconsole/network-helper.js @@ -0,0 +1,814 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* + * Software License Agreement (BSD License) + * + * Copyright (c) 2007, Parakey Inc. + * All rights reserved. + * + * Redistribution and use of this software in source and binary forms, + * with or without modification, are permitted provided that the + * following conditions are met: + * + * * Redistributions of source code must retain the above + * copyright notice, this list of conditions and the + * following disclaimer. + * + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the + * following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * * Neither the name of Parakey Inc. nor the names of its + * contributors may be used to endorse or promote products + * derived from this software without specific prior + * written permission of Parakey Inc. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Creator: + * Joe Hewitt + * Contributors + * John J. Barton (IBM Almaden) + * Jan Odvarko (Mozilla Corp.) + * Max Stepanov (Aptana Inc.) + * Rob Campbell (Mozilla Corp.) + * Hans Hillen (Paciello Group, Mozilla) + * Curtis Bartley (Mozilla Corp.) + * Mike Collins (IBM Almaden) + * Kevin Decker + * Mike Ratcliffe (Comartis AG) + * Hernan RodrÃguez Colmeiro + * Austin Andrews + * Christoph Dorn + * Steven Roussey (AppCenter Inc, Network54) + * Mihai Sucan (Mozilla Corp.) + */ + +"use strict"; + +const {components, Cc, Ci} = require("chrome"); +loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const Services = require("Services"); + +// The cache used in the `nsIURL` function. +const gNSURLStore = new Map(); + +/** + * Helper object for networking stuff. + * + * Most of the following functions have been taken from the Firebug source. They + * have been modified to match the Firefox coding rules. + */ +var NetworkHelper = { + /** + * Converts text with a given charset to unicode. + * + * @param string text + * Text to convert. + * @param string charset + * Charset to convert the text to. + * @returns string + * Converted text. + */ + convertToUnicode: function (text, charset) { + let conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + try { + conv.charset = charset || "UTF-8"; + return conv.ConvertToUnicode(text); + } catch (ex) { + return text; + } + }, + + /** + * Reads all available bytes from stream and converts them to charset. + * + * @param nsIInputStream stream + * @param string charset + * @returns string + * UTF-16 encoded string based on the content of stream and charset. + */ + readAndConvertFromStream: function (stream, charset) { + let text = null; + try { + text = NetUtil.readInputStreamToString(stream, stream.available()); + return this.convertToUnicode(text, charset); + } catch (err) { + return text; + } + }, + + /** + * Reads the posted text from request. + * + * @param nsIHttpChannel request + * @param string charset + * The content document charset, used when reading the POSTed data. + * @returns string or null + * Returns the posted string if it was possible to read from request + * otherwise null. + */ + readPostTextFromRequest: function (request, charset) { + if (request instanceof Ci.nsIUploadChannel) { + let iStream = request.uploadStream; + + let isSeekableStream = false; + if (iStream instanceof Ci.nsISeekableStream) { + isSeekableStream = true; + } + + let prevOffset; + if (isSeekableStream) { + prevOffset = iStream.tell(); + iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); + } + + // Read data from the stream. + let text = this.readAndConvertFromStream(iStream, charset); + + // Seek locks the file, so seek to the beginning only if necko hasn't + // read it yet, since necko doesn't seek to 0 before reading (at lest + // not till 459384 is fixed). + if (isSeekableStream && prevOffset == 0) { + iStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); + } + return text; + } + return null; + }, + + /** + * Reads the posted text from the page's cache. + * + * @param nsIDocShell docShell + * @param string charset + * @returns string or null + * Returns the posted string if it was possible to read from + * docShell otherwise null. + */ + readPostTextFromPage: function (docShell, charset) { + let webNav = docShell.QueryInterface(Ci.nsIWebNavigation); + return this.readPostTextFromPageViaWebNav(webNav, charset); + }, + + /** + * Reads the posted text from the page's cache, given an nsIWebNavigation + * object. + * + * @param nsIWebNavigation webNav + * @param string charset + * @returns string or null + * Returns the posted string if it was possible to read from + * webNav, otherwise null. + */ + readPostTextFromPageViaWebNav: function (webNav, charset) { + if (webNav instanceof Ci.nsIWebPageDescriptor) { + let descriptor = webNav.currentDescriptor; + + if (descriptor instanceof Ci.nsISHEntry && descriptor.postData && + descriptor instanceof Ci.nsISeekableStream) { + descriptor.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0); + + return this.readAndConvertFromStream(descriptor, charset); + } + } + return null; + }, + + /** + * Gets the web appId that is associated with request. + * + * @param nsIHttpChannel request + * @returns number|null + * The appId for the given request, if available. + */ + getAppIdForRequest: function (request) { + try { + return this.getRequestLoadContext(request).appId; + } catch (ex) { + // request loadContent is not always available. + } + return null; + }, + + /** + * Gets the topFrameElement that is associated with request. This + * works in single-process and multiprocess contexts. It may cross + * the content/chrome boundary. + * + * @param nsIHttpChannel request + * @returns nsIDOMElement|null + * The top frame element for the given request. + */ + getTopFrameForRequest: function (request) { + try { + return this.getRequestLoadContext(request).topFrameElement; + } catch (ex) { + // request loadContent is not always available. + } + return null; + }, + + /** + * Gets the nsIDOMWindow that is associated with request. + * + * @param nsIHttpChannel request + * @returns nsIDOMWindow or null + */ + getWindowForRequest: function (request) { + try { + return this.getRequestLoadContext(request).associatedWindow; + } catch (ex) { + // TODO: bug 802246 - getWindowForRequest() throws on b2g: there is no + // associatedWindow property. + } + return null; + }, + + /** + * Gets the nsILoadContext that is associated with request. + * + * @param nsIHttpChannel request + * @returns nsILoadContext or null + */ + getRequestLoadContext: function (request) { + try { + return request.notificationCallbacks.getInterface(Ci.nsILoadContext); + } catch (ex) { + // Ignore. + } + + try { + return request.loadGroup.notificationCallbacks + .getInterface(Ci.nsILoadContext); + } catch (ex) { + // Ignore. + } + + return null; + }, + + /** + * Determines whether the request has been made for the top level document. + * + * @param nsIHttpChannel request + * @returns Boolean True if the request represents the top level document. + */ + isTopLevelLoad: function (request) { + if (request instanceof Ci.nsIChannel) { + let loadInfo = request.loadInfo; + if (loadInfo && loadInfo.isTopLevelLoad) { + return (request.loadFlags & Ci.nsIChannel.LOAD_DOCUMENT_URI); + } + } + + return false; + }, + + /** + * Loads the content of url from the cache. + * + * @param string url + * URL to load the cached content for. + * @param string charset + * Assumed charset of the cached content. Used if there is no charset + * on the channel directly. + * @param function callback + * Callback that is called with the loaded cached content if available + * or null if something failed while getting the cached content. + */ + loadFromCache: function (url, charset, callback) { + let channel = NetUtil.newChannel({uri: url, + loadUsingSystemPrincipal: true}); + + // Ensure that we only read from the cache and not the server. + channel.loadFlags = Ci.nsIRequest.LOAD_FROM_CACHE | + Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE | + Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY; + + NetUtil.asyncFetch( + channel, + (inputStream, statusCode, request) => { + if (!components.isSuccessCode(statusCode)) { + callback(null); + return; + } + + // Try to get the encoding from the channel. If there is none, then use + // the passed assumed charset. + let requestChannel = request.QueryInterface(Ci.nsIChannel); + let contentCharset = requestChannel.contentCharset || charset; + + // Read the content of the stream using contentCharset as encoding. + callback(this.readAndConvertFromStream(inputStream, contentCharset)); + }); + }, + + /** + * Parse a raw Cookie header value. + * + * @param string header + * The raw Cookie header value. + * @return array + * Array holding an object for each cookie. Each object holds the + * following properties: name and value. + */ + parseCookieHeader: function (header) { + let cookies = header.split(";"); + let result = []; + + cookies.forEach(function (cookie) { + let equal = cookie.indexOf("="); + let name = cookie.substr(0, equal); + let value = cookie.substr(equal + 1); + result.push({name: unescape(name.trim()), + value: unescape(value.trim())}); + }); + + return result; + }, + + /** + * Parse a raw Set-Cookie header value. + * + * @param string header + * The raw Set-Cookie header value. + * @return array + * Array holding an object for each cookie. Each object holds the + * following properties: name, value, secure (boolean), httpOnly + * (boolean), path, domain and expires (ISO date string). + */ + parseSetCookieHeader: function (header) { + let rawCookies = header.split(/\r\n|\n|\r/); + let cookies = []; + + rawCookies.forEach(function (cookie) { + let equal = cookie.indexOf("="); + let name = unescape(cookie.substr(0, equal).trim()); + let parts = cookie.substr(equal + 1).split(";"); + let value = unescape(parts.shift().trim()); + + cookie = {name: name, value: value}; + + parts.forEach(function (part) { + part = part.trim(); + if (part.toLowerCase() == "secure") { + cookie.secure = true; + } else if (part.toLowerCase() == "httponly") { + cookie.httpOnly = true; + } else if (part.indexOf("=") > -1) { + let pair = part.split("="); + pair[0] = pair[0].toLowerCase(); + if (pair[0] == "path" || pair[0] == "domain") { + cookie[pair[0]] = pair[1]; + } else if (pair[0] == "expires") { + try { + pair[1] = pair[1].replace(/-/g, " "); + cookie.expires = new Date(pair[1]).toISOString(); + } catch (ex) { + // Ignore. + } + } + } + }); + + cookies.push(cookie); + }); + + return cookies; + }, + + // This is a list of all the mime category maps jviereck could find in the + // firebug code base. + mimeCategoryMap: { + "text/plain": "txt", + "text/html": "html", + "text/xml": "xml", + "text/xsl": "txt", + "text/xul": "txt", + "text/css": "css", + "text/sgml": "txt", + "text/rtf": "txt", + "text/x-setext": "txt", + "text/richtext": "txt", + "text/javascript": "js", + "text/jscript": "txt", + "text/tab-separated-values": "txt", + "text/rdf": "txt", + "text/xif": "txt", + "text/ecmascript": "js", + "text/vnd.curl": "txt", + "text/x-json": "json", + "text/x-js": "txt", + "text/js": "txt", + "text/vbscript": "txt", + "view-source": "txt", + "view-fragment": "txt", + "application/xml": "xml", + "application/xhtml+xml": "xml", + "application/atom+xml": "xml", + "application/rss+xml": "xml", + "application/vnd.mozilla.maybe.feed": "xml", + "application/vnd.mozilla.xul+xml": "xml", + "application/javascript": "js", + "application/x-javascript": "js", + "application/x-httpd-php": "txt", + "application/rdf+xml": "xml", + "application/ecmascript": "js", + "application/http-index-format": "txt", + "application/json": "json", + "application/x-js": "txt", + "application/x-mpegurl": "txt", + "application/vnd.apple.mpegurl": "txt", + "multipart/mixed": "txt", + "multipart/x-mixed-replace": "txt", + "image/svg+xml": "svg", + "application/octet-stream": "bin", + "image/jpeg": "image", + "image/jpg": "image", + "image/gif": "image", + "image/png": "image", + "image/bmp": "image", + "application/x-shockwave-flash": "flash", + "video/x-flv": "flash", + "audio/mpeg3": "media", + "audio/x-mpeg-3": "media", + "video/mpeg": "media", + "video/x-mpeg": "media", + "video/vnd.mpeg.dash.mpd": "xml", + "audio/ogg": "media", + "application/ogg": "media", + "application/x-ogg": "media", + "application/x-midi": "media", + "audio/midi": "media", + "audio/x-mid": "media", + "audio/x-midi": "media", + "music/crescendo": "media", + "audio/wav": "media", + "audio/x-wav": "media", + "text/json": "json", + "application/x-json": "json", + "application/json-rpc": "json", + "application/x-web-app-manifest+json": "json", + "application/manifest+json": "json" + }, + + /** + * Check if the given MIME type is a text-only MIME type. + * + * @param string mimeType + * @return boolean + */ + isTextMimeType: function (mimeType) { + if (mimeType.indexOf("text/") == 0) { + return true; + } + + // XML and JSON often come with custom MIME types, so in addition to the + // standard "application/xml" and "application/json", we also look for + // variants like "application/x-bigcorp+xml". For JSON we allow "+json" and + // "-json" as suffixes. + if (/^application\/\w+(?:[\.-]\w+)*(?:\+xml|[-+]json)$/.test(mimeType)) { + return true; + } + + let category = this.mimeCategoryMap[mimeType] || null; + switch (category) { + case "txt": + case "js": + case "json": + case "css": + case "html": + case "svg": + case "xml": + return true; + + default: + return false; + } + }, + + /** + * Takes a securityInfo object of nsIRequest, the nsIRequest itself and + * extracts security information from them. + * + * @param object securityInfo + * The securityInfo object of a request. If null channel is assumed + * to be insecure. + * @param object httpActivity + * The httpActivity object for the request with at least members + * { private, hostname }. + * + * @return object + * Returns an object containing following members: + * - state: The security of the connection used to fetch this + * request. Has one of following string values: + * * "insecure": the connection was not secure (only http) + * * "weak": the connection has minor security issues + * * "broken": secure connection failed (e.g. expired cert) + * * "secure": the connection was properly secured. + * If state == broken: + * - errorMessage: full error message from + * nsITransportSecurityInfo. + * If state == secure: + * - protocolVersion: one of TLSv1, TLSv1.1, TLSv1.2, TLSv1.3. + * - cipherSuite: the cipher suite used in this connection. + * - cert: information about certificate used in this connection. + * See parseCertificateInfo for the contents. + * - hsts: true if host uses Strict Transport Security, + * false otherwise + * - hpkp: true if host uses Public Key Pinning, false otherwise + * If state == weak: Same as state == secure and + * - weaknessReasons: list of reasons that cause the request to be + * considered weak. See getReasonsForWeakness. + */ + parseSecurityInfo: function (securityInfo, httpActivity) { + const info = { + state: "insecure", + }; + + // The request did not contain any security info. + if (!securityInfo) { + return info; + } + + /** + * Different scenarios to consider here and how they are handled: + * - request is HTTP, the connection is not secure + * => securityInfo is null + * => state === "insecure" + * + * - request is HTTPS, the connection is secure + * => .securityState has STATE_IS_SECURE flag + * => state === "secure" + * + * - request is HTTPS, the connection has security issues + * => .securityState has STATE_IS_INSECURE flag + * => .errorCode is an NSS error code. + * => state === "broken" + * + * - request is HTTPS, the connection was terminated before the security + * could be validated + * => .securityState has STATE_IS_INSECURE flag + * => .errorCode is NOT an NSS error code. + * => .errorMessage is not available. + * => state === "insecure" + * + * - request is HTTPS but it uses a weak cipher or old protocol, see + * http://hg.mozilla.org/mozilla-central/annotate/def6ed9d1c1a/ + * security/manager/ssl/nsNSSCallbacks.cpp#l1233 + * - request is mixed content (which makes no sense whatsoever) + * => .securityState has STATE_IS_BROKEN flag + * => .errorCode is NOT an NSS error code + * => .errorMessage is not available + * => state === "weak" + */ + + securityInfo.QueryInterface(Ci.nsITransportSecurityInfo); + securityInfo.QueryInterface(Ci.nsISSLStatusProvider); + + const wpl = Ci.nsIWebProgressListener; + const NSSErrorsService = Cc["@mozilla.org/nss_errors_service;1"] + .getService(Ci.nsINSSErrorsService); + const SSLStatus = securityInfo.SSLStatus; + if (!NSSErrorsService.isNSSErrorCode(securityInfo.errorCode)) { + const state = securityInfo.securityState; + + let uri = null; + if (httpActivity.channel && httpActivity.channel.URI) { + uri = httpActivity.channel.URI; + } + if (uri && !uri.schemeIs("https") && !uri.schemeIs("wss")) { + // it is not enough to look at the transport security info - + // schemes other than https and wss are subject to + // downgrade/etc at the scheme level and should always be + // considered insecure + info.state = "insecure"; + } else if (state & wpl.STATE_IS_SECURE) { + // The connection is secure if the scheme is sufficient + info.state = "secure"; + } else if (state & wpl.STATE_IS_BROKEN) { + // The connection is not secure, there was no error but there's some + // minor security issues. + info.state = "weak"; + info.weaknessReasons = this.getReasonsForWeakness(state); + } else if (state & wpl.STATE_IS_INSECURE) { + // This was most likely an https request that was aborted before + // validation. Return info as info.state = insecure. + return info; + } else { + DevToolsUtils.reportException("NetworkHelper.parseSecurityInfo", + "Security state " + state + " has no known STATE_IS_* flags."); + return info; + } + + // Cipher suite. + info.cipherSuite = SSLStatus.cipherName; + + // Protocol version. + info.protocolVersion = + this.formatSecurityProtocol(SSLStatus.protocolVersion); + + // Certificate. + info.cert = this.parseCertificateInfo(SSLStatus.serverCert); + + // HSTS and HPKP if available. + if (httpActivity.hostname) { + const sss = Cc["@mozilla.org/ssservice;1"] + .getService(Ci.nsISiteSecurityService); + + // SiteSecurityService uses different storage if the channel is + // private. Thus we must give isSecureHost correct flags or we + // might get incorrect results. + let flags = (httpActivity.private) ? + Ci.nsISocketProvider.NO_PERMANENT_STORAGE : 0; + + let host = httpActivity.hostname; + + info.hsts = sss.isSecureHost(sss.HEADER_HSTS, host, flags); + info.hpkp = sss.isSecureHost(sss.HEADER_HPKP, host, flags); + } else { + DevToolsUtils.reportException("NetworkHelper.parseSecurityInfo", + "Could not get HSTS/HPKP status as hostname is not available."); + info.hsts = false; + info.hpkp = false; + } + } else { + // The connection failed. + info.state = "broken"; + info.errorMessage = securityInfo.errorMessage; + } + + return info; + }, + + /** + * Takes an nsIX509Cert and returns an object with certificate information. + * + * @param nsIX509Cert cert + * The certificate to extract the information from. + * @return object + * An object with following format: + * { + * subject: { commonName, organization, organizationalUnit }, + * issuer: { commonName, organization, organizationUnit }, + * validity: { start, end }, + * fingerprint: { sha1, sha256 } + * } + */ + parseCertificateInfo: function (cert) { + let info = {}; + if (cert) { + info.subject = { + commonName: cert.commonName, + organization: cert.organization, + organizationalUnit: cert.organizationalUnit, + }; + + info.issuer = { + commonName: cert.issuerCommonName, + organization: cert.issuerOrganization, + organizationUnit: cert.issuerOrganizationUnit, + }; + + info.validity = { + start: cert.validity.notBeforeLocalDay, + end: cert.validity.notAfterLocalDay, + }; + + info.fingerprint = { + sha1: cert.sha1Fingerprint, + sha256: cert.sha256Fingerprint, + }; + } else { + DevToolsUtils.reportException("NetworkHelper.parseCertificateInfo", + "Secure connection established without certificate."); + } + + return info; + }, + + /** + * Takes protocolVersion of SSLStatus object and returns human readable + * description. + * + * @param Number version + * One of nsISSLStatus version constants. + * @return string + * One of TLSv1, TLSv1.1, TLSv1.2, TLSv1.3 if @param version + * is valid, Unknown otherwise. + */ + formatSecurityProtocol: function (version) { + switch (version) { + case Ci.nsISSLStatus.TLS_VERSION_1: + return "TLSv1"; + case Ci.nsISSLStatus.TLS_VERSION_1_1: + return "TLSv1.1"; + case Ci.nsISSLStatus.TLS_VERSION_1_2: + return "TLSv1.2"; + case Ci.nsISSLStatus.TLS_VERSION_1_3: + return "TLSv1.3"; + default: + DevToolsUtils.reportException("NetworkHelper.formatSecurityProtocol", + "protocolVersion " + version + " is unknown."); + return "Unknown"; + } + }, + + /** + * Takes the securityState bitfield and returns reasons for weak connection + * as an array of strings. + * + * @param Number state + * nsITransportSecurityInfo.securityState. + * + * @return Array[String] + * List of weakness reasons. A subset of { cipher } where + * * cipher: The cipher suite is consireded to be weak (RC4). + */ + getReasonsForWeakness: function (state) { + const wpl = Ci.nsIWebProgressListener; + + // If there's non-fatal security issues the request has STATE_IS_BROKEN + // flag set. See http://hg.mozilla.org/mozilla-central/file/44344099d119 + // /security/manager/ssl/nsNSSCallbacks.cpp#l1233 + let reasons = []; + + if (state & wpl.STATE_IS_BROKEN) { + let isCipher = state & wpl.STATE_USES_WEAK_CRYPTO; + + if (isCipher) { + reasons.push("cipher"); + } + + if (!isCipher) { + DevToolsUtils.reportException("NetworkHelper.getReasonsForWeakness", + "STATE_IS_BROKEN without a known reason. Full state was: " + state); + } + } + + return reasons; + }, + + /** + * Parse a url's query string into its components + * + * @param string queryString + * The query part of a url + * @return array + * Array of query params {name, value} + */ + parseQueryString: function (queryString) { + // Make sure there's at least one param available. + // Be careful here, params don't necessarily need to have values, so + // no need to verify the existence of a "=". + if (!queryString) { + return null; + } + + // Turn the params string into an array containing { name: value } tuples. + let paramsArray = queryString.replace(/^[?&]/, "").split("&").map(e => { + let param = e.split("="); + return { + name: param[0] ? + NetworkHelper.convertToUnicode(unescape(param[0])) : "", + value: param[1] ? + NetworkHelper.convertToUnicode(unescape(param[1])) : "" + }; + }); + + return paramsArray; + }, + + /** + * Helper for getting an nsIURL instance out of a string. + */ + nsIURL: function (url, store = gNSURLStore) { + if (store.has(url)) { + return store.get(url); + } + + let uri = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL); + store.set(url, uri); + return uri; + } +}; + +for (let prop of Object.getOwnPropertyNames(NetworkHelper)) { + exports[prop] = NetworkHelper[prop]; +} diff --git a/devtools/shared/webconsole/network-monitor.js b/devtools/shared/webconsole/network-monitor.js new file mode 100644 index 000000000..084493432 --- /dev/null +++ b/devtools/shared/webconsole/network-monitor.js @@ -0,0 +1,2044 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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, Ci, Cm, Cu, Cr, components} = require("chrome"); +const Services = require("Services"); +const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm"); + +loader.lazyRequireGetter(this, "NetworkHelper", + "devtools/shared/webconsole/network-helper"); +loader.lazyRequireGetter(this, "DevToolsUtils", + "devtools/shared/DevToolsUtils"); +loader.lazyRequireGetter(this, "flags", + "devtools/shared/flags"); +loader.lazyRequireGetter(this, "DebuggerServer", + "devtools/server/main", true); +loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); +loader.lazyServiceGetter(this, "gActivityDistributor", + "@mozilla.org/network/http-activity-distributor;1", + "nsIHttpActivityDistributor"); +const {NetworkThrottleManager} = require("devtools/shared/webconsole/throttle"); + +// 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: 1 MB +const RESPONSE_BODY_LIMIT = 1048576; +// Exported for testing. +exports.RESPONSE_BODY_LIMIT = RESPONSE_BODY_LIMIT; + +/** + * Check if a given network request should be logged by a network monitor + * based on the specified filters. + * + * @param nsIHttpChannel channel + * Request to check. + * @param filters + * NetworkMonitor filters to match against. + * @return boolean + * True if the network request should be logged, false otherwise. + */ +function matchRequest(channel, filters) { + // Log everything if no filter is specified + if (!filters.outerWindowID && !filters.window && !filters.appId) { + return true; + } + + // Ignore requests from chrome or add-on code when we are monitoring + // content. + // TODO: one particular test (browser_styleeditor_fetch-from-cache.js) needs + // the flags.testing check. We will move to a better way to serve + // its needs in bug 1167188, where this check should be removed. + if (!flags.testing && channel.loadInfo && + channel.loadInfo.loadingDocument === null && + channel.loadInfo.loadingPrincipal === + Services.scriptSecurityManager.getSystemPrincipal()) { + return false; + } + + if (filters.window) { + // Since frames support, this.window may not be the top level content + // frame, so that we can't only compare with win.top. + let win = NetworkHelper.getWindowForRequest(channel); + while (win) { + if (win == filters.window) { + return true; + } + if (win.parent == win) { + break; + } + win = win.parent; + } + } + + if (filters.outerWindowID) { + let topFrame = NetworkHelper.getTopFrameForRequest(channel); + if (topFrame && topFrame.outerWindowID && + topFrame.outerWindowID == filters.outerWindowID) { + return true; + } + } + + if (filters.appId) { + let appId = NetworkHelper.getAppIdForRequest(channel); + if (appId && appId == filters.appId) { + return true; + } + } + + return false; +} + +/** + * This is a nsIChannelEventSink implementation that monitors channel redirects and + * informs the registered StackTraceCollector about the old and new channels. + */ +const SINK_CLASS_DESCRIPTION = "NetworkMonitor Channel Event Sink"; +const SINK_CLASS_ID = components.ID("{e89fa076-c845-48a8-8c45-2604729eba1d}"); +const SINK_CONTRACT_ID = "@mozilla.org/network/monitor/channeleventsink;1"; +const SINK_CATEGORY_NAME = "net-channel-event-sinks"; + +function ChannelEventSink() { + this.wrappedJSObject = this; + this.collectors = new Set(); +} + +ChannelEventSink.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink]), + + registerCollector(collector) { + this.collectors.add(collector); + }, + + unregisterCollector(collector) { + this.collectors.delete(collector); + + if (this.collectors.size == 0) { + ChannelEventSinkFactory.unregister(); + } + }, + + asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) { + for (let collector of this.collectors) { + try { + collector.onChannelRedirect(oldChannel, newChannel, flags); + } catch (ex) { + console.error("StackTraceCollector.onChannelRedirect threw an exception", ex); + } + } + callback.onRedirectVerifyCallback(Cr.NS_OK); + } +}; + +const ChannelEventSinkFactory = XPCOMUtils.generateSingletonFactory(ChannelEventSink); + +ChannelEventSinkFactory.register = function () { + const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + if (registrar.isCIDRegistered(SINK_CLASS_ID)) { + return; + } + + registrar.registerFactory(SINK_CLASS_ID, + SINK_CLASS_DESCRIPTION, + SINK_CONTRACT_ID, + ChannelEventSinkFactory); + + XPCOMUtils.categoryManager.addCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, + SINK_CONTRACT_ID, false, true); +}; + +ChannelEventSinkFactory.unregister = function () { + const registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); + registrar.unregisterFactory(SINK_CLASS_ID, ChannelEventSinkFactory); + + XPCOMUtils.categoryManager.deleteCategoryEntry(SINK_CATEGORY_NAME, SINK_CONTRACT_ID, + false); +}; + +ChannelEventSinkFactory.getService = function () { + // Make sure the ChannelEventSink service is registered before accessing it + ChannelEventSinkFactory.register(); + + return Cc[SINK_CONTRACT_ID].getService(Ci.nsIChannelEventSink).wrappedJSObject; +}; + +function StackTraceCollector(filters) { + this.filters = filters; + this.stacktracesById = new Map(); +} + +StackTraceCollector.prototype = { + init() { + Services.obs.addObserver(this, "http-on-opening-request", false); + ChannelEventSinkFactory.getService().registerCollector(this); + }, + + destroy() { + Services.obs.removeObserver(this, "http-on-opening-request"); + ChannelEventSinkFactory.getService().unregisterCollector(this); + }, + + _saveStackTrace(channel, stacktrace) { + this.stacktracesById.set(channel.channelId, stacktrace); + }, + + observe(subject) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + + if (!matchRequest(channel, this.filters)) { + return; + } + + // Convert the nsIStackFrame XPCOM objects to a nice JSON that can be + // passed around through message managers etc. + let frame = components.stack; + let stacktrace = []; + if (frame && frame.caller) { + frame = frame.caller; + while (frame) { + stacktrace.push({ + filename: frame.filename, + lineNumber: frame.lineNumber, + columnNumber: frame.columnNumber, + functionName: frame.name, + asyncCause: frame.asyncCause, + }); + frame = frame.caller || frame.asyncCaller; + } + } + + this._saveStackTrace(channel, stacktrace); + }, + + onChannelRedirect(oldChannel, newChannel, flags) { + // We can be called with any nsIChannel, but are interested only in HTTP channels + try { + oldChannel.QueryInterface(Ci.nsIHttpChannel); + newChannel.QueryInterface(Ci.nsIHttpChannel); + } catch (ex) { + return; + } + + let oldId = oldChannel.channelId; + let stacktrace = this.stacktracesById.get(oldId); + if (stacktrace) { + this.stacktracesById.delete(oldId); + this._saveStackTrace(newChannel, stacktrace); + } + }, + + getStackTrace(channelId) { + let trace = this.stacktracesById.get(channelId); + this.stacktracesById.delete(channelId); + return trace; + } +}; + +exports.StackTraceCollector = StackTraceCollector; + +/** + * 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 owner + * The response listener owner. This object needs to hold the + * |openResponses| object. + * @param object httpActivity + * HttpActivity object associated with this request. See NetworkMonitor + * for more information. + */ +function NetworkResponseListener(owner, httpActivity) { + this.owner = owner; + this.receivedData = ""; + this.httpActivity = httpActivity; + this.bodySize = 0; + // Note that this is really only needed for the non-e10s case. + // See bug 1309523. + let channel = this.httpActivity.channel; + this._wrappedNotificationCallbacks = channel.notificationCallbacks; + channel.notificationCallbacks = this; +} + +NetworkResponseListener.prototype = { + QueryInterface: + XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInputStreamCallback, + Ci.nsIRequestObserver, Ci.nsIInterfaceRequestor, + Ci.nsISupports]), + + // nsIInterfaceRequestor implementation + + /** + * This object implements nsIProgressEventSink, but also needs to forward + * interface requests to the notification callbacks of other objects. + */ + getInterface(iid) { + if (iid.equals(Ci.nsIProgressEventSink)) { + return this; + } + if (this._wrappedNotificationCallbacks) { + return this._wrappedNotificationCallbacks.getInterface(iid); + } + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + /** + * Forward notifications for interfaces this object implements, in case other + * objects also implemented them. + */ + _forwardNotification(iid, method, args) { + if (!this._wrappedNotificationCallbacks) { + return; + } + try { + let impl = this._wrappedNotificationCallbacks.getInterface(iid); + impl[method].apply(impl, args); + } catch (e) { + if (e.result != Cr.NS_ERROR_NO_INTERFACE) { + throw e; + } + } + }, + + /** + * This NetworkResponseListener tracks the NetworkMonitor.openResponses object + * to find the associated uncached headers. + * @private + */ + _foundOpenResponse: false, + + /** + * If the channel already had notificationCallbacks, hold them here internally + * so that we can forward getInterface requests to that object. + */ + _wrappedNotificationCallbacks: null, + + /** + * 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 uncompressed, decoded response body size. + */ + bodySize: null, + + /** + * Response body size on the wire, potentially compressed / encoded. + */ + transferredSize: 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 stream + * The input stream from where we are waiting for data to come in. + * @param nsIInputStreamCallback listener + * 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 (stream, listener) { + // Asynchronously wait for the stream to be readable or closed. + stream.asyncWait(listener, 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 request + * @param nsISupports context + * @param nsIInputStream inputStream + * @param unsigned long offset + * @param unsigned long count + */ + onDataAvailable: function (request, context, inputStream, offset, count) { + this._findOpenResponse(); + let data = NetUtil.readInputStreamToString(inputStream, count); + + this.bodySize += count; + + if (!this.httpActivity.discardResponseBody && + this.receivedData.length < RESPONSE_BODY_LIMIT) { + this.receivedData += + NetworkHelper.convertToUnicode(data, request.contentCharset); + } + }, + + /** + * See documentation at + * https://developer.mozilla.org/En/NsIRequestObserver + * + * @param nsIRequest request + * @param nsISupports context + */ + onStartRequest: function (request) { + // Converter will call this again, we should just ignore that. + if (this.request) { + return; + } + + this.request = request; + this._getSecurityInfo(); + this._findOpenResponse(); + // We need to track the offset for the onDataAvailable calls where + // we pass the data from our pipe to the converter. + this.offset = 0; + + // In the multi-process mode, the conversion happens on the child + // side while we can only monitor the channel on the parent + // side. If the content is gzipped, we have to unzip it + // ourself. For that we use the stream converter services. Do not + // do that for Service workers as they are run in the child + // process. + let channel = this.request; + if (!this.httpActivity.fromServiceWorker && + channel instanceof Ci.nsIEncodedChannel && + channel.contentEncodings && + !channel.applyConversion) { + let encodingHeader = channel.getResponseHeader("Content-Encoding"); + let scs = Cc["@mozilla.org/streamConverters;1"] + .getService(Ci.nsIStreamConverterService); + let encodings = encodingHeader.split(/\s*\t*,\s*\t*/); + let nextListener = this; + let acceptedEncodings = ["gzip", "deflate", "br", "x-gzip", "x-deflate"]; + for (let i in encodings) { + // There can be multiple conversions applied + let enc = encodings[i].toLowerCase(); + if (acceptedEncodings.indexOf(enc) > -1) { + this.converter = scs.asyncConvertData(enc, "uncompressed", + nextListener, null); + nextListener = this.converter; + } + } + if (this.converter) { + this.converter.onStartRequest(this.request, null); + } + } + // Asynchronously wait for the data coming from the request. + this.setAsyncListener(this.sink.inputStream, this); + }, + + /** + * Parse security state of this request and report it to the client. + */ + _getSecurityInfo: DevToolsUtils.makeInfallible(function () { + // Many properties of the securityInfo (e.g., the server certificate or HPKP + // status) are not available in the content process and can't be even touched safely, + // because their C++ getters trigger assertions. This function is called in content + // process for synthesized responses from service workers, in the parent otherwise. + if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) { + return; + } + + // Take the security information from the original nsIHTTPChannel instead of + // the nsIRequest received in onStartRequest. If response to this request + // was a redirect from http to https, the request object seems to contain + // security info for the https request after redirect. + let secinfo = this.httpActivity.channel.securityInfo; + let info = NetworkHelper.parseSecurityInfo(secinfo, this.httpActivity); + + this.httpActivity.owner.addSecurityInfo(info); + }), + + /** + * Handle the onStopRequest by closing the sink output stream. + * + * For more documentation about nsIRequestObserver go to: + * https://developer.mozilla.org/En/NsIRequestObserver + */ + onStopRequest: function () { + this._findOpenResponse(); + this.sink.outputStream.close(); + }, + + // nsIProgressEventSink implementation + + /** + * Handle progress event as data is transferred. This is used to record the + * size on the wire, which may be compressed / encoded. + */ + onProgress: function (request, context, progress, progressMax) { + this.transferredSize = progress; + // Need to forward as well to keep things like Download Manager's progress + // bar working properly. + this._forwardNotification(Ci.nsIProgressEventSink, "onProgress", arguments); + }, + + onStatus: function () { + this._forwardNotification(Ci.nsIProgressEventSink, "onStatus", arguments); + }, + + /** + * 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 () { + if (!this.owner || this._foundOpenResponse) { + return; + } + + let openResponse = null; + + for (let id in this.owner.openResponses) { + let item = this.owner.openResponses[id]; + 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 () { + 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 [data] + * Optional, the received data coming from the response listener or + * from the cache. + */ + _onComplete: function (data) { + let response = { + mimeType: "", + text: data || "", + }; + + response.size = this.bodySize; + response.transferredSize = this.transferredSize; + + try { + response.mimeType = this.request.contentType; + } catch (ex) { + // Ignore. + } + + if (!response.mimeType || + !NetworkHelper.isTextMimeType(response.mimeType)) { + response.encoding = "base64"; + try { + response.text = btoa(response.text); + } catch (err) { + // Ignore. + } + } + + if (response.mimeType && this.request.contentCharset) { + response.mimeType += "; charset=" + this.request.contentCharset; + } + + this.receivedData = ""; + + this.httpActivity.owner.addResponseContent( + response, + this.httpActivity.discardResponseBody + ); + + this._wrappedNotificationCallbacks = null; + this.httpActivity = null; + this.sink = null; + this.inputStream = null; + this.converter = 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 stream + * The sink input stream from which data is coming. + * @returns void + */ + onInputStreamReady: function (stream) { + if (!(stream 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 = stream.available(); + } catch (ex) { + // Ignore. + } + + if (available != -1) { + if (available != 0) { + if (this.converter) { + this.converter.onDataAvailable(this.request, null, stream, + this.offset, available); + } else { + this.onDataAvailable(this.request, null, stream, this.offset, + available); + } + } + this.offset += available; + this.setAsyncListener(stream, this); + } else { + this.onStreamClose(); + this.offset = 0; + } + }, +}; + +/** + * 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 object filters + * Object with the filters to use for network requests: + * - window (nsIDOMWindow): filter network requests by the associated + * window object. + * - appId (number): filter requests by the appId. + * - outerWindowID (number): filter requests by their top frame's outerWindowID. + * Filters are optional. If any of these filters match the request is + * logged (OR is applied). If no filter is provided then all requests are + * logged. + * @param object owner + * The network monitor owner. This object needs to hold: + * - onNetworkEvent(requestInfo) + * This method is invoked once for every new network request and it is + * given the initial network request information as an argument. + * onNetworkEvent() must return an object which holds several add*() + * methods which are used to add further network request/response information. + * - stackTraceCollector + * If the owner has this optional property, it will be used as a + * StackTraceCollector by the NetworkMonitor. + */ +function NetworkMonitor(filters, owner) { + this.filters = filters; + this.owner = owner; + this.openRequests = {}; + this.openResponses = {}; + this._httpResponseExaminer = + DevToolsUtils.makeInfallible(this._httpResponseExaminer).bind(this); + this._httpModifyExaminer = + DevToolsUtils.makeInfallible(this._httpModifyExaminer).bind(this); + this._serviceWorkerRequest = this._serviceWorkerRequest.bind(this); + this._throttleData = null; + this._throttler = null; +} + +exports.NetworkMonitor = NetworkMonitor; + +NetworkMonitor.prototype = { + filters: null, + + 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" + }, + + httpDownloadActivities: [ + gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_START, + gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER, + gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE, + gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE + ], + + // 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. + * @type boolean + */ + saveRequestAndResponseBodies: true, + + /** + * 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 () { + this.responsePipeSegmentSize = Services.prefs + .getIntPref("network.buffer.cache.size"); + this.interceptedChannels = new Set(); + + if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) { + gActivityDistributor.addObserver(this); + Services.obs.addObserver(this._httpResponseExaminer, + "http-on-examine-response", false); + Services.obs.addObserver(this._httpResponseExaminer, + "http-on-examine-cached-response", false); + Services.obs.addObserver(this._httpModifyExaminer, + "http-on-modify-request", false); + } + // In child processes, only watch for service worker requests + // everything else only happens in the parent process + Services.obs.addObserver(this._serviceWorkerRequest, + "service-worker-synthesized-response", false); + }, + + get throttleData() { + return this._throttleData; + }, + + set throttleData(value) { + this._throttleData = value; + // Clear out any existing throttlers + this._throttler = null; + }, + + _getThrottler: function () { + if (this.throttleData !== null && this._throttler === null) { + this._throttler = new NetworkThrottleManager(this.throttleData); + } + return this._throttler; + }, + + _serviceWorkerRequest: function (subject, topic, data) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + + if (!matchRequest(channel, this.filters)) { + return; + } + + this.interceptedChannels.add(subject); + + // On e10s, we never receive http-on-examine-cached-response, so fake one. + if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) { + this._httpResponseExaminer(channel, "http-on-examine-cached-response"); + } + }, + + /** + * Observe notifications for the http-on-examine-response topic, coming from + * the nsIObserverService. + * + * @private + * @param nsIHttpChannel subject + * @param string topic + * @returns void + */ + _httpResponseExaminer: function (subject, topic) { + // 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 || + (topic != "http-on-examine-response" && + topic != "http-on-examine-cached-response") || + !(subject instanceof Ci.nsIHttpChannel)) { + return; + } + + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + + if (!matchRequest(channel, this.filters)) { + return; + } + + let response = { + id: gSequenceId(), + channel: channel, + headers: [], + cookies: [], + }; + + let setCookieHeader = null; + + channel.visitResponseHeaders({ + visitHeader: function (name, value) { + let lowerName = name.toLowerCase(); + if (lowerName == "set-cookie") { + setCookieHeader = value; + } + response.headers.push({ name: name, value: value }); + } + }); + + if (!response.headers.length) { + // No need to continue. + return; + } + + 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; + + if (topic === "http-on-examine-cached-response") { + // Service worker requests emits cached-reponse notification on non-e10s, + // and we fake one on e10s. + let fromServiceWorker = this.interceptedChannels.has(channel); + this.interceptedChannels.delete(channel); + + // If this is a cached response, there never was a request event + // so we need to construct one here so the frontend gets all the + // expected events. + let httpActivity = this._createNetworkEvent(channel, { + fromCache: !fromServiceWorker, + fromServiceWorker: fromServiceWorker + }); + httpActivity.owner.addResponseStart({ + httpVersion: response.httpVersion, + remoteAddress: "", + remotePort: "", + status: response.status, + statusText: response.statusText, + headersSize: 0, + }, "", true); + + // There also is never any timing events, so we can fire this + // event with zeroed out values. + let timings = this._setupHarTimings(httpActivity, true); + httpActivity.owner.addEventTimings(timings.total, timings.timings); + } + }, + + /** + * Observe notifications for the http-on-modify-request topic, coming from + * the nsIObserverService. + * + * @private + * @param nsIHttpChannel aSubject + * @returns void + */ + _httpModifyExaminer: function (subject) { + let throttler = this._getThrottler(); + if (throttler) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + if (matchRequest(channel, this.filters)) { + // Read any request body here, before it is throttled. + let httpActivity = this.createOrGetActivityObject(channel); + this._onRequestBodySent(httpActivity); + throttler.manageUpload(channel); + } + } + }, + + /** + * A helper function for observeActivity. This does whatever work + * is required by a particular http activity event. Arguments are + * the same as for observeActivity. + */ + _dispatchActivity: function (httpActivity, channel, activityType, + activitySubtype, timestamp, extraSizeData, + extraStringData) { + let transCodes = this.httpTransactionCodes; + + // Store the time information for this activity subtype. + if (activitySubtype in transCodes) { + let stage = transCodes[activitySubtype]; + if (stage in httpActivity.timings) { + httpActivity.timings[stage].last = timestamp; + } else { + httpActivity.timings[stage] = { + first: timestamp, + last: timestamp, + }; + } + } + + switch (activitySubtype) { + case gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_BODY_SENT: + this._onRequestBodySent(httpActivity); + if (httpActivity.sentBody !== null) { + httpActivity.owner.addRequestPostData({ text: httpActivity.sentBody }); + httpActivity.sentBody = null; + } + break; + case gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER: + this._onResponseHeader(httpActivity, extraStringData); + break; + case gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE: + this._onTransactionClose(httpActivity); + break; + default: + break; + } + }, + + /** + * Begin observing HTTP traffic that originates inside the current tab. + * + * @see https://developer.mozilla.org/en/XPCOM_Interface_Reference/nsIHttpActivityObserver + * + * @param nsIHttpChannel channel + * @param number activityType + * @param number activitySubtype + * @param number timestamp + * @param number extraSizeData + * @param string extraStringData + */ + observeActivity: + DevToolsUtils.makeInfallible(function (channel, activityType, activitySubtype, + timestamp, extraSizeData, + extraStringData) { + if (!this.owner || + activityType != gActivityDistributor.ACTIVITY_TYPE_HTTP_TRANSACTION && + activityType != gActivityDistributor.ACTIVITY_TYPE_SOCKET_TRANSPORT) { + return; + } + + if (!(channel instanceof Ci.nsIHttpChannel)) { + return; + } + + channel = channel.QueryInterface(Ci.nsIHttpChannel); + + if (activitySubtype == + gActivityDistributor.ACTIVITY_SUBTYPE_REQUEST_HEADER) { + this._onRequestHeader(channel, timestamp, extraStringData); + return; + } + + // Iterate over all currently ongoing requests. If channel can't + // be found within them, then exit this function. + let httpActivity = this._findActivityObject(channel); + if (!httpActivity) { + return; + } + + // If we're throttling, we must not report events as they arrive + // from platform, but instead let the throttler emit the events + // after some time has elapsed. + if (httpActivity.downloadThrottle && + this.httpDownloadActivities.indexOf(activitySubtype) >= 0) { + let callback = this._dispatchActivity.bind(this); + httpActivity.downloadThrottle + .addActivityCallback(callback, httpActivity, channel, activityType, + activitySubtype, timestamp, extraSizeData, + extraStringData); + } else { + this._dispatchActivity(httpActivity, channel, activityType, + activitySubtype, timestamp, extraSizeData, + extraStringData); + } + }), + + /** + * + */ + _createNetworkEvent: function (channel, { timestamp, extraStringData, + fromCache, fromServiceWorker }) { + let httpActivity = this.createOrGetActivityObject(channel); + + channel.QueryInterface(Ci.nsIPrivateBrowsingChannel); + httpActivity.private = channel.isChannelPrivate; + + if (timestamp) { + httpActivity.timings.REQUEST_HEADER = { + first: timestamp, + last: timestamp + }; + } + + let event = {}; + event.method = channel.requestMethod; + event.channelId = channel.channelId; + event.url = channel.URI.spec; + event.private = httpActivity.private; + event.headersSize = 0; + event.startedDateTime = + (timestamp ? new Date(Math.round(timestamp / 1000)) : new Date()) + .toISOString(); + event.fromCache = fromCache; + event.fromServiceWorker = fromServiceWorker; + httpActivity.fromServiceWorker = fromServiceWorker; + + if (extraStringData) { + event.headersSize = extraStringData.length; + } + + // Determine the cause and if this is an XHR request. + let causeType = channel.loadInfo.externalContentPolicyType; + let loadingPrincipal = channel.loadInfo.loadingPrincipal; + let causeUri = loadingPrincipal ? loadingPrincipal.URI : null; + let stacktrace; + // If this is the parent process, there is no stackTraceCollector - the stack + // trace will be added in NetworkMonitorChild._onNewEvent. + if (this.owner.stackTraceCollector) { + stacktrace = this.owner.stackTraceCollector.getStackTrace(event.channelId); + } + + event.cause = { + type: causeType, + loadingDocumentUri: causeUri ? causeUri.spec : null, + stacktrace + }; + + httpActivity.isXHR = event.isXHR = + (causeType === Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST || + causeType === Ci.nsIContentPolicy.TYPE_FETCH); + + // Determine the HTTP version. + let httpVersionMaj = {}; + let httpVersionMin = {}; + channel.QueryInterface(Ci.nsIHttpChannelInternal); + channel.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. + channel.visitRequestHeaders({ + visitHeader: function (name, value) { + if (name == "Cookie") { + cookieHeader = value; + } + headers.push({ name: name, value: value }); + } + }); + + if (cookieHeader) { + cookies = NetworkHelper.parseCookieHeader(cookieHeader); + } + + httpActivity.owner = this.owner.onNetworkEvent(event); + + this._setupResponseListener(httpActivity, fromCache); + + httpActivity.owner.addRequestHeaders(headers, extraStringData); + httpActivity.owner.addRequestCookies(cookies); + + return httpActivity; + }, + + /** + * 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 channel + * @param number timestamp + * @param string extraStringData + * @return void + */ + _onRequestHeader: function (channel, timestamp, extraStringData) { + if (!matchRequest(channel, this.filters)) { + return; + } + + this._createNetworkEvent(channel, { timestamp, extraStringData }); + }, + + /** + * Find an HTTP activity object for the channel. + * + * @param nsIHttpChannel channel + * The HTTP channel whose activity object we want to find. + * @return object + * The HTTP activity object, or null if it is not found. + */ + _findActivityObject: function (channel) { + for (let id in this.openRequests) { + let item = this.openRequests[id]; + if (item.channel === channel) { + return item; + } + } + return null; + }, + + /** + * Find an existing HTTP activity object, or create a new one. 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. + * + * @see http://www.softwareishard.com/blog/har-12-spec + * @param nsIHttpChannel channel + * The HTTP channel for which the HTTP activity object is created. + * @return object + * The new HTTP activity object. + */ + createOrGetActivityObject: function (channel) { + let httpActivity = this._findActivityObject(channel); + if (!httpActivity) { + let win = NetworkHelper.getWindowForRequest(channel); + let charset = win ? win.document.characterSet : null; + + httpActivity = { + id: gSequenceId(), + channel: channel, + // see _onRequestBodySent() + charset: charset, + sentBody: null, + url: channel.URI.spec, + // needed for host specific security info + hostname: channel.URI.host, + discardRequestBody: !this.saveRequestAndResponseBodies, + discardResponseBody: !this.saveRequestAndResponseBodies, + // internal timing information, see observeActivity() + timings: {}, + // see _onResponseHeader() + responseStatus: null, + // the activity owner which is notified when changes happen + owner: null, + }; + + this.openRequests[httpActivity.id] = httpActivity; + } + + return httpActivity; + }, + + /** + * Setup the network response listener for the given HTTP activity. The + * NetworkResponseListener is responsible for storing the response body. + * + * @private + * @param object httpActivity + * The HTTP activity object we are tracking. + */ + _setupResponseListener: function (httpActivity, fromCache) { + let channel = httpActivity.channel; + channel.QueryInterface(Ci.nsITraceableChannel); + + if (!fromCache) { + let throttler = this._getThrottler(); + if (throttler) { + httpActivity.downloadThrottle = throttler.manage(channel); + } + } + + // 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, httpActivity); + + // 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 httpActivity + * The HTTP activity object we are working with. + */ + _onRequestBodySent: function (httpActivity) { + // Return early if we don't need the request body, or if we've + // already found it. + if (httpActivity.discardRequestBody || httpActivity.sentBody !== null) { + return; + } + + let sentBody = NetworkHelper.readPostTextFromRequest(httpActivity.channel, + httpActivity.charset); + + if (sentBody !== null && this.window && + httpActivity.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, httpActivity.charset); + } + + if (sentBody !== null) { + httpActivity.sentBody = sentBody; + } + }, + + /** + * Handler for ACTIVITY_SUBTYPE_RESPONSE_HEADER. This method stores + * information about the response headers. + * + * @private + * @param object httpActivity + * The HTTP activity object we are working with. + * @param string extraStringData + * The uncached response headers. + */ + _onResponseHeader: function (httpActivity, extraStringData) { + // extraStringData 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() method at this point sometimes causes an + // NS_ERROR_NOT_AVAILABLE exception. + // + // We could parse extraStringData 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 = extraStringData.split(/\r\n|\n|\r/); + let statusLine = headers.shift(); + let statusLineArray = statusLine.split(" "); + + let response = {}; + response.httpVersion = statusLineArray.shift(); + response.remoteAddress = httpActivity.channel.remoteAddress; + response.remotePort = httpActivity.channel.remotePort; + response.status = statusLineArray.shift(); + response.statusText = statusLineArray.join(" "); + response.headersSize = extraStringData.length; + + httpActivity.responseStatus = response.status; + + // Discard the response body for known response statuses. + switch (parseInt(response.status, 10)) { + case HTTP_MOVED_PERMANENTLY: + case HTTP_FOUND: + case HTTP_SEE_OTHER: + case HTTP_TEMPORARY_REDIRECT: + httpActivity.discardResponseBody = true; + break; + } + + response.discardResponseBody = httpActivity.discardResponseBody; + + httpActivity.owner.addResponseStart(response, extraStringData); + }, + + /** + * 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 httpActivity + * The HTTP activity object we work with. + */ + _onTransactionClose: function (httpActivity) { + let result = this._setupHarTimings(httpActivity); + httpActivity.owner.addEventTimings(result.total, result.timings); + delete this.openRequests[httpActivity.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 httpActivity + * The HTTP activity object we are working with. + * @param boolean fromCache + * Indicates that the result was returned from the browser cache + * @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 (httpActivity, fromCache) { + if (fromCache) { + // If it came from the browser cache, we have no timing + // information and these should all be 0 + return { + total: 0, + timings: { + blocked: 0, + dns: 0, + connect: 0, + send: 0, + wait: 0, + receive: 0 + } + }; + } + + let timings = httpActivity.timings; + let harTimings = {}; + + if (timings.STATUS_RESOLVING && timings.STATUS_CONNECTING_TO) { + harTimings.blocked = timings.STATUS_RESOLVING.first - + timings.REQUEST_HEADER.first; + } else if (timings.STATUS_SENDING_TO) { + harTimings.blocked = timings.STATUS_SENDING_TO.first - + timings.REQUEST_HEADER.first; + } else { + 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 { + harTimings.connect = -1; + } + + if (timings.STATUS_SENDING_TO) { + harTimings.send = timings.STATUS_SENDING_TO.last - timings.STATUS_SENDING_TO.first; + } else if (timings.REQUEST_HEADER && timings.REQUEST_BODY_SENT) { + harTimings.send = timings.REQUEST_BODY_SENT.last - timings.REQUEST_HEADER.first; + } 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 () { + if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT) { + gActivityDistributor.removeObserver(this); + Services.obs.removeObserver(this._httpResponseExaminer, + "http-on-examine-response"); + Services.obs.removeObserver(this._httpResponseExaminer, + "http-on-examine-cached-response"); + Services.obs.removeObserver(this._httpModifyExaminer, + "http-on-modify-request", false); + } + + Services.obs.removeObserver(this._serviceWorkerRequest, + "service-worker-synthesized-response"); + + this.interceptedChannels.clear(); + this.openRequests = {}; + this.openResponses = {}; + this.owner = null; + this.filters = null; + this._throttler = null; + }, +}; + +/** + * The NetworkMonitorChild is used to proxy all of the network activity of the + * child app process from the main process. The child WebConsoleActor creates an + * instance of this object. + * + * Network requests for apps happen in the main process. As such, + * a NetworkMonitor instance is used by the WebappsActor in the main process to + * log the network requests for this child process. + * + * The main process creates NetworkEventActorProxy instances per request. These + * send the data to this object using the nsIMessageManager. Here we proxy the + * data to the WebConsoleActor or to a NetworkEventActor. + * + * @constructor + * @param number appId + * The web appId of the child process. + * @param number outerWindowID + * The outerWindowID of the TabActor's main window. + * @param nsIMessageManager messageManager + * The nsIMessageManager to use to communicate with the parent process. + * @param object DebuggerServerConnection + * The RDP connection to the client. + * @param object owner + * The WebConsoleActor that is listening for the network requests. + */ +function NetworkMonitorChild(appId, outerWindowID, messageManager, conn, owner) { + this.appId = appId; + this.outerWindowID = outerWindowID; + this.conn = conn; + this.owner = owner; + this._messageManager = messageManager; + this._onNewEvent = this._onNewEvent.bind(this); + this._onUpdateEvent = this._onUpdateEvent.bind(this); + this._netEvents = new Map(); + this._msgName = `debug:${this.conn.prefix}netmonitor`; +} + +exports.NetworkMonitorChild = NetworkMonitorChild; + +NetworkMonitorChild.prototype = { + appId: null, + owner: null, + _netEvents: null, + _saveRequestAndResponseBodies: true, + _throttleData: null, + + get saveRequestAndResponseBodies() { + return this._saveRequestAndResponseBodies; + }, + + set saveRequestAndResponseBodies(val) { + this._saveRequestAndResponseBodies = val; + + this._messageManager.sendAsyncMessage(this._msgName, { + action: "setPreferences", + preferences: { + saveRequestAndResponseBodies: this._saveRequestAndResponseBodies, + }, + }); + }, + + get throttleData() { + return this._throttleData; + }, + + set throttleData(val) { + this._throttleData = val; + + this._messageManager.sendAsyncMessage(this._msgName, { + action: "setPreferences", + preferences: { + throttleData: this._throttleData, + }, + }); + }, + + init: function () { + this.conn.setupInParent({ + module: "devtools/shared/webconsole/network-monitor", + setupParent: "setupParentProcess" + }); + + let mm = this._messageManager; + mm.addMessageListener(`${this._msgName}:newEvent`, this._onNewEvent); + mm.addMessageListener(`${this._msgName}:updateEvent`, this._onUpdateEvent); + mm.sendAsyncMessage(this._msgName, { + appId: this.appId, + outerWindowID: this.outerWindowID, + action: "start", + }); + }, + + _onNewEvent: DevToolsUtils.makeInfallible(function _onNewEvent(msg) { + let {id, event} = msg.data; + + // Try to add stack trace to the event data received from parent + if (this.owner.stackTraceCollector) { + event.cause.stacktrace = + this.owner.stackTraceCollector.getStackTrace(event.channelId); + } + + let actor = this.owner.onNetworkEvent(event); + this._netEvents.set(id, Cu.getWeakReference(actor)); + }), + + _onUpdateEvent: DevToolsUtils.makeInfallible(function _onUpdateEvent(msg) { + let {id, method, args} = msg.data; + let weakActor = this._netEvents.get(id); + let actor = weakActor ? weakActor.get() : null; + if (!actor) { + console.error(`Received ${this._msgName}:updateEvent for unknown event ID: ${id}`); + return; + } + if (!(method in actor)) { + console.error(`Received ${this._msgName}:updateEvent unsupported ` + + `method: ${method}`); + return; + } + actor[method].apply(actor, args); + }), + + destroy: function () { + let mm = this._messageManager; + try { + mm.removeMessageListener(`${this._msgName}:newEvent`, this._onNewEvent); + mm.removeMessageListener(`${this._msgName}:updateEvent`, this._onUpdateEvent); + } catch (e) { + // On b2g, when registered to a new root docshell, + // all message manager functions throw when trying to call them during + // message-manager-disconnect event. + // As there is no attribute/method on message manager to know + // if they are still usable or not, we can only catch the exception... + } + this._netEvents.clear(); + this._messageManager = null; + this.conn = null; + this.owner = null; + }, +}; + +/** + * The NetworkEventActorProxy is used to send network request information from + * the main process to the child app process. One proxy is used per request. + * Similarly, one NetworkEventActor in the child app process is used per + * request. The client receives all network logs from the child actors. + * + * The child process has a NetworkMonitorChild instance that is listening for + * all network logging from the main process. The net monitor shim is used to + * proxy the data to the WebConsoleActor instance of the child process. + * + * @constructor + * @param nsIMessageManager messageManager + * The message manager for the child app process. This is used for + * communication with the NetworkMonitorChild instance of the process. + * @param string msgName + * The message name to be used for this connection. + */ +function NetworkEventActorProxy(messageManager, msgName) { + this.id = gSequenceId(); + this.messageManager = messageManager; + this._msgName = msgName; +} +exports.NetworkEventActorProxy = NetworkEventActorProxy; + +NetworkEventActorProxy.methodFactory = function (method) { + return DevToolsUtils.makeInfallible(function () { + let args = Array.slice(arguments); + let mm = this.messageManager; + mm.sendAsyncMessage(`${this._msgName}:updateEvent`, { + id: this.id, + method: method, + args: args, + }); + }, "NetworkEventActorProxy." + method); +}; + +NetworkEventActorProxy.prototype = { + /** + * Initialize the network event. This method sends the network request event + * to the content process. + * + * @param object event + * Object describing the network request. + * @return object + * This object. + */ + init: DevToolsUtils.makeInfallible(function (event) { + let mm = this.messageManager; + mm.sendAsyncMessage(`${this._msgName}:newEvent`, { + id: this.id, + event: event, + }); + return this; + }), +}; + +(function () { + // Listeners for new network event data coming from the NetworkMonitor. + let methods = ["addRequestHeaders", "addRequestCookies", "addRequestPostData", + "addResponseStart", "addSecurityInfo", "addResponseHeaders", + "addResponseCookies", "addResponseContent", "addEventTimings"]; + let factory = NetworkEventActorProxy.methodFactory; + for (let method of methods) { + NetworkEventActorProxy.prototype[method] = factory(method); + } +})(); + +/** + * This is triggered by the child calling `setupInParent` when the child's network monitor + * is starting up. This initializes the parent process side of the monitoring. + */ +function setupParentProcess({ mm, prefix }) { + let networkMonitor = new NetworkMonitorParent(mm, prefix); + return { + onBrowserSwap: newMM => networkMonitor.setMessageManager(newMM), + onDisconnected: () => { + networkMonitor.destroy(); + networkMonitor = null; + } + }; +} + +exports.setupParentProcess = setupParentProcess; + +/** + * The NetworkMonitorParent runs in the parent process and uses the message manager to + * listen for requests from the child process to start/stop the network monitor. Most + * request data is only available from the parent process, so that's why the network + * monitor needs to run there when debugging tabs that are in the child. + * + * @param nsIMessageManager mm + * The message manager for the browser we're filtering on. + * @param string prefix + * The RDP connection prefix that uniquely identifies the connection. + */ +function NetworkMonitorParent(mm, prefix) { + this._msgName = `debug:${prefix}netmonitor`; + this.onNetMonitorMessage = this.onNetMonitorMessage.bind(this); + this.onNetworkEvent = this.onNetworkEvent.bind(this); + this.setMessageManager(mm); +} + +NetworkMonitorParent.prototype = { + netMonitor: null, + messageManager: null, + + setMessageManager(mm) { + if (this.messageManager) { + let oldMM = this.messageManager; + oldMM.removeMessageListener(this._msgName, this.onNetMonitorMessage); + } + this.messageManager = mm; + if (mm) { + mm.addMessageListener(this._msgName, this.onNetMonitorMessage); + } + }, + + /** + * Handler for `debug:${prefix}netmonitor` messages received through the message manager + * from the content process. + * + * @param object msg + * Message from the content. + */ + onNetMonitorMessage: DevToolsUtils.makeInfallible(function (msg) { + let {action} = msg.json; + // Pipe network monitor data from parent to child via the message manager. + switch (action) { + case "start": + if (!this.netMonitor) { + let {appId, outerWindowID} = msg.json; + this.netMonitor = new NetworkMonitor({ + outerWindowID, + appId, + }, this); + this.netMonitor.init(); + } + break; + case "setPreferences": { + let {preferences} = msg.json; + for (let key of Object.keys(preferences)) { + if ((key == "saveRequestAndResponseBodies" || + key == "throttleData") && this.netMonitor) { + this.netMonitor[key] = preferences[key]; + } + } + break; + } + + case "stop": + if (this.netMonitor) { + this.netMonitor.destroy(); + this.netMonitor = null; + } + break; + + case "disconnect": + this.destroy(); + break; + } + }), + + /** + * Handler for new network requests. This method is invoked by the current + * NetworkMonitor instance. + * + * @param object event + * Object describing the network request. + * @return object + * A NetworkEventActorProxy instance which is notified when further + * data about the request is available. + */ + onNetworkEvent: DevToolsUtils.makeInfallible(function (event) { + return new NetworkEventActorProxy(this.messageManager, this._msgName).init(event); + }), + + destroy: function () { + this.setMessageManager(null); + + if (this.netMonitor) { + this.netMonitor.destroy(); + this.netMonitor = null; + } + }, +}; + +/** + * 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 window + * The window for which we need to track location changes. + * @param object owner + * The listener owner which needs to implement two methods: + * - onFileActivity(aFileURI) + * - onLocationChange(aState, aTabURI, aPageTitle) + */ +function ConsoleProgressListener(window, owner) { + this.window = window; + this.owner = owner; +} +exports.ConsoleProgressListener = ConsoleProgressListener; + +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 () { + 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 monitor + * 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 (monitor) { + switch (monitor) { + 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 " + + monitor + "!"); + } + this._init(); + }, + + /** + * Stop a monitor. + * + * @param number monitor + * Tells what you want to stop tracking. See this.startMonitor() for + * the list of constants. + */ + stopMonitor: function (monitor) { + switch (monitor) { + 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 " + + monitor + "!"); + } + + if (!this._fileActivity && !this._locationChange) { + this.destroy(); + } + }, + + onStateChange: function (progress, request, state, status) { + if (!this.owner) { + return; + } + + if (this._fileActivity) { + this._checkFileActivity(progress, request, state, status); + } + + if (this._locationChange) { + this._checkLocationChange(progress, request, state, status); + } + }, + + /** + * 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 (progress, request, state, status) { + if (!(state & Ci.nsIWebProgressListener.STATE_START)) { + return; + } + + let uri = null; + if (request instanceof Ci.imgIRequest) { + let imgIRequest = request.QueryInterface(Ci.imgIRequest); + uri = imgIRequest.URI; + } else if (request instanceof Ci.nsIChannel) { + let nsIChannel = request.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 (progress, request, state) { + let isStart = state & Ci.nsIWebProgressListener.STATE_START; + let isStop = state & Ci.nsIWebProgressListener.STATE_STOP; + let isNetwork = state & Ci.nsIWebProgressListener.STATE_IS_NETWORK; + let isWindow = state & Ci.nsIWebProgressListener.STATE_IS_WINDOW; + + // Skip non-interesting states. + if (!isNetwork || !isWindow || progress.DOMWindow != this.window) { + return; + } + + if (isStart && request instanceof Ci.nsIChannel) { + this.owner.onLocationChange("start", request.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 () { + 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 = 1; diff --git a/devtools/shared/webconsole/server-logger-monitor.js b/devtools/shared/webconsole/server-logger-monitor.js new file mode 100644 index 000000000..9cc2682ea --- /dev/null +++ b/devtools/shared/webconsole/server-logger-monitor.js @@ -0,0 +1,191 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 {Ci} = require("chrome"); +const Services = require("Services"); + +const {makeInfallible} = require("devtools/shared/DevToolsUtils"); + +loader.lazyGetter(this, "NetworkHelper", () => require("devtools/shared/webconsole/network-helper")); + +// Helper tracer. Should be generic sharable by other modules (bug 1171927) +const trace = { + log: function (...args) { + } +}; + +const acceptableHeaders = ["x-chromelogger-data"]; + +/** + * This object represents HTTP events observer. It's intended to be + * used in e10s enabled browser only. + * + * Since child processes can't register HTTP event observer they use + * this module to do the observing in the parent process. This monitor + * is loaded through DebuggerServerConnection.setupInParent() that is + * executed from within the child process. The execution is done by + * {@ServerLoggingListener}. The monitor listens to HTTP events and + * forwards it into the right child process. + * + * Read more about the architecture: + * https://github.com/mozilla/gecko-dev/blob/fx-team/devtools/server/docs/actor-e10s-handling.md + */ +var ServerLoggerMonitor = { + // Initialization + + initialize: function () { + this.onChildMessage = this.onChildMessage.bind(this); + this.onExamineResponse = this.onExamineResponse.bind(this); + + // Set of registered child frames (loggers). + this.targets = new Set(); + }, + + // Parent Child Relationship + + attach: makeInfallible(function ({ mm, prefix }) { + trace.log("ServerLoggerMonitor.attach; ", arguments); + + let setMessageManager = newMM => { + if (mm) { + mm.removeMessageListener("debug:server-logger", this.onChildMessage); + } + mm = newMM; + if (mm) { + mm.addMessageListener("debug:server-logger", this.onChildMessage); + } + }; + + // Start listening for messages from the {@ServerLogger} actor + // living in the child process. + setMessageManager(mm); + + return { + onBrowserSwap: setMessageManager, + onDisconnected: () => { + trace.log("ServerLoggerMonitor.onDisconnectChild; ", arguments); + setMessageManager(null); + } + }; + }), + + // Child Message Handling + + onChildMessage: function (msg) { + let method = msg.data.method; + + trace.log("ServerLoggerMonitor.onChildMessage; ", method, msg); + + switch (method) { + case "attachChild": + return this.onAttachChild(msg); + case "detachChild": + return this.onDetachChild(msg); + default: + trace.log("Unknown method name: ", method); + return undefined; + } + }, + + onAttachChild: function (event) { + let target = event.target; + let size = this.targets.size; + + trace.log("ServerLoggerMonitor.onAttachChild; size: ", size, target); + + // If this is the first child attached, register global HTTP observer. + if (!size) { + trace.log("ServerLoggerMonitor.onAttatchChild; Add HTTP Observer"); + Services.obs.addObserver(this.onExamineResponse, + "http-on-examine-response", false); + } + + // Collect child loggers. The frame element where the + // window/document lives. + this.targets.add(target); + }, + + onDetachChild: function (event) { + let target = event.target; + this.targets.delete(target); + + let size = this.targets.size; + trace.log("ServerLoggerMonitor.onDetachChild; size: ", size, target); + + // If this is the last child process attached, unregister + // the global HTTP observer. + if (!size) { + trace.log("ServerLoggerMonitor.onDetachChild; Remove HTTP Observer"); + Services.obs.removeObserver(this.onExamineResponse, + "http-on-examine-response", false); + } + }, + + // HTTP Observer + + onExamineResponse: makeInfallible(function (subject, topic) { + let httpChannel = subject.QueryInterface(Ci.nsIHttpChannel); + + trace.log("ServerLoggerMonitor.onExamineResponse; ", httpChannel.name, + this.targets); + + // Ignore requests from chrome or add-on code when we are monitoring + // content. + if (!httpChannel.loadInfo && + httpChannel.loadInfo.loadingDocument === null && + httpChannel.loadInfo.loadingPrincipal === + Services.scriptSecurityManager.getSystemPrincipal()) { + return; + } + + let requestFrame = NetworkHelper.getTopFrameForRequest(httpChannel); + if (!requestFrame) { + return; + } + + // Ignore requests from parent frames that aren't registered. + if (!this.targets.has(requestFrame)) { + return; + } + + let headers = []; + + httpChannel.visitResponseHeaders((header, value) => { + header = header.toLowerCase(); + if (acceptableHeaders.indexOf(header) !== -1) { + headers.push({header: header, value: value}); + } + }); + + if (!headers.length) { + return; + } + + let { messageManager } = requestFrame; + messageManager.sendAsyncMessage("debug:server-logger", { + method: "examineHeaders", + headers: headers, + }); + + trace.log("ServerLoggerMonitor.onExamineResponse; headers ", + headers.length, ", ", headers); + }), +}; + +/** + * Executed automatically by the framework. + */ +function setupParentProcess(event) { + return ServerLoggerMonitor.attach(event); +} + +// Monitor initialization. +ServerLoggerMonitor.initialize(); + +// Exports from this module +exports.setupParentProcess = setupParentProcess; diff --git a/devtools/shared/webconsole/server-logger.js b/devtools/shared/webconsole/server-logger.js new file mode 100644 index 000000000..58a2f216a --- /dev/null +++ b/devtools/shared/webconsole/server-logger.js @@ -0,0 +1,514 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 {Ci} = require("chrome"); +const {Class} = require("sdk/core/heritage"); +const Services = require("Services"); + +const {DebuggerServer} = require("devtools/server/main"); +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); + +loader.lazyGetter(this, "NetworkHelper", () => require("devtools/shared/webconsole/network-helper")); + +// Helper tracer. Should be generic sharable by other modules (bug 1171927) +const trace = { + log: function () { + } +}; + +// Constants +const makeInfallible = DevToolsUtils.makeInfallible; +const acceptableHeaders = ["x-chromelogger-data"]; + +/** + * The listener is responsible for detecting server side logs + * within HTTP headers and sending them to the client. + * + * The logic is based on "http-on-examine-response" event that is + * sent when a response from the server is received. Consequently HTTP + * headers are parsed to find server side logs. + * + * A listeners for "http-on-examine-response" is registered when + * the listener starts and removed when destroy is executed. + */ +var ServerLoggingListener = Class({ + /** + * Initialization of the listener. The main step during the initialization + * process is registering a listener for "http-on-examine-response" event. + * + * @param {Object} win (nsIDOMWindow): + * filter network requests by the associated window object. + * If null (i.e. in the browser context) log everything + * @param {Object} owner + * The {@WebConsoleActor} instance + */ + initialize: function (win, owner) { + trace.log("ServerLoggingListener.initialize; ", owner.actorID, + ", child process: ", DebuggerServer.isInChildProcess); + + this.owner = owner; + this.window = win; + + this.onExamineResponse = this.onExamineResponse.bind(this); + this.onExamineHeaders = this.onExamineHeaders.bind(this); + this.onParentMessage = this.onParentMessage.bind(this); + + this.attach(); + }, + + /** + * The destroy is called by the parent WebConsoleActor actor. + */ + destroy: function () { + trace.log("ServerLoggingListener.destroy; ", this.owner.actorID, + ", child process: ", DebuggerServer.isInChildProcess); + + this.detach(); + }, + + /** + * The main responsibility of this method is registering a listener for + * "http-on-examine-response" events. + */ + attach: makeInfallible(function () { + trace.log("ServerLoggingListener.attach; child process: ", + DebuggerServer.isInChildProcess); + + // Setup the child <-> parent communication if this actor module + // is running in a child process. If e10s is disabled (this actor + // running in the same process as everything else) register observer + // listener just like in good old pre e10s days. + if (DebuggerServer.isInChildProcess) { + this.attachParentProcess(); + } else { + Services.obs.addObserver(this.onExamineResponse, + "http-on-examine-response", false); + } + }), + + /** + * Remove the "http-on-examine-response" listener. + */ + detach: makeInfallible(function () { + trace.log("ServerLoggingListener.detach; ", this.owner.actorID); + + if (DebuggerServer.isInChildProcess) { + this.detachParentProcess(); + } else { + Services.obs.removeObserver(this.onExamineResponse, + "http-on-examine-response", false); + } + }), + + // Parent Child Relationship + + attachParentProcess: function () { + trace.log("ServerLoggingListener.attachParentProcess;"); + + this.owner.conn.setupInParent({ + module: "devtools/shared/webconsole/server-logger-monitor", + setupParent: "setupParentProcess" + }); + + let mm = this.owner.conn.parentMessageManager; + let { addMessageListener, sendSyncMessage } = mm; + + // It isn't possible to register HTTP-* event observer inside + // a child process (in case of e10s), so listen for messages + // coming from the {@ServerLoggerMonitor} that lives inside + // the parent process. + addMessageListener("debug:server-logger", this.onParentMessage); + + // Attach to the {@ServerLoggerMonitor} object to subscribe events. + sendSyncMessage("debug:server-logger", { + method: "attachChild" + }); + }, + + detachParentProcess: makeInfallible(function () { + trace.log("ServerLoggingListener.detachParentProcess;"); + + let mm = this.owner.conn.parentMessageManager; + let { removeMessageListener, sendSyncMessage } = mm; + + sendSyncMessage("debug:server-logger", { + method: "detachChild", + }); + + removeMessageListener("debug:server-logger", this.onParentMessage); + }), + + onParentMessage: makeInfallible(function (msg) { + if (!msg.data) { + return; + } + + let method = msg.data.method; + trace.log("ServerLogger.onParentMessage; ", method, msg.data); + + switch (method) { + case "examineHeaders": + this.onExamineHeaders(msg); + break; + default: + trace.log("Unknown method name: ", method); + } + }), + + // HTTP Observer + + onExamineHeaders: function (event) { + let headers = event.data.headers; + + trace.log("ServerLoggingListener.onExamineHeaders;", headers); + + let parsedMessages = []; + + for (let item of headers) { + let header = item.header; + let value = item.value; + + let messages = this.parse(header, value); + if (messages) { + parsedMessages.push(...messages); + } + } + + if (!parsedMessages.length) { + return; + } + + for (let message of parsedMessages) { + this.sendMessage(message); + } + }, + + onExamineResponse: makeInfallible(function (subject) { + let httpChannel = subject.QueryInterface(Ci.nsIHttpChannel); + + trace.log("ServerLoggingListener.onExamineResponse; ", httpChannel.name, + ", ", this.owner.actorID, httpChannel); + + if (!this._matchRequest(httpChannel)) { + trace.log("ServerLoggerMonitor.onExamineResponse; No matching request!"); + return; + } + + let headers = []; + + httpChannel.visitResponseHeaders((header, value) => { + header = header.toLowerCase(); + if (acceptableHeaders.indexOf(header) !== -1) { + headers.push({header: header, value: value}); + } + }); + + this.onExamineHeaders({ + data: { + headers: headers, + } + }); + }), + + /** + * Check if a given network request should be logged by this network monitor + * instance based on the current filters. + * + * @private + * @param nsIHttpChannel channel + * Request to check. + * @return boolean + * True if the network request should be logged, false otherwise. + */ + _matchRequest: function (channel) { + trace.log("_matchRequest ", this.window, ", ", this.topFrame); + + // Log everything if the window is null (it's null in the browser context) + if (!this.window) { + return true; + } + + // Ignore requests from chrome or add-on code when we are monitoring + // content. + if (!channel.loadInfo && + channel.loadInfo.loadingDocument === null && + channel.loadInfo.loadingPrincipal === + Services.scriptSecurityManager.getSystemPrincipal()) { + return false; + } + + // Since frames support, this.window may not be the top level content + // frame, so that we can't only compare with win.top. + let win = NetworkHelper.getWindowForRequest(channel); + while (win) { + if (win == this.window) { + return true; + } + if (win.parent == win) { + break; + } + win = win.parent; + } + + return false; + }, + + // Server Logs + + /** + * Search through HTTP headers to catch all server side logs. + * Learn more about the data structure: + * https://craig.is/writing/chrome-logger/techspecs + */ + parse: function (header, value) { + let data; + + try { + let result = decodeURIComponent(escape(atob(value))); + data = JSON.parse(result); + } catch (err) { + console.error("Failed to parse HTTP log data! " + err); + return null; + } + + let parsedMessage = []; + let columnMap = this.getColumnMap(data); + + trace.log("ServerLoggingListener.parse; ColumnMap", columnMap); + trace.log("ServerLoggingListener.parse; data", data); + + let lastLocation; + + for (let row of data.rows) { + let backtrace = row[columnMap.get("backtrace")]; + let rawLogs = row[columnMap.get("log")]; + let type = row[columnMap.get("type")] || "log"; + + // Old version of the protocol includes a label. + // If this is the old version do some converting. + if (data.columns.indexOf("label") != -1) { + let label = row[columnMap.get("label")]; + let showLabel = label && typeof label === "string"; + + rawLogs = [rawLogs]; + + if (showLabel) { + rawLogs.unshift(label); + } + } + + // If multiple logs come from the same line only the first log + // has info about the backtrace. So, remember the last valid + // location and use it for those that not set. + let location = this.parseBacktrace(backtrace); + if (location) { + lastLocation = location; + } else { + location = lastLocation; + } + + parsedMessage.push({ + logs: rawLogs, + location: location, + type: type + }); + } + + return parsedMessage; + }, + + parseBacktrace: function (backtrace) { + if (!backtrace) { + return null; + } + + let result = backtrace.match(/\s*(\d+)$/); + if (!result || result.length < 2) { + return backtrace; + } + + return { + url: backtrace.slice(0, -result[0].length), + line: result[1] + }; + }, + + getColumnMap: function (data) { + let columnMap = new Map(); + let columnName; + + for (let key in data.columns) { + columnName = data.columns[key]; + columnMap.set(columnName, key); + } + + return columnMap; + }, + + sendMessage: function (msg) { + trace.log("ServerLoggingListener.sendMessage; message", msg); + + let formatted = format(msg); + trace.log("ServerLoggingListener.sendMessage; formatted", formatted); + + let win = this.window; + let innerID = win ? getInnerId(win) : null; + let location = msg.location; + + let message = { + category: "server", + innerID: innerID, + level: msg.type, + filename: location ? location.url : null, + lineNumber: location ? location.line : null, + columnNumber: 0, + private: false, + timeStamp: Date.now(), + arguments: formatted ? formatted.logs : null, + styles: formatted ? formatted.styles : null, + }; + + // Make sure to set the group name. + if (msg.type == "group" && formatted && formatted.logs) { + message.groupName = formatted ? formatted.logs[0] : null; + } + + // A message for console.table() method (passed in as the first + // argument) isn't supported. But, it's passed in by some server + // side libraries that implement console.* API - let's just remove it. + let args = message.arguments; + if (msg.type == "table" && args) { + if (typeof args[0] == "string") { + args.shift(); + } + } + + trace.log("ServerLoggingListener.sendMessage; raw: ", + msg.logs.join(", "), message); + + this.owner.onServerLogCall(message); + }, +}); + +// Helpers + +/** + * Parse printf-like specifiers ("%f", "%d", ...) and + * format the logs according to them. + */ +function format(msg) { + if (!msg.logs || !msg.logs[0]) { + return null; + } + + // Initialize the styles array (used for the "%c" specifier). + msg.styles = []; + + // Remove and get the first log (in which the specifiers are). + // Note that the first string doesn't have to be specified. + // An example of a log on the server side: + // ChromePhp::log("server info: ", $_SERVER); + // ChromePhp::log($_SERVER); + let firstString = ""; + if (typeof msg.logs[0] == "string") { + firstString = msg.logs.shift(); + } + + // All the specifiers present in the first string. + let splitLogRegExp = /(.*?)(%[oOcsdif]|$)/g; + let splitLogRegExpRes; + let concatString = ""; + let pushConcatString = () => { + if (concatString) { + rebuiltLogArray.push(concatString); + } + concatString = ""; + }; + + // This array represents the string of the log, in which the specifiers + // are replaced. It alternates strings and objects (%o;%O). + let rebuiltLogArray = []; + + // Get the strings before the specifiers (or the last chunk before the end + // of the string). + while ((splitLogRegExpRes = splitLogRegExp.exec(firstString)) !== null) { + let [, log, specifier] = splitLogRegExpRes; + + // We may start with a specifier or add consecutively several ones. In such + // a case, there is no log. + // Example: "%ctest" => first iteration: log = "", specifier = "%c". + // => second iteration: log = "test", specifier = "". + if (log) { + concatString += log; + } + + // Break now if there is no specifier anymore + // (means that we have reached the end of the string). + if (!specifier) { + break; + } + + let argument = msg.logs.shift(); + switch (specifier) { + case "%i": + case "%d": + // Parse into integer. + concatString += (argument | 0); + break; + case "%f": + // Parse into float. + concatString += (+argument); + break; + case "%o": + case "%O": + // Push the concatenated string and reinitialize concatString. + pushConcatString(); + // Push the object. + rebuiltLogArray.push(argument); + break; + case "%s": + concatString += argument; + break; + case "%c": + pushConcatString(); + let fillNullArrayLength = rebuiltLogArray.length - msg.styles.length; + let fillNullArray = Array(fillNullArrayLength).fill(null); + msg.styles.push(...fillNullArray, argument); + break; + } + } + + if (concatString) { + rebuiltLogArray.push(concatString); + } + + // Append the rest of arguments that don't have corresponding + // specifiers to the message logs. + msg.logs.unshift(...rebuiltLogArray); + + // Remove special ___class_name property that isn't supported + // by the current implementation. This property represents object class + // allowing custom rendering in the console panel. + for (let log of msg.logs) { + if (typeof log == "object") { + delete log.___class_name; + } + } + + return msg; +} + +// These helper are cloned from SDK to avoid loading to +// much SDK modules just because of two functions. +function getInnerId(win) { + return win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; +} + +// Exports from this module +exports.ServerLoggingListener = ServerLoggingListener; diff --git a/devtools/shared/webconsole/test/chrome.ini b/devtools/shared/webconsole/test/chrome.ini new file mode 100644 index 000000000..ae867b821 --- /dev/null +++ b/devtools/shared/webconsole/test/chrome.ini @@ -0,0 +1,41 @@ +[DEFAULT] +tags = devtools +support-files = + common.js + data.json + data.json^headers^ + helper_serviceworker.js + network_requests_iframe.html + sandboxed_iframe.html + console-test-worker.js + !/browser/base/content/test/general/browser_star_hsts.sjs + !/browser/base/content/test/general/pinning_headers.sjs + +[test_basics.html] +[test_bug819670_getter_throws.html] +[test_cached_messages.html] +[test_commands_other.html] +[test_commands_registration.html] +[test_consoleapi.html] +[test_consoleapi_innerID.html] +[test_console_serviceworker.html] +[test_console_serviceworker_cached.html] +[test_console_styling.html] +[test_file_uri.html] +[test_reflow.html] +[test_jsterm.html] +[test_jsterm_autocomplete.html] +[test_jsterm_cd_iframe.html] +[test_jsterm_last_result.html] +[test_jsterm_queryselector.html] +[test_network_get.html] +[test_network_longstring.html] +[test_network_post.html] +[test_network_security-hpkp.html] +[test_network_security-hsts.html] +[test_nsiconsolemessage.html] +[test_object_actor.html] +[test_object_actor_native_getters.html] +[test_object_actor_native_getters_lenient_this.html] +[test_page_errors.html] +[test_throw.html] diff --git a/devtools/shared/webconsole/test/common.js b/devtools/shared/webconsole/test/common.js new file mode 100644 index 000000000..b6649bc44 --- /dev/null +++ b/devtools/shared/webconsole/test/common.js @@ -0,0 +1,345 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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"; + +var {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +const XHTML_NS = "http://www.w3.org/1999/xhtml"; + +// This gives logging to stdout for tests +var {console} = Cu.import("resource://gre/modules/Console.jsm", {}); + +var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +var Services = require("Services"); +var WebConsoleUtils = require("devtools/client/webconsole/utils").Utils; +var {Task} = require("devtools/shared/task"); + +var ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"] + .getService(Ci.nsIConsoleAPIStorage); +var {DebuggerServer} = require("devtools/server/main"); +var {DebuggerClient, ObjectClient} = require("devtools/shared/client/main"); + +var {ConsoleServiceListener, ConsoleAPIListener} = + require("devtools/server/actors/utils/webconsole-utils"); + +function initCommon() +{ + // Services.prefs.setBoolPref("devtools.debugger.log", true); +} + +function initDebuggerServer() +{ + if (!DebuggerServer.initialized) { + DebuggerServer.init(); + DebuggerServer.addBrowserActors(); + } + DebuggerServer.allowChromeProcess = true; +} + +function connectToDebugger(aCallback) +{ + initCommon(); + initDebuggerServer(); + + let transport = DebuggerServer.connectPipe(); + let client = new DebuggerClient(transport); + + let dbgState = { dbgClient: client }; + client.connect().then(response => aCallback(dbgState, response)); +} + +function attachConsole(aListeners, aCallback) { + _attachConsole(aListeners, aCallback); +} +function attachConsoleToTab(aListeners, aCallback) { + _attachConsole(aListeners, aCallback, true); +} +function attachConsoleToWorker(aListeners, aCallback) { + _attachConsole(aListeners, aCallback, true, true); +} + +function _attachConsole(aListeners, aCallback, aAttachToTab, aAttachToWorker) +{ + function _onAttachConsole(aState, aResponse, aWebConsoleClient) + { + if (aResponse.error) { + console.error("attachConsole failed: " + aResponse.error + " " + + aResponse.message); + } + + aState.client = aWebConsoleClient; + + aCallback(aState, aResponse); + } + + connectToDebugger(function _onConnect(aState, aResponse) { + if (aResponse.error) { + console.error("client.connect() failed: " + aResponse.error + " " + + aResponse.message); + aCallback(aState, aResponse); + return; + } + + if (aAttachToTab) { + aState.dbgClient.listTabs(function _onListTabs(aResponse) { + if (aResponse.error) { + console.error("listTabs failed: " + aResponse.error + " " + + aResponse.message); + aCallback(aState, aResponse); + return; + } + let tab = aResponse.tabs[aResponse.selected]; + aState.dbgClient.attachTab(tab.actor, function (response, tabClient) { + if (aAttachToWorker) { + let workerName = "console-test-worker.js#" + new Date().getTime(); + var worker = new Worker(workerName); + // Keep a strong reference to the Worker to avoid it being + // GCd during the test (bug 1237492). + aState._worker_ref = worker; + worker.addEventListener("message", function listener() { + worker.removeEventListener("message", listener); + tabClient.listWorkers(function (response) { + let worker = response.workers.filter(w => w.url == workerName)[0]; + if (!worker) { + console.error("listWorkers failed. Unable to find the " + + "worker actor\n"); + return; + } + tabClient.attachWorker(worker.actor, function (response, workerClient) { + if (!workerClient || response.error) { + console.error("attachWorker failed. No worker client or " + + " error: " + response.error); + return; + } + workerClient.attachThread({}, function (aResponse) { + aState.actor = workerClient.consoleActor; + aState.dbgClient.attachConsole(workerClient.consoleActor, aListeners, + _onAttachConsole.bind(null, aState)); + }); + }); + }); + }); + } else { + aState.actor = tab.consoleActor; + aState.dbgClient.attachConsole(tab.consoleActor, aListeners, + _onAttachConsole.bind(null, aState)); + } + }); + }); + } else { + aState.dbgClient.getProcess().then(response => { + aState.dbgClient.attachTab(response.form.actor, function () { + let consoleActor = response.form.consoleActor; + aState.actor = consoleActor; + aState.dbgClient.attachConsole(consoleActor, aListeners, + _onAttachConsole.bind(null, aState)); + }); + }); + } + }); +} + +function closeDebugger(aState, aCallback) +{ + aState.dbgClient.close().then(aCallback); + aState.dbgClient = null; + aState.client = null; +} + +function checkConsoleAPICalls(consoleCalls, expectedConsoleCalls) +{ + is(consoleCalls.length, expectedConsoleCalls.length, + "received correct number of console calls"); + expectedConsoleCalls.forEach(function (aMessage, aIndex) { + info("checking received console call #" + aIndex); + checkConsoleAPICall(consoleCalls[aIndex], expectedConsoleCalls[aIndex]); + }); +} + +function checkConsoleAPICall(aCall, aExpected) +{ + if (aExpected.level != "trace" && aExpected.arguments) { + is(aCall.arguments.length, aExpected.arguments.length, + "number of arguments"); + } + + checkObject(aCall, aExpected); +} + +function checkObject(aObject, aExpected) +{ + for (let name of Object.keys(aExpected)) + { + let expected = aExpected[name]; + let value = aObject[name]; + checkValue(name, value, expected); + } +} + +function checkValue(aName, aValue, aExpected) +{ + if (aExpected === null) { + ok(!aValue, "'" + aName + "' is null"); + } + else if (aValue === undefined) { + ok(false, "'" + aName + "' is undefined"); + } + else if (aValue === null) { + ok(false, "'" + aName + "' is null"); + } + else if (typeof aExpected == "string" || typeof aExpected == "number" || + typeof aExpected == "boolean") { + is(aValue, aExpected, "property '" + aName + "'"); + } + else if (aExpected instanceof RegExp) { + ok(aExpected.test(aValue), aName + ": " + aExpected + " matched " + aValue); + } + else if (Array.isArray(aExpected)) { + info("checking array for property '" + aName + "'"); + checkObject(aValue, aExpected); + } + else if (typeof aExpected == "object") { + info("checking object for property '" + aName + "'"); + checkObject(aValue, aExpected); + } +} + +function checkHeadersOrCookies(aArray, aExpected) +{ + let foundHeaders = {}; + + for (let elem of aArray) { + if (!(elem.name in aExpected)) { + continue; + } + foundHeaders[elem.name] = true; + info("checking value of header " + elem.name); + checkValue(elem.name, elem.value, aExpected[elem.name]); + } + + for (let header in aExpected) { + if (!(header in foundHeaders)) { + ok(false, header + " was not found"); + } + } +} + +function checkRawHeaders(aText, aExpected) +{ + let headers = aText.split(/\r\n|\n|\r/); + let arr = []; + for (let header of headers) { + let index = header.indexOf(": "); + if (index < 0) { + continue; + } + arr.push({ + name: header.substr(0, index), + value: header.substr(index + 2) + }); + } + + checkHeadersOrCookies(arr, aExpected); +} + +var gTestState = {}; + +function runTests(aTests, aEndCallback) +{ + function* driver() + { + let lastResult, sendToNext; + for (let i = 0; i < aTests.length; i++) { + gTestState.index = i; + let fn = aTests[i]; + info("will run test #" + i + ": " + fn.name); + lastResult = fn(sendToNext, lastResult); + sendToNext = yield lastResult; + } + yield aEndCallback(sendToNext, lastResult); + } + gTestState.driver = driver(); + return gTestState.driver.next(); +} + +function nextTest(aMessage) +{ + return gTestState.driver.next(aMessage); +} + +function withFrame(url) { + return new Promise(resolve => { + let iframe = document.createElement("iframe"); + iframe.onload = function () { + resolve(iframe); + }; + iframe.src = url; + document.body.appendChild(iframe); + }); +} + +function navigateFrame(iframe, url) { + return new Promise(resolve => { + iframe.onload = function () { + resolve(iframe); + }; + iframe.src = url; + }); +} + +function forceReloadFrame(iframe) { + return new Promise(resolve => { + iframe.onload = function () { + resolve(iframe); + }; + iframe.contentWindow.location.reload(true); + }); +} + +function withActiveServiceWorker(win, url, scope) { + let opts = {}; + if (scope) { + opts.scope = scope; + } + return win.navigator.serviceWorker.register(url, opts).then(swr => { + if (swr.active) { + return swr; + } + + // Unfortunately we can't just use navigator.serviceWorker.ready promise + // here. If the service worker is for a scope that does not cover the window + // then the ready promise will never resolve. Instead monitor the service + // workers state change events to determine when its activated. + return new Promise(resolve => { + let sw = swr.waiting || swr.installing; + sw.addEventListener("statechange", function stateHandler(evt) { + if (sw.state === "activated") { + sw.removeEventListener("statechange", stateHandler); + resolve(swr); + } + }); + }); + }); +} + +function messageServiceWorker(win, scope, message) { + return win.navigator.serviceWorker.getRegistration(scope).then(swr => { + return new Promise(resolve => { + win.navigator.serviceWorker.onmessage = evt => { + resolve(); + }; + let sw = swr.active || swr.waiting || swr.installing; + sw.postMessage({ type: "PING", message: message }); + }); + }); +} + +function unregisterServiceWorker(win) { + return win.navigator.serviceWorker.ready.then(swr => { + return swr.unregister(); + }); +} diff --git a/devtools/shared/webconsole/test/console-test-worker.js b/devtools/shared/webconsole/test/console-test-worker.js new file mode 100644 index 000000000..881eab0b8 --- /dev/null +++ b/devtools/shared/webconsole/test/console-test-worker.js @@ -0,0 +1,16 @@ +"use strict"; + +function f() { + var a = 1; + var b = 2; + var c = 3; +} + +self.onmessage = function (event) { + if (event.data == "ping") { + f(); + postMessage("pong"); + } +}; + +postMessage("load"); diff --git a/devtools/shared/webconsole/test/data.json b/devtools/shared/webconsole/test/data.json new file mode 100644 index 000000000..d46085c12 --- /dev/null +++ b/devtools/shared/webconsole/test/data.json @@ -0,0 +1,3 @@ +{ id: "test JSON data", myArray: [ "foo", "bar", "baz", "biff" ], + veryLong: "foo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo barfoo bar" +} diff --git a/devtools/shared/webconsole/test/data.json^headers^ b/devtools/shared/webconsole/test/data.json^headers^ new file mode 100644 index 000000000..bb6b45500 --- /dev/null +++ b/devtools/shared/webconsole/test/data.json^headers^ @@ -0,0 +1,3 @@ +Content-Type: application/json +x-very-long: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse a ipsum massa. Phasellus at elit dictum libero laoreet sagittis. Phasellus condimentum ultricies imperdiet. Nam eu ligula justo, ut tincidunt quam. Etiam sollicitudin, tortor sed egestas blandit, sapien sem tincidunt nulla, eu luctus libero odio quis leo. Nam elit massa, mattis quis blandit ac, facilisis vitae arcu. Donec vitae dictum neque. Proin ornare nisl at lectus commodo iaculis eget eget est. Quisque scelerisque vestibulum quam sed interdum. +x-very-short: hello world diff --git a/devtools/shared/webconsole/test/helper_serviceworker.js b/devtools/shared/webconsole/test/helper_serviceworker.js new file mode 100644 index 000000000..633f3e09b --- /dev/null +++ b/devtools/shared/webconsole/test/helper_serviceworker.js @@ -0,0 +1,19 @@ +console.log("script evaluation"); + +addEventListener("install", function (evt) { + console.log("install event"); +}); + +addEventListener("activate", function (evt) { + console.log("activate event"); +}); + +addEventListener("fetch", function (evt) { + console.log("fetch event: " + evt.request.url); + evt.respondWith(new Response("Hello world")); +}); + +addEventListener("message", function (evt) { + console.log("message event: " + evt.data.message); + evt.source.postMessage({ type: "PONG" }); +}); diff --git a/devtools/shared/webconsole/test/network_requests_iframe.html b/devtools/shared/webconsole/test/network_requests_iframe.html new file mode 100644 index 000000000..9147b6720 --- /dev/null +++ b/devtools/shared/webconsole/test/network_requests_iframe.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset="utf-8"> + <title>Console HTTP test page</title> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + <script type="text/javascript"><!-- + var setAllowAllCookies = false; + + function makeXhr(aMethod, aUrl, aRequestBody, aCallback) { + // On the first call, allow all cookies and set cookies, then resume the actual test + if (!setAllowAllCookies) + SpecialPowers.pushPrefEnv({"set": [["network.cookie.cookieBehavior", 0]]}, function () { + setAllowAllCookies = true; + setCookies(); + makeXhrCallback(aMethod, aUrl, aRequestBody, aCallback); + }); + else + makeXhrCallback(aMethod, aUrl, aRequestBody, aCallback); + } + + function makeXhrCallback(aMethod, aUrl, aRequestBody, aCallback) { + var xmlhttp = new XMLHttpRequest(); + xmlhttp.open(aMethod, aUrl, true); + if (aCallback) { + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState == 4) { + aCallback(); + } + }; + } + xmlhttp.send(aRequestBody); + } + + function testXhrGet(aCallback) { + makeXhr('get', 'data.json', null, aCallback); + } + + function testXhrPost(aCallback) { + var body = "Hello world! " + (new Array(50)).join("foobaz barr"); + makeXhr('post', 'data.json', body, aCallback); + } + + function setCookies() { + document.cookie = "foobar=fooval"; + document.cookie = "omgfoo=bug768096"; + document.cookie = "badcookie=bug826798=st3fan"; + } + // --></script> + </head> + <body> + <h1>Web Console HTTP Logging Testpage</h1> + <h2>This page is used to test the HTTP logging.</h2> + + <form action="?" method="post"> + <input name="name" type="text" value="foo bar"><br> + <input name="age" type="text" value="144"><br> + </form> + </body> +</html> diff --git a/devtools/shared/webconsole/test/sandboxed_iframe.html b/devtools/shared/webconsole/test/sandboxed_iframe.html new file mode 100644 index 000000000..55a6224b5 --- /dev/null +++ b/devtools/shared/webconsole/test/sandboxed_iframe.html @@ -0,0 +1,8 @@ +<html> +<head><title>Sandboxed iframe</title></head> +<body> + <iframe id="sandboxed-iframe" + sandbox="allow-scripts" + srcdoc="<script>var foobarObject = {bug1051224: 'sandboxed'};</script>"></iframe> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_basics.html b/devtools/shared/webconsole/test/test_basics.html new file mode 100644 index 000000000..fa54557ae --- /dev/null +++ b/devtools/shared/webconsole/test/test_basics.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Basic Web Console Actor tests</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Basic Web Console Actor tests</p> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +function startTest() +{ + removeEventListener("load", startTest); + + attachConsoleToTab(["PageError"], onStartPageError); +} + +function onStartPageError(aState, aResponse) +{ + is(aResponse.startedListeners.length, 1, "startedListeners.length"); + is(aResponse.startedListeners[0], "PageError", "startedListeners: PageError"); + ok(aResponse.nativeConsoleAPI, "nativeConsoleAPI"); + + closeDebugger(aState, function() { + top.console_ = top.console; + top.console = { lolz: "foo" }; + attachConsoleToTab(["PageError", "ConsoleAPI", "foo"], + onStartPageErrorAndConsoleAPI); + }); +} + +function onStartPageErrorAndConsoleAPI(aState, aResponse) +{ + let startedListeners = aResponse.startedListeners; + is(startedListeners.length, 2, "startedListeners.length"); + isnot(startedListeners.indexOf("PageError"), -1, "startedListeners: PageError"); + isnot(startedListeners.indexOf("ConsoleAPI"), -1, + "startedListeners: ConsoleAPI"); + is(startedListeners.indexOf("foo"), -1, "startedListeners: no foo"); + ok(!aResponse.nativeConsoleAPI, "!nativeConsoleAPI"); + + top.console = top.console_; + aState.client.stopListeners(["ConsoleAPI", "foo"], + onStopConsoleAPI.bind(null, aState)); +} + +function onStopConsoleAPI(aState, aResponse) +{ + is(aResponse.stoppedListeners.length, 1, "stoppedListeners.length"); + is(aResponse.stoppedListeners[0], "ConsoleAPI", "stoppedListeners: ConsoleAPI"); + + closeDebugger(aState, function() { + attachConsoleToTab(["ConsoleAPI"], onStartConsoleAPI); + }); +} + +function onStartConsoleAPI(aState, aResponse) +{ + is(aResponse.startedListeners.length, 1, "startedListeners.length"); + is(aResponse.startedListeners[0], "ConsoleAPI", "startedListeners: ConsoleAPI"); + ok(aResponse.nativeConsoleAPI, "nativeConsoleAPI"); + + top.console = top.console_; + delete top.console_; + + closeDebugger(aState, function() { + SimpleTest.finish(); + }); +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_bug819670_getter_throws.html b/devtools/shared/webconsole/test/test_bug819670_getter_throws.html new file mode 100644 index 000000000..1b45c2d88 --- /dev/null +++ b/devtools/shared/webconsole/test/test_bug819670_getter_throws.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for Bug 819670 - Web console object inspection does not handle native getters throwing very well</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for Bug 819670 - Web console object inspection does not handle native getters throwing very well</p> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +function startTest() +{ + removeEventListener("load", startTest); + attachConsoleToTab([], onAttach); +} + +function onAttach(aState, aResponse) +{ + onEvaluate = onEvaluate.bind(null, aState); + aState.client.evaluateJS("document.__proto__", onEvaluate); +} + +function onEvaluate(aState, aResponse) +{ + checkObject(aResponse, { + from: aState.actor, + input: "document.__proto__", + result: { + type: "object", + actor: /[a-z]/, + }, + }); + + ok(!aResponse.exception, "no eval exception"); + ok(!aResponse.helperResult, "no helper result"); + + onInspect = onInspect.bind(null, aState); + let client = new ObjectClient(aState.dbgClient, aResponse.result); + client.getPrototypeAndProperties(onInspect); +} + +function onInspect(aState, aResponse) +{ + ok(!aResponse.error, "no response error"); + + let expectedProps = { + "addBroadcastListenerFor": { value: { type: "object" } }, + "commandDispatcher": { get: { type: "object" } }, + "getBoxObjectFor": { value: { type: "object" } }, + "getElementsByAttribute": { value: { type: "object" } }, + }; + + let props = aResponse.ownProperties; + ok(props, "response properties available"); + + if (props) { + ok(Object.keys(props).length > Object.keys(expectedProps).length, + "number of enumerable properties"); + checkObject(props, expectedProps); + } + + closeDebugger(aState, function() { + SimpleTest.finish(); + }); +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_cached_messages.html b/devtools/shared/webconsole/test/test_cached_messages.html new file mode 100644 index 000000000..210543ca3 --- /dev/null +++ b/devtools/shared/webconsole/test/test_cached_messages.html @@ -0,0 +1,230 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for cached messages</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for cached messages</p> + +<script class="testbody" type="application/javascript;version=1.8"> +let expectedConsoleCalls = []; +let expectedPageErrors = []; + +function doPageErrors() +{ + Services.console.reset(); + + expectedPageErrors = [ + { + _type: "PageError", + errorMessage: /fooColor/, + sourceName: /.+/, + category: "CSS Parser", + timeStamp: /^\d+$/, + error: false, + warning: true, + exception: false, + strict: false, + }, + { + _type: "PageError", + errorMessage: /doTheImpossible/, + sourceName: /.+/, + category: "chrome javascript", + timeStamp: /^\d+$/, + error: false, + warning: false, + exception: true, + strict: false, + }, + ]; + + let container = document.createElement("script"); + document.body.appendChild(container); + container.textContent = "document.body.style.color = 'fooColor';"; + document.body.removeChild(container); + + SimpleTest.expectUncaughtException(); + + container = document.createElement("script"); + document.body.appendChild(container); + container.textContent = "document.doTheImpossible();"; + document.body.removeChild(container); +} + +function doConsoleCalls() +{ + ConsoleAPIStorage.clearEvents(); + + top.console.log("foobarBaz-log", undefined); + top.console.info("foobarBaz-info", null); + top.console.warn("foobarBaz-warn", document.body); + + expectedConsoleCalls = [ + { + _type: "ConsoleAPI", + level: "log", + filename: /test_cached_messages/, + functionName: "doConsoleCalls", + timeStamp: /^\d+$/, + arguments: ["foobarBaz-log", { type: "undefined" }], + }, + { + _type: "ConsoleAPI", + level: "info", + filename: /test_cached_messages/, + functionName: "doConsoleCalls", + timeStamp: /^\d+$/, + arguments: ["foobarBaz-info", { type: "null" }], + }, + { + _type: "ConsoleAPI", + level: "warn", + filename: /test_cached_messages/, + functionName: "doConsoleCalls", + timeStamp: /^\d+$/, + arguments: ["foobarBaz-warn", { type: "object", actor: /[a-z]/ }], + }, + ]; +} +</script> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +let consoleAPIListener, consoleServiceListener; +let consoleAPICalls = 0; +let pageErrors = 0; + +let handlers = { + onConsoleAPICall: function onConsoleAPICall(aMessage) + { + for (let msg of expectedConsoleCalls) { + if (msg.functionName == aMessage.functionName && + msg.filename.test(aMessage.filename)) { + consoleAPICalls++; + break; + } + } + if (consoleAPICalls == expectedConsoleCalls.length) { + checkConsoleAPICache(); + } + }, + + onConsoleServiceMessage: function onConsoleServiceMessage(aMessage) + { + if (!(aMessage instanceof Ci.nsIScriptError)) { + return; + } + for (let msg of expectedPageErrors) { + if (msg.category == aMessage.category && + msg.errorMessage.test(aMessage.errorMessage)) { + pageErrors++; + break; + } + } + if (pageErrors == expectedPageErrors.length) { + testPageErrors(); + } + }, +}; + +function startTest() +{ + removeEventListener("load", startTest); + + consoleAPIListener = new ConsoleAPIListener(top, handlers); + consoleAPIListener.init(); + + doConsoleCalls(); +} + +function checkConsoleAPICache() +{ + consoleAPIListener.destroy(); + consoleAPIListener = null; + attachConsole(["ConsoleAPI"], onAttach1); +} + +function onAttach1(aState, aResponse) +{ + aState.client.getCachedMessages(["ConsoleAPI"], + onCachedConsoleAPI.bind(null, aState)); +} + +function onCachedConsoleAPI(aState, aResponse) +{ + let msgs = aResponse.messages; + info("cached console messages: " + msgs.length); + + ok(msgs.length >= expectedConsoleCalls.length, + "number of cached console messages"); + + for (let msg of msgs) { + for (let expected of expectedConsoleCalls) { + if (expected.functionName == msg.functionName && + expected.filename.test(msg.filename)) { + expectedConsoleCalls.splice(expectedConsoleCalls.indexOf(expected)); + checkConsoleAPICall(msg, expected); + break; + } + } + } + + is(expectedConsoleCalls.length, 0, "all expected messages have been found"); + + closeDebugger(aState, function() { + consoleServiceListener = new ConsoleServiceListener(null, handlers); + consoleServiceListener.init(); + doPageErrors(); + }); +} + +function testPageErrors() +{ + consoleServiceListener.destroy(); + consoleServiceListener = null; + attachConsole(["PageError"], onAttach2); +} + +function onAttach2(aState, aResponse) +{ + aState.client.getCachedMessages(["PageError"], + onCachedPageErrors.bind(null, aState)); +} + +function onCachedPageErrors(aState, aResponse) +{ + let msgs = aResponse.messages; + info("cached page errors: " + msgs.length); + + ok(msgs.length >= expectedPageErrors.length, + "number of cached page errors"); + + for (let msg of msgs) { + for (let expected of expectedPageErrors) { + if (expected.category == msg.category && + expected.errorMessage.test(msg.errorMessage)) { + expectedPageErrors.splice(expectedPageErrors.indexOf(expected)); + checkObject(msg, expected); + break; + } + } + } + + is(expectedPageErrors.length, 0, "all expected messages have been found"); + + closeDebugger(aState, function() { + SimpleTest.finish(); + }); +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_commands_other.html b/devtools/shared/webconsole/test/test_commands_other.html new file mode 100644 index 000000000..47d1142c9 --- /dev/null +++ b/devtools/shared/webconsole/test/test_commands_other.html @@ -0,0 +1,83 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for the other command helpers</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for the querySelector / querySelectorAll helpers</p> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); +let gState; +let gWin; +let tests; + +function evaluateJS(input) { + return new Promise((resolve) => gState.client.evaluateJS(input, resolve)); +} + +function startTest() { + info ("Content window opened, attaching console to it"); + + let systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal); + ok (!gWin.document.nodePrincipal.equals(systemPrincipal), + "The test document is not using the system principal"); + + attachConsoleToTab([], state => { + gState = state; + runTests(tests, testEnd); + }); +} + +tests = [ + Task.async(function* keys() { + let response = yield evaluateJS("keys({foo: 'bar'})"); + checkObject(response, { + from: gState.actor, + result: { + class: "Array", + preview: { + items: ["foo"] + } + } + }); + nextTest(); + }), + Task.async(function* values() { + let response = yield evaluateJS("values({foo: 'bar'})"); + checkObject(response, { + from: gState.actor, + result: { + class: "Array", + preview: { + items: ["bar"] + } + } + }); + nextTest(); + }), +]; + +function testEnd() { + gWin.close(); + gWin = null; + closeDebugger(gState, function() { + gState = null; + SimpleTest.finish(); + }); +} + +window.onload = function() { + // Open a content window to test XRay functionality on built in functions. + gWin = window.open("data:text/html,"); + info ("Waiting for content window to load"); + gWin.onload = startTest; +} +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_commands_registration.html b/devtools/shared/webconsole/test/test_commands_registration.html new file mode 100644 index 000000000..2a68f7cdc --- /dev/null +++ b/devtools/shared/webconsole/test/test_commands_registration.html @@ -0,0 +1,191 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for Web Console commands registration.</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for Web Console commands registration.</p> +<p id="quack"></p> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +let gState; +let tests; + +let {WebConsoleCommands} = require("devtools/server/actors/utils/webconsole-utils"); + +function evaluateJS(input) { + return new Promise((resolve) => gState.client.evaluateJS(input, resolve)); +} + +function* evaluateJSAndCheckResult(input, result) { + let response = yield evaluateJS(input); + checkObject(response, {result}); +} + +function startTest() +{ + removeEventListener("load", startTest); + + attachConsoleToTab(["PageError"], onAttach); +} + +function onAttach(aState, aResponse) +{ + gState = aState; + + runTests(tests, testEnd); +} + +tests = [ + Task.async(function* registerNewCommand() { + let win; + WebConsoleCommands.register("setFoo", (owner, value) => { + owner.window.foo = value; + return "ok"; + }); + + ok(WebConsoleCommands.hasCommand("setFoo"), + "The command should be registered"); + + let command = "setFoo('bar')"; + let response = yield evaluateJS(command); + + checkObject(response, { + from: gState.actor, + input: command, + result: "ok" + }); + is(top.foo, "bar", "top.foo should equal to 'bar'"); + nextTest(); + }), + + Task.async(function* wrapCommand() { + let origKeys = WebConsoleCommands.getCommand("keys"); + + let newKeys = (...args) => { + let [owner, arg0] = args; + if (arg0 === ">o_/") { + return "bang!"; + } + else { + return origKeys(...args); + } + }; + + WebConsoleCommands.register("keys", newKeys); + is(WebConsoleCommands.getCommand("keys"), newKeys, + "the keys() command should have been replaced"); + + let response = yield evaluateJS("keys('>o_/')"); + checkObject(response, { + from: gState.actor, + result: "bang!" + }); + + response = yield evaluateJS("keys({foo: 'bar'})"); + checkObject(response, { + from: gState.actor, + result: { + class: "Array", + preview: { + items: ["foo"] + } + } + }); + + WebConsoleCommands.register("keys", origKeys); + is(WebConsoleCommands.getCommand("keys"), origKeys, + "the keys() command should be restored"); + nextTest(); + }), + + Task.async(function* unregisterCommand() { + WebConsoleCommands.unregister("setFoo"); + + let response = yield evaluateJS("setFoo"); + + checkObject(response, { + from: gState.actor, + input: "setFoo", + result: { + type: "undefined" + }, + exceptionMessage: /setFoo is not defined/ + }); + nextTest(); + }), + + Task.async(function* registerAccessor() { + WebConsoleCommands.register("$foo", { + get(owner) { + let foo = owner.window.frames[0].window.document.getElementById("quack"); + return owner.makeDebuggeeValue(foo); + } + }); + let command = "$foo.textContent = '>o_/'"; + let response = yield evaluateJS(command); + + checkObject(response, { + from: gState.actor, + input: command, + result: ">o_/" + }); + is(document.getElementById("quack").textContent, ">o_/", + "#foo textContent should equal to \">o_/\""); + WebConsoleCommands.unregister("$foo"); + ok(!WebConsoleCommands.hasCommand("$foo"), "$foo should be unregistered"); + nextTest(); + }), + + Task.async(function* unregisterAfterOverridingTwice() { + WebConsoleCommands.register("keys", (owner, obj) => "command 1"); + info("checking the value of the first override"); + yield evaluateJSAndCheckResult("keys('foo');", "command 1"); + + let orig = WebConsoleCommands.getCommand("keys"); + WebConsoleCommands.register("keys", (owner, obj) => { + if (obj === "quack") + return "bang!"; + return orig(owner, obj); + }); + + info("checking the values after the second override"); + yield evaluateJSAndCheckResult("keys({});", "command 1"); + yield evaluateJSAndCheckResult("keys('quack');", "bang!"); + + WebConsoleCommands.unregister("keys"); + + info("checking the value after unregistration (should restore " + + "the original command)"); + yield evaluateJSAndCheckResult("keys({});", { + class: "Array", + preview: {items: []} + }); + nextTest(); + + }) +]; + +function testEnd() +{ + // If this is the first run, reload the page and do it again. + // Otherwise, end the test. + delete top.foo; + closeDebugger(gState, function() { + gState = null; + SimpleTest.finish(); + }); +} + +addEventListener("load", startTest); +</script> +</body> +</html> + diff --git a/devtools/shared/webconsole/test/test_console_serviceworker.html b/devtools/shared/webconsole/test/test_console_serviceworker.html new file mode 100644 index 000000000..83d728d92 --- /dev/null +++ b/devtools/shared/webconsole/test/test_console_serviceworker.html @@ -0,0 +1,157 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for the Console API and Service Workers</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for the Console API and Service Workers</p> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +let BASE_URL = "https://example.com/chrome/devtools/shared/webconsole/test/"; +let SERVICE_WORKER_URL = BASE_URL + "helper_serviceworker.js"; +let SCOPE = BASE_URL + "foo/"; +let NONSCOPE_FRAME_URL = BASE_URL + "sandboxed_iframe.html"; +let SCOPE_FRAME_URL = SCOPE + "fake.html"; +let SCOPE_FRAME_URL2 = SCOPE + "whatsit.html"; +let MESSAGE = 'Tic Tock'; + +let expectedConsoleCalls = [ + { + level: "log", + filename: /helper_serviceworker/, + arguments: ['script evaluation'], + }, + { + level: "log", + filename: /helper_serviceworker/, + arguments: ['install event'], + }, + { + level: "log", + filename: /helper_serviceworker/, + arguments: ['activate event'], + }, + { + level: "log", + filename: /helper_serviceworker/, + arguments: ['fetch event: ' + SCOPE_FRAME_URL], + }, + { + level: "log", + filename: /helper_serviceworker/, + arguments: ['fetch event: ' + SCOPE_FRAME_URL2], + }, + { + level: "log", + filename: /helper_serviceworker/, + arguments: ['message event: ' + MESSAGE], + }, +]; +let consoleCalls = []; + +let startTest = Task.async(function*() { + removeEventListener("load", startTest); + + yield new Promise(resolve => { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["devtools.webconsole.filter.serviceworkers", true] + ]}, resolve); + }); + + attachConsoleToTab(["ConsoleAPI"], onAttach); +}); +addEventListener("load", startTest); + +let onAttach = Task.async(function*(state, response) { + onConsoleAPICall = onConsoleAPICall.bind(null, state); + state.dbgClient.addListener("consoleAPICall", onConsoleAPICall); + + let currentFrame; + try { + // First, we need a frame from which to register our script. This + // will not trigger any console calls. + info("Loading a non-scope frame from which to register a service worker."); + currentFrame = yield withFrame(NONSCOPE_FRAME_URL); + + // Now register the service worker and wait for it to become + // activate. This should trigger 3 console calls; 1 for script + // evaluation, 1 for the install event, and 1 for the activate + // event. These console calls are received because we called + // register(), not because we are in scope for the worker. + info("Registering the service worker"); + yield withActiveServiceWorker(currentFrame.contentWindow, + SERVICE_WORKER_URL, SCOPE); + ok(!currentFrame.contentWindow.navigator.serviceWorker.controller, + 'current frame should not be controlled'); + + // Now that the service worker is activate, lets navigate our frame. + // This will trigger 1 more console call for the fetch event. + info("Service worker registered. Navigating frame."); + yield navigateFrame(currentFrame, SCOPE_FRAME_URL); + ok(currentFrame.contentWindow.navigator.serviceWorker.controller, + 'navigated frame should be controlled'); + + // We now have a controlled frame. Lets perform a non-navigation fetch. + // This should produce another console call for the fetch event. + info("Frame navigated. Calling fetch()."); + yield currentFrame.contentWindow.fetch(SCOPE_FRAME_URL2); + + // Now force refresh our controlled frame. This will cause the frame + // to bypass the service worker and become an uncontrolled frame. It + // also happens to make the frame display a 404 message because the URL + // does not resolve to a real resource. This is ok, as we really only + // care about the frame being non-controlled, but still having a location + // that matches our service worker scope so we can provide its not + // incorrectly getting console calls. + info("Completed fetch(). Force refreshing to get uncontrolled frame."); + yield forceReloadFrame(currentFrame); + ok(!currentFrame.contentWindow.navigator.serviceWorker.controller, + 'current frame should not be controlled after force refresh'); + is(currentFrame.contentWindow.location.toString(), SCOPE_FRAME_URL, + 'current frame should still have in-scope location URL even though it got 404'); + + // Now postMessage() the service worker to trigger its message event + // handler. This will generate 1 or 2 to console.log() statements + // depending on if the worker thread needs to spin up again. Although we + // don't have a controlled or registering document in both cases, we still + // could get console calls since we only flush reports when the channel is + // finally destroyed. + info("Completed force refresh. Messaging service worker."); + yield messageServiceWorker(currentFrame.contentWindow, SCOPE, MESSAGE); + + info("Done messaging service worker. Unregistering service worker."); + yield unregisterServiceWorker(currentFrame.contentWindow); + + info('Service worker unregistered. Checking console calls.'); + state.dbgClient.removeListener("consoleAPICall", onConsoleAPICall); + checkConsoleAPICalls(consoleCalls, expectedConsoleCalls); + } catch(error) { + ok(false, 'unexpected error: ' + error); + } finally { + if (currentFrame) { + currentFrame.remove(); + currentFrame = null; + } + consoleCalls = []; + closeDebugger(state, function() { + SimpleTest.finish(); + }); + } +}); + +function onConsoleAPICall(state, type, packet) { + info("received message level: " + packet.message.level); + is(packet.from, state.actor, "console API call actor"); + consoleCalls.push(packet.message); +} +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_console_serviceworker_cached.html b/devtools/shared/webconsole/test/test_console_serviceworker_cached.html new file mode 100644 index 000000000..5aab64d7f --- /dev/null +++ b/devtools/shared/webconsole/test/test_console_serviceworker_cached.html @@ -0,0 +1,117 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for getCachedMessages and Service Workers</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for getCachedMessages and Service Workers</p> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +let BASE_URL = "https://example.com/chrome/devtools/shared/webconsole/test/"; +let SERVICE_WORKER_URL = BASE_URL + "helper_serviceworker.js"; +let FRAME_URL = BASE_URL + "sandboxed_iframe.html"; + +let firstTabExpectedCalls = [ + { + level: "log", + filename: /helper_serviceworker/, + arguments: ['script evaluation'], + }, + { + level: "log", + filename: /helper_serviceworker/, + arguments: ['install event'], + }, + { + level: "log", + filename: /helper_serviceworker/, + arguments: ['activate event'], + }, +]; + +let secondTabExpectedCalls = [ + { + level: "log", + filename: /helper_serviceworker/, + arguments: ['fetch event: ' + FRAME_URL], + } +]; + +let startTest = Task.async(function*() { + removeEventListener("load", startTest); + + yield new Promise(resolve => { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["devtools.webconsole.filter.serviceworkers", true] + ]}, resolve); + }); + + info("Adding a tab and attaching a service worker"); + let tab1 = yield addTab(FRAME_URL); + let swr = yield withActiveServiceWorker(tab1.linkedBrowser.contentWindow, + SERVICE_WORKER_URL); + + yield new Promise(resolve => { + info("Attaching console to tab 1"); + attachConsoleToTab(["ConsoleAPI"], function(state) { + state.client.getCachedMessages(["ConsoleAPI"], function(calls) { + checkConsoleAPICalls(calls.messages, firstTabExpectedCalls); + closeDebugger(state, resolve); + }); + }); + }); + + // Because this tab is being added after the original messages happened, + // they shouldn't show up in a call to getCachedMessages. + // However, there is a fetch event which is logged due to loading the tab. + info("Adding a new tab at the same URL"); + let tab2 = yield addTab(FRAME_URL); + yield new Promise(resolve => { + info("Attaching console to tab 2"); + attachConsoleToTab(["ConsoleAPI"], function(state) { + state.client.getCachedMessages(["ConsoleAPI"], function(calls) { + checkConsoleAPICalls(calls.messages, secondTabExpectedCalls); + closeDebugger(state, resolve); + }); + }); + }); + + yield swr.unregister(); + + SimpleTest.finish(); +}); +addEventListener("load", startTest); + +// This test needs to add tabs that are controlled by a service worker +// so use some special powers to dig around and find gBrowser +let {gBrowser} = SpecialPowers._getTopChromeWindow(SpecialPowers.window.get()); + +SimpleTest.registerCleanupFunction(() => { + while (gBrowser.tabs.length > 1) { + gBrowser.removeCurrentTab(); + } +}); + +function addTab(url) { + info("Adding a new tab with URL: '" + url + "'"); + return new Promise(resolve => { + let tab = gBrowser.selectedTab = gBrowser.addTab(url); + gBrowser.selectedBrowser.addEventListener("load", function onload() { + gBrowser.selectedBrowser.removeEventListener("load", onload, true); + info("URL '" + url + "' loading complete"); + resolve(tab); + }, true); + }); +} + +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_console_styling.html b/devtools/shared/webconsole/test/test_console_styling.html new file mode 100644 index 000000000..97d21bcb9 --- /dev/null +++ b/devtools/shared/webconsole/test/test_console_styling.html @@ -0,0 +1,126 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for console.log styling with %c</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for console.log styling with %c</p> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +let expectedConsoleCalls = []; + +function doConsoleCalls(aState) +{ + top.console.log("%cOne formatter with no styles"); + top.console.log("%cOne formatter", "color: red"); + top.console.log("%cTwo formatters%cEach with an arg", + "color: red", "background: red"); + top.console.log("%c%cTwo formatters next to each other", + "color: red", "background: red"); + top.console.log("%c%c%cThree formatters next to each other", + "color: red", "background: red", "font-size: 150%"); + top.console.log("%c%cTwo formatters%cWith a third separated", + "color: red", "background: red", "font-size: 150%"); + top.console.log("%cOne formatter", "color: red", + "Second arg with no styles"); + top.console.log("%cOne formatter", "color: red", + "%cSecond formatter is ignored", "background: blue") + + expectedConsoleCalls = [ + { + level: "log", + styles: /^$/, + arguments: ["%cOne formatter with no styles"], + }, + { + level: "log", + styles: /^color: red$/, + arguments: ["One formatter"], + }, + { + level: "log", + styles: /^color: red,background: red$/, + arguments: ["Two formatters", "Each with an arg"], + }, + { + level: "log", + styles: /^background: red$/, + arguments: ["Two formatters next to each other"], + }, + { + level: "log", + styles: /^font-size: 150%$/, + arguments: ["Three formatters next to each other"], + }, + { + level: "log", + styles: /^background: red,font-size: 150%$/, + arguments: ["Two formatters", "With a third separated"], + }, + { + level: "log", + styles: /^color: red$/, + arguments: ["One formatter", "Second arg with no styles"], + }, + { + level: "log", + styles: /^color: red$/, + arguments: ["One formatter", + "%cSecond formatter is ignored", + "background: blue"], + }, + ]; +} + +function startTest() +{ + removeEventListener("load", startTest); + + attachConsoleToTab(["ConsoleAPI"], onAttach); +} + +function onAttach(aState, aResponse) +{ + onConsoleAPICall = onConsoleAPICall.bind(null, aState); + aState.dbgClient.addListener("consoleAPICall", onConsoleAPICall); + doConsoleCalls(aState.actor); +} + +let consoleCalls = []; + +function onConsoleAPICall(aState, aType, aPacket) +{ + info("received message level: " + aPacket.message.level); + is(aPacket.from, aState.actor, "console API call actor"); + + consoleCalls.push(aPacket.message); + if (consoleCalls.length != expectedConsoleCalls.length) { + return; + } + + aState.dbgClient.removeListener("consoleAPICall", onConsoleAPICall); + + expectedConsoleCalls.forEach(function(aMessage, aIndex) { + info("checking received console call #" + aIndex); + checkConsoleAPICall(consoleCalls[aIndex], expectedConsoleCalls[aIndex]); + }); + + + consoleCalls = []; + + closeDebugger(aState, function() { + SimpleTest.finish(); + }); +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_consoleapi.html b/devtools/shared/webconsole/test/test_consoleapi.html new file mode 100644 index 000000000..848db9cb6 --- /dev/null +++ b/devtools/shared/webconsole/test/test_consoleapi.html @@ -0,0 +1,233 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for the Console API</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for the Console API</p> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +let expectedConsoleCalls = []; + +function doConsoleCalls(aState) +{ + let longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 2)).join("a"); + + top.console.log("foobarBaz-log", undefined); + + top.console.log("Float from not a number: %f", "foo"); + top.console.log("Float from string: %f", "1.2"); + top.console.log("Float from number: %f", 1.3); + + top.console.info("foobarBaz-info", null); + top.console.warn("foobarBaz-warn", top.document.documentElement); + top.console.debug(null); + top.console.trace(); + top.console.dir(top.document, top.location); + top.console.log("foo", longString); + + let sandbox = new Cu.Sandbox(null, { invisibleToDebugger: true }); + let sandboxObj = sandbox.eval("new Object"); + top.console.log(sandboxObj); + + function fromAsmJS() { + top.console.error("foobarBaz-asmjs-error", undefined); + } + + (function(global, foreign) { + "use asm"; + var fromAsmJS = foreign.fromAsmJS; + function inAsmJS2() { fromAsmJS() } + function inAsmJS1() { inAsmJS2() } + return inAsmJS1 + })(null, { fromAsmJS:fromAsmJS })(); + + expectedConsoleCalls = [ + { + level: "log", + filename: /test_consoleapi/, + functionName: "doConsoleCalls", + timeStamp: /^\d+$/, + arguments: ["foobarBaz-log", { type: "undefined" }], + }, + { + level: "log", + arguments: ["Float from not a number: NaN"], + }, + { + level: "log", + arguments: ["Float from string: 1.200000"], + }, + { + level: "log", + arguments: ["Float from number: 1.300000"], + }, + { + level: "info", + filename: /test_consoleapi/, + functionName: "doConsoleCalls", + timeStamp: /^\d+$/, + arguments: ["foobarBaz-info", { type: "null" }], + }, + { + level: "warn", + filename: /test_consoleapi/, + functionName: "doConsoleCalls", + timeStamp: /^\d+$/, + arguments: ["foobarBaz-warn", { type: "object", actor: /[a-z]/ }], + }, + { + level: "debug", + filename: /test_consoleapi/, + functionName: "doConsoleCalls", + timeStamp: /^\d+$/, + arguments: [{ type: "null" }], + }, + { + level: "trace", + filename: /test_consoleapi/, + functionName: "doConsoleCalls", + timeStamp: /^\d+$/, + stacktrace: [ + { + filename: /test_consoleapi/, + functionName: "doConsoleCalls", + }, + { + filename: /test_consoleapi/, + functionName: "onAttach", + }, + ], + }, + { + level: "dir", + filename: /test_consoleapi/, + functionName: "doConsoleCalls", + timeStamp: /^\d+$/, + arguments: [ + { + type: "object", + actor: /[a-z]/, + class: "XULDocument", + }, + { + type: "object", + actor: /[a-z]/, + class: "Location", + } + ], + }, + { + level: "log", + filename: /test_consoleapi/, + functionName: "doConsoleCalls", + timeStamp: /^\d+$/, + arguments: [ + "foo", + { + type: "longString", + initial: longString.substring(0, + DebuggerServer.LONG_STRING_INITIAL_LENGTH), + length: longString.length, + actor: /[a-z]/, + }, + ], + }, + { + level: "log", + filename: /test_consoleapi/, + functionName: "doConsoleCalls", + timeStamp: /^\d+$/, + arguments: [ + { + type: "object", + actor: /[a-z]/, + class: "Object", + }, + ], + }, + { + level: "error", + filename: /test_consoleapi/, + functionName: "fromAsmJS", + timeStamp: /^\d+$/, + arguments: ["foobarBaz-asmjs-error", { type: "undefined" }], + + stacktrace: [ + { + filename: /test_consoleapi/, + functionName: "fromAsmJS", + }, + { + filename: /test_consoleapi/, + functionName: "inAsmJS2", + }, + { + filename: /test_consoleapi/, + functionName: "inAsmJS1", + }, + { + filename: /test_consoleapi/, + functionName: "doConsoleCalls", + }, + { + filename: /test_consoleapi/, + functionName: "onAttach", + }, + ], + }, + ]; +} + +function startTest() +{ + removeEventListener("load", startTest); + + attachConsoleToTab(["ConsoleAPI"], onAttach); +} + +function onAttach(aState, aResponse) +{ + onConsoleAPICall = onConsoleAPICall.bind(null, aState); + aState.dbgClient.addListener("consoleAPICall", onConsoleAPICall); + doConsoleCalls(aState.actor); +} + +let consoleCalls = []; + +function onConsoleAPICall(aState, aType, aPacket) +{ + info("received message level: " + aPacket.message.level); + is(aPacket.from, aState.actor, "console API call actor"); + + consoleCalls.push(aPacket.message); + if (consoleCalls.length != expectedConsoleCalls.length) { + return; + } + + aState.dbgClient.removeListener("consoleAPICall", onConsoleAPICall); + + expectedConsoleCalls.forEach(function(aMessage, aIndex) { + info("checking received console call #" + aIndex); + checkConsoleAPICall(consoleCalls[aIndex], expectedConsoleCalls[aIndex]); + }); + + + consoleCalls = []; + + closeDebugger(aState, function() { + SimpleTest.finish(); + }); +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_consoleapi_innerID.html b/devtools/shared/webconsole/test/test_consoleapi_innerID.html new file mode 100644 index 000000000..b477a36be --- /dev/null +++ b/devtools/shared/webconsole/test/test_consoleapi_innerID.html @@ -0,0 +1,164 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for the innerID property of the Console API</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for the Console API</p> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +let expectedConsoleCalls = []; + +function doConsoleCalls(aState) +{ + let { ConsoleAPI } = Cu.import("resource://gre/modules/Console.jsm", {}); + let console = new ConsoleAPI({ + innerID: window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .currentInnerWindowID + }); + + let longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 2)).join("a"); + + console.log("foobarBaz-log", undefined); + console.info("foobarBaz-info", null); + console.warn("foobarBaz-warn", top.document.documentElement); + console.debug(null); + console.trace(); + console.dir(top.document, top.location); + console.log("foo", longString); + + expectedConsoleCalls = [ + { + level: "log", + filename: /test_consoleapi/, + functionName: "doConsoleCalls", + timeStamp: /^\d+$/, + arguments: ["foobarBaz-log", { type: "undefined" }], + }, + { + level: "info", + filename: /test_consoleapi/, + functionName: "doConsoleCalls", + timeStamp: /^\d+$/, + arguments: ["foobarBaz-info", { type: "null" }], + }, + { + level: "warn", + filename: /test_consoleapi/, + functionName: "doConsoleCalls", + timeStamp: /^\d+$/, + arguments: ["foobarBaz-warn", { type: "object", actor: /[a-z]/ }], + }, + { + level: "debug", + filename: /test_consoleapi/, + functionName: "doConsoleCalls", + timeStamp: /^\d+$/, + arguments: [{ type: "null" }], + }, + { + level: "trace", + filename: /test_consoleapi/, + functionName: "doConsoleCalls", + timeStamp: /^\d+$/, + stacktrace: [ + { + filename: /test_consoleapi/, + functionName: "doConsoleCalls", + }, + { + filename: /test_consoleapi/, + functionName: "onAttach", + }, + ], + }, + { + level: "dir", + filename: /test_consoleapi/, + functionName: "doConsoleCalls", + timeStamp: /^\d+$/, + arguments: [ + { + type: "object", + actor: /[a-z]/, + class: "XULDocument", + }, + { + type: "object", + actor: /[a-z]/, + class: "Location", + } + ], + }, + { + level: "log", + filename: /test_consoleapi/, + functionName: "doConsoleCalls", + timeStamp: /^\d+$/, + arguments: [ + "foo", + { + type: "longString", + initial: longString.substring(0, + DebuggerServer.LONG_STRING_INITIAL_LENGTH), + length: longString.length, + actor: /[a-z]/, + }, + ], + }, + ]; +} + +function startTest() +{ + removeEventListener("load", startTest); + + attachConsoleToTab(["ConsoleAPI"], onAttach); +} + +function onAttach(aState, aResponse) +{ + onConsoleAPICall = onConsoleAPICall.bind(null, aState); + aState.dbgClient.addListener("consoleAPICall", onConsoleAPICall); + doConsoleCalls(aState.actor); +} + +let consoleCalls = []; + +function onConsoleAPICall(aState, aType, aPacket) +{ + info("received message level: " + aPacket.message.level); + is(aPacket.from, aState.actor, "console API call actor"); + + consoleCalls.push(aPacket.message); + if (consoleCalls.length != expectedConsoleCalls.length) { + return; + } + + aState.dbgClient.removeListener("consoleAPICall", onConsoleAPICall); + + expectedConsoleCalls.forEach(function(aMessage, aIndex) { + info("checking received console call #" + aIndex); + checkConsoleAPICall(consoleCalls[aIndex], expectedConsoleCalls[aIndex]); + }); + + + consoleCalls = []; + + closeDebugger(aState, function() { + SimpleTest.finish(); + }); +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_file_uri.html b/devtools/shared/webconsole/test/test_file_uri.html new file mode 100644 index 000000000..f5aada5b1 --- /dev/null +++ b/devtools/shared/webconsole/test/test_file_uri.html @@ -0,0 +1,106 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for file activity tracking</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for file activity tracking</p> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); + +let gState; +let gTmpFile; + +function doFileActivity() +{ + info("doFileActivity"); + let fileContent = "<p>hello world from bug 798764"; + + gTmpFile = FileUtils.getFile("TmpD", ["bug798764.html"]); + gTmpFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); + + let fout = FileUtils.openSafeFileOutputStream(gTmpFile, + FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE); + + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]. + createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + let fileContentStream = converter.convertToInputStream(fileContent); + + NetUtil.asyncCopy(fileContentStream, fout, addIframe); +} + +function addIframe(aStatus) +{ + ok(Components.isSuccessCode(aStatus), + "the temporary file was saved successfully"); + + let iframe = document.createElement("iframe"); + iframe.src = NetUtil.newURI(gTmpFile).spec; + document.body.appendChild(iframe); +} + +function startTest() +{ + removeEventListener("load", startTest); + + attachConsole(["FileActivity"], onAttach); +} + +function onAttach(aState, aResponse) +{ + gState = aState; + gState.dbgClient.addListener("fileActivity", onFileActivity); + doFileActivity(); +} + +function onFileActivity(aType, aPacket) +{ + is(aPacket.from, gState.actor, "fileActivity actor"); + + gState.dbgClient.removeListener("fileActivity", onFileActivity); + + info("aPacket.uri: " + aPacket.uri); + ok(/\bbug798764\b.*\.html$/.test(aPacket.uri), "file URI match"); + + testEnd(); +} + +function testEnd() +{ + if (gTmpFile) { + SimpleTest.executeSoon(function() { + try { + gTmpFile.remove(false); + } + catch (ex if (ex.name == "NS_ERROR_FILE_IS_LOCKED")) { + // Sometimes remove() throws because the file is not unlocked soon + // enough. + } + gTmpFile = null; + }); + } + + if (gState) { + closeDebugger(gState, function() { + gState = null; + SimpleTest.finish(); + }); + } else { + SimpleTest.finish(); + } +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_jsterm.html b/devtools/shared/webconsole/test/test_jsterm.html new file mode 100644 index 000000000..b6eefad4b --- /dev/null +++ b/devtools/shared/webconsole/test/test_jsterm.html @@ -0,0 +1,309 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for JavaScript terminal functionality</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for JavaScript terminal functionality</p> + +<iframe id="content-iframe" src="http://example.com/chrome/devtools/shared/webconsole/test/sandboxed_iframe.html"></iframe> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +let gState; + +let {MAX_AUTOCOMPLETE_ATTEMPTS,MAX_AUTOCOMPLETIONS} = require("devtools/shared/webconsole/js-property-provider"); + +// This test runs all of its assertions twice - once with +// evaluateJS and once with evaluateJSAsync. +let evaluatingSync = true; +function evaluateJS(input, options = {}) { + return new Promise((resolve, reject) => { + if (evaluatingSync) { + gState.client.evaluateJS(input, resolve, options); + } else { + gState.client.evaluateJSAsync(input, resolve, options); + } + }); +} + +function startTest() +{ + removeEventListener("load", startTest); + + attachConsoleToTab(["PageError"], onAttach); +} + +function onAttach(aState, aResponse) +{ + top.foobarObject = Object.create(null); + top.foobarObject.foo = 1; + top.foobarObject.foobar = 2; + top.foobarObject.foobaz = 3; + top.foobarObject.omg = 4; + top.foobarObject.omgfoo = 5; + top.foobarObject.strfoo = "foobarz"; + top.foobarObject.omgstr = "foobarz" + + (new Array(DebuggerServer.LONG_STRING_LENGTH * 2)).join("abb"); + + top.largeObject1 = Object.create(null); + for (let i = 0; i < MAX_AUTOCOMPLETE_ATTEMPTS + 1; i++) { + top.largeObject1['a' + i] = i; + } + + top.largeObject2 = Object.create(null); + for (let i = 0; i < MAX_AUTOCOMPLETIONS * 2; i++) { + top.largeObject2['a' + i] = i; + } + + gState = aState; + + let tests = [doSimpleEval, doWindowEval, doEvalWithException, + doEvalWithHelper, doEvalString, doEvalLongString, + doEvalWithBinding, doEvalWithBindingFrame, + forceLexicalInit].map(t => { + return Task.async(t); + }); + + runTests(tests, testEnd); +} + +function* doSimpleEval() { + info("test eval '2+2'"); + let response = yield evaluateJS("2+2"); + checkObject(response, { + from: gState.actor, + input: "2+2", + result: 4, + }); + + ok(!response.exception, "no eval exception"); + ok(!response.helperResult, "no helper result"); + + nextTest(); +} + +function* doWindowEval() { + info("test eval 'document'"); + let response = yield evaluateJS("document"); + checkObject(response, { + from: gState.actor, + input: "document", + result: { + type: "object", + class: "XULDocument", + actor: /[a-z]/, + }, + }); + + ok(!response.exception, "no eval exception"); + ok(!response.helperResult, "no helper result"); + + nextTest(); +} + +function* doEvalWithException() { + info("test eval with exception"); + let response = yield evaluateJS("window.doTheImpossible()"); + checkObject(response, { + from: gState.actor, + input: "window.doTheImpossible()", + result: { + type: "undefined", + }, + exceptionMessage: /doTheImpossible/, + }); + + ok(response.exception, "js eval exception"); + ok(!response.helperResult, "no helper result"); + + nextTest(); +} + +function* doEvalWithHelper() { + info("test eval with helper"); + let response = yield evaluateJS("clear()"); + checkObject(response, { + from: gState.actor, + input: "clear()", + result: { + type: "undefined", + }, + helperResult: { type: "clearOutput" }, + }); + + ok(!response.exception, "no eval exception"); + + nextTest(); +} + +function* doEvalString() { + let response = yield evaluateJS("window.foobarObject.strfoo"); + checkObject(response, { + from: gState.actor, + input: "window.foobarObject.strfoo", + result: "foobarz", + }); + + nextTest(); +} + +function* doEvalLongString() { + let response = yield evaluateJS("window.foobarObject.omgstr"); + let str = top.foobarObject.omgstr; + let initial = str.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH); + + checkObject(response, { + from: gState.actor, + input: "window.foobarObject.omgstr", + result: { + type: "longString", + initial: initial, + length: str.length, + }, + }); + + nextTest(); +} + +function* doEvalWithBinding() { + let response = yield evaluateJS("document;"); + let documentActor = response.result.actor; + + info("running a command with _self as document using bindObjectActor"); + let bindObjectSame = yield evaluateJS("_self === document", { + bindObjectActor: documentActor + }); + checkObject(bindObjectSame, { + result: true + }); + + info("running a command with _self as document using selectedObjectActor"); + let selectedObjectSame = yield evaluateJS("_self === document", { + selectedObjectActor: documentActor + }); + checkObject(selectedObjectSame, { + result: true + }); + + nextTest(); +} + +function* doEvalWithBindingFrame() { + let frameWin = top.document.querySelector("iframe").contentWindow; + frameWin.fooFrame = { bar: 1 }; + + let response = yield evaluateJS( + "document.querySelector('iframe').contentWindow.fooFrame" + ); + let iframeObjectActor = response.result.actor; + ok(iframeObjectActor, "There is an actor associated with the response"); + + let bindObjectGlobal = yield evaluateJS("this.temp0 = _self;", { + bindObjectActor: iframeObjectActor + }); + ok(!top.temp0, + "Global doesn't match the top global with bindObjectActor"); + ok(frameWin.temp0 && frameWin.temp0.bar === 1, + "Global matches the object's global with bindObjectActor"); + + let selectedObjectGlobal = yield evaluateJS("this.temp1 = _self;", { + selectedObjectActor: iframeObjectActor + }); + ok(top.temp1 && top.temp1.bar === 1, + "Global matches the top global with bindObjectActor"); + ok(!frameWin.temp1, + "Global doesn't match the object's global with bindObjectActor"); + + nextTest() +} + +function* forceLexicalInit() { + info("test that failed let/const bindings are initialized to undefined"); + + const testData = [ + { + stmt: "let foopie = wubbalubadubdub", + vars: ["foopie"] + }, + { + stmt: "let {z, w={n}=null} = {}", + vars: ["z", "w"] + }, + { + stmt: "let [a, b, c] = null", + vars: ["a", "b", "c"] + }, + { + stmt: "const nein1 = rofl, nein2 = copter", + vars: ["nein1", "nein2"] + }, + { + stmt: "const {ha} = null", + vars: ["ha"] + }, + { + stmt: "const [haw=[lame]=null] = []", + vars: ["haw"] + }, + { + stmt: "const [rawr, wat=[lame]=null] = []", + vars: ["rawr", "haw"] + }, + { + stmt: "let {zzz: xyz=99, zwz: wb} = nexistepas()", + vars: ["xyz", "wb"] + }, + { + stmt: "let {c3pdoh=101} = null", + vars: ["c3pdoh"] + } + ]; + + for (let data of testData) { + let response = yield evaluateJS(data.stmt); + checkObject(response, { + from: gState.actor, + input: data.stmt, + result: undefined, + }); + ok(response.exception, "expected exception"); + for (let varName of data.vars) { + let response2 = yield evaluateJS(varName); + checkObject(response2, { + from: gState.actor, + input: varName, + result: undefined, + }); + ok(!response2.exception, "unexpected exception"); + } + } + + nextTest(); +} + +function testEnd() +{ + // If this is the first run, reload the page and do it again. + // Otherwise, end the test. + closeDebugger(gState, function() { + gState = null; + if (evaluatingSync) { + evaluatingSync = false; + startTest(); + } else { + SimpleTest.finish(); + } + }); +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_jsterm_autocomplete.html b/devtools/shared/webconsole/test/test_jsterm_autocomplete.html new file mode 100644 index 000000000..0e32e1a63 --- /dev/null +++ b/devtools/shared/webconsole/test/test_jsterm_autocomplete.html @@ -0,0 +1,183 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for JavaScript terminal functionality</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for JavaScript terminal autocomplete functionality</p> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +let gState; +let {MAX_AUTOCOMPLETE_ATTEMPTS,MAX_AUTOCOMPLETIONS} = require("devtools/shared/webconsole/js-property-provider"); + +function evaluateJS(input, options = {}) { + return new Promise((resolve, reject) => { + gState.client.evaluateJSAsync(input, resolve, options); + }); +} + +function autocompletePromise(str, cursor, frameActor) { + return new Promise(resolve => { + gState.client.autocomplete(str, cursor, resolve, frameActor); + }); +} + +// This test runs all of its assertions twice - once with +// the tab as a target and once with a worker +let runningInTab = true; +function startTest({worker}) { + if (worker) { + attachConsoleToWorker(["PageError"], onAttach); + } else { + attachConsoleToTab(["PageError"], onAttach); + } +}; + +let onAttach = Task.async(function*(aState, response) { + gState = aState; + + let longStrLength = DebuggerServer.LONG_STRING_LENGTH; + + // Set up the global variables needed to test autocompletion + // in the target. + let script = ` + // This is for workers so autocomplete acts the same + if (!this.window) { + window = this; + } + + window.foobarObject = Object.create(null); + window.foobarObject.foo = 1; + window.foobarObject.foobar = 2; + window.foobarObject.foobaz = 3; + window.foobarObject.omg = 4; + window.foobarObject.omgfoo = 5; + window.foobarObject.strfoo = "foobarz"; + window.foobarObject.omgstr = "foobarz" + + (new Array(${longStrLength})).join("abb"); + window.largeObject1 = Object.create(null); + for (let i = 0; i < ${MAX_AUTOCOMPLETE_ATTEMPTS + 1}; i++) { + window.largeObject1['a' + i] = i; + } + + window.largeObject2 = Object.create(null); + for (let i = 0; i < ${MAX_AUTOCOMPLETIONS * 2}; i++) { + window.largeObject2['a' + i] = i; + } + `; + + yield evaluateJS(script); + + let tests = [doAutocomplete1, doAutocomplete2, doAutocomplete3, + doAutocomplete4, doAutocompleteLarge1, + doAutocompleteLarge2].map(t => { + return Task.async(t); + }); + + runTests(tests, testEnd); +}); + +function* doAutocomplete1() { + info("test autocomplete for 'window.foo'"); + let response = yield autocompletePromise("window.foo", 10); + let matches = response.matches; + + is(response.matchProp, "foo", "matchProp"); + is(matches.length, 1, "matches.length"); + is(matches[0], "foobarObject", "matches[0]"); + + nextTest(); +} + +function* doAutocomplete2() { + info("test autocomplete for 'window.foobarObject.'"); + let response = yield autocompletePromise("window.foobarObject.", 20); + let matches = response.matches; + + ok(!response.matchProp, "matchProp"); + is(matches.length, 7, "matches.length"); + checkObject(matches, + ["foo", "foobar", "foobaz", "omg", "omgfoo", "omgstr", "strfoo"]); + + nextTest(); +} + +function* doAutocomplete3() { + // Check that completion suggestions are offered inside the string. + info("test autocomplete for 'dump(window.foobarObject.)'"); + let response = yield autocompletePromise("dump(window.foobarObject.)", 25); + let matches = response.matches; + + ok(!response.matchProp, "matchProp"); + is(matches.length, 7, "matches.length"); + checkObject(matches, + ["foo", "foobar", "foobaz", "omg", "omgfoo", "omgstr", "strfoo"]); + + nextTest(); +} + +function* doAutocomplete4() { + // Check that completion requests can have no suggestions. + info("test autocomplete for 'dump(window.foobarObject.)'"); + let response = yield autocompletePromise("dump(window.foobarObject.)", 26); + ok(!response.matchProp, "matchProp"); + is(response.matches.length, 0, "matches.length"); + + nextTest(); +} + +function* doAutocompleteLarge1() { + // Check that completion requests with too large objects will + // have no suggestions. + info("test autocomplete for 'window.largeObject1.'"); + let response = yield autocompletePromise("window.largeObject1.", 20); + ok(!response.matchProp, "matchProp"); + info (response.matches.join("|")); + is(response.matches.length, 0, "Bailed out with too many properties"); + + nextTest(); +} + +function* doAutocompleteLarge2() { + // Check that completion requests with pretty large objects will + // have MAX_AUTOCOMPLETIONS suggestions + info("test autocomplete for 'window.largeObject2.'"); + let response = yield autocompletePromise("window.largeObject2.", 20); + ok(!response.matchProp, "matchProp"); + is(response.matches.length, MAX_AUTOCOMPLETIONS, "matches.length is MAX_AUTOCOMPLETIONS"); + + nextTest(); +} + +function testEnd() +{ + // If this is the first run, reload the page and do it again + // in a worker. Otherwise, end the test. + closeDebugger(gState, function() { + gState = null; + if (runningInTab) { + runningInTab = false; + startTest({ + worker: true + }); + } else { + SimpleTest.finish(); + } + }); +} + +addEventListener("load", () => { + startTest({ + worker: false + }); +}); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_jsterm_cd_iframe.html b/devtools/shared/webconsole/test/test_jsterm_cd_iframe.html new file mode 100644 index 000000000..7207a00a1 --- /dev/null +++ b/devtools/shared/webconsole/test/test_jsterm_cd_iframe.html @@ -0,0 +1,223 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for the cd() function</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for the cd() function</p> + +<iframe id="content-iframe" src="http://example.com/chrome/devtools/shared/webconsole/test/sandboxed_iframe.html"></iframe> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +let gState; + +function startTest() +{ + removeEventListener("load", startTest); + + attachConsoleToTab([], onAttach); +} + +function onAttach(aState, aResponse) +{ + top.foobarObject = Object.create(null); + top.foobarObject.bug609872 = "parent"; + + window.foobarObject = Object.create(null); + window.foobarObject.bug609872 = "child"; + + gState = aState; + + let tests = [doCheckParent, doCdIframe, doCheckIframe, + doCdContentIframe, + doCdSandboxedIframe, doCheckSandboxedIframe, + doCdParent, + doCdParent, + doCheckParent2]; + runTests(tests, testEnd); +} + +function doCheckParent() +{ + info("check parent window"); + gState.client.evaluateJS("window.foobarObject.bug609872", + onFooObjectFromParent); +} + +function onFooObjectFromParent(aResponse) +{ + checkObject(aResponse, { + from: gState.actor, + input: "window.foobarObject.bug609872", + result: "parent", + }); + + ok(!aResponse.exception, "no eval exception"); + ok(!aResponse.helperResult, "no helper result"); + + nextTest(); +} + +function doCdIframe() +{ + info("test cd('iframe')"); + gState.client.evaluateJS("cd('iframe')", onCdIframe); +} + +function onCdIframe(aResponse) +{ + checkObject(aResponse, { + from: gState.actor, + input: "cd('iframe')", + result: { type: "undefined" }, + helperResult: { type: "cd" }, + }); + + ok(!aResponse.exception, "no eval exception"); + + nextTest(); +} + +function doCheckIframe() +{ + info("check foobarObject from the iframe"); + gState.client.evaluateJS("window.foobarObject.bug609872", + onFooObjectFromIframe); +} + +function onFooObjectFromIframe(aResponse) +{ + checkObject(aResponse, { + from: gState.actor, + input: "window.foobarObject.bug609872", + result: "child", + }); + + ok(!aResponse.exception, "no js eval exception"); + ok(!aResponse.helperResult, "no helper result"); + + nextTest(); +} + +function doCdContentIframe() +{ + info("test cd('#content-iframe')"); + gState.client.evaluateJS("cd('#content-iframe')", onCdContentIframe); +} + +function onCdContentIframe(aResponse) +{ + checkObject(aResponse, { + from: gState.actor, + input: "cd('#content-iframe')", + result: { type: "undefined" }, + helperResult: { type: "cd" }, + }); + + ok(!aResponse.exception, "no eval exception"); + + nextTest(); +} +function doCdSandboxedIframe() +{ + // Don't use string to ensure we don't get security exception + // when passing a content window reference. + let cmd = "cd(document.getElementById('sandboxed-iframe').contentWindow)"; + info("test " + cmd); + gState.client.evaluateJS(cmd, onCdSandboxedIframe.bind(null, cmd)); +} + +function onCdSandboxedIframe(cmd, aResponse) +{ + checkObject(aResponse, { + from: gState.actor, + input: cmd, + result: { type: "undefined" }, + helperResult: { type: "cd" }, + }); + + ok(!aResponse.exception, "no eval exception"); + + nextTest(); +} + +function doCheckSandboxedIframe() +{ + info("check foobarObject from the sandboxed iframe"); + gState.client.evaluateJS("window.foobarObject.bug1051224", + onFooObjectFromSandboxedIframe); +} + +function onFooObjectFromSandboxedIframe(aResponse) +{ + checkObject(aResponse, { + from: gState.actor, + input: "window.foobarObject.bug1051224", + result: "sandboxed", + }); + + ok(!aResponse.exception, "no js eval exception"); + ok(!aResponse.helperResult, "no helper result"); + + nextTest(); +} + +function doCdParent() +{ + info("test cd() back to parent"); + gState.client.evaluateJS("cd()", onCdParent); +} + +function onCdParent(aResponse) +{ + checkObject(aResponse, { + from: gState.actor, + input: "cd()", + result: { type: "undefined" }, + helperResult: { type: "cd" }, + }); + + ok(!aResponse.exception, "no eval exception"); + + nextTest(); +} + +function doCheckParent2() +{ + gState.client.evaluateJS("window.foobarObject.bug609872", + onFooObjectFromParent2); +} + +function onFooObjectFromParent2(aResponse) +{ + checkObject(aResponse, { + from: gState.actor, + input: "window.foobarObject.bug609872", + result: "parent", + }); + + ok(!aResponse.exception, "no eval exception"); + ok(!aResponse.helperResult, "no helper result"); + + nextTest(); +} + +function testEnd() +{ + closeDebugger(gState, function() { + gState = null; + SimpleTest.finish(); + }); +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_jsterm_last_result.html b/devtools/shared/webconsole/test/test_jsterm_last_result.html new file mode 100644 index 000000000..e90ce14ed --- /dev/null +++ b/devtools/shared/webconsole/test/test_jsterm_last_result.html @@ -0,0 +1,130 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for the $_ getter</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for the $_ getter</p> + +<iframe id="content-iframe" src="http://example.com/chrome/devtools/shared/webconsole/test/sandboxed_iframe.html"></iframe> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); +let gState; + +function evaluateJS(input, callback) { + return new Promise((resolve, reject) => { + gState.client.evaluateJSAsync(input, response => { + if (callback) { + callback(response); + } + resolve(response); + }); + }); +} + +function startTest() +{ + removeEventListener("load", startTest); + attachConsoleToTab([], state => { + gState = state; + let tests = [checkUndefinedResult,checkAdditionResult,checkObjectResult]; + runTests(tests, testEnd); + }); +} + +let checkUndefinedResult = Task.async(function*() { + info ("$_ returns undefined if nothing has evaluated yet"); + let response = yield evaluateJS("$_"); + basicResultCheck(response, "$_", undefined); + nextTest(); +}); + +let checkAdditionResult = Task.async(function*() { + info ("$_ returns last value and performs basic arithmetic"); + let response = yield evaluateJS("2+2"); + basicResultCheck(response, "2+2", 4); + + response = yield evaluateJS("$_"); + basicResultCheck(response, "$_", 4); + + response = yield evaluateJS("$_ + 2"); + basicResultCheck(response, "$_ + 2", 6); + + response = yield evaluateJS("$_ + 4"); + basicResultCheck(response, "$_ + 4", 10); + + nextTest(); +}); + +let checkObjectResult = Task.async(function*() { + info ("$_ has correct references to objects"); + + let response = yield evaluateJS("var foo = {bar:1}; foo;"); + basicResultCheck(response, "var foo = {bar:1}; foo;", { + type: "object", + class: "Object", + actor: /[a-z]/, + }); + checkObject(response.result.preview.ownProperties, { + bar: { + value: 1 + } + }); + + response = yield evaluateJS("$_"); + basicResultCheck(response, "$_", { + type: "object", + class: "Object", + actor: /[a-z]/, + }); + checkObject(response.result.preview.ownProperties, { + bar: { + value: 1 + } + }); + + top.foo.bar = 2; + + response = yield evaluateJS("$_"); + basicResultCheck(response, "$_", { + type: "object", + class: "Object", + actor: /[a-z]/, + }); + checkObject(response.result.preview.ownProperties, { + bar: { + value: 2 + } + }); + + nextTest(); +}); + +function basicResultCheck(response, input, output) { + checkObject(response, { + from: gState.actor, + input: input, + result: output, + }); + ok(!response.exception, "no eval exception"); + ok(!response.helperResult, "no helper result"); +} + +function testEnd() +{ + closeDebugger(gState, function() { + gState = null; + SimpleTest.finish(); + }); +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_jsterm_queryselector.html b/devtools/shared/webconsole/test/test_jsterm_queryselector.html new file mode 100644 index 000000000..b75ee399c --- /dev/null +++ b/devtools/shared/webconsole/test/test_jsterm_queryselector.html @@ -0,0 +1,134 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for the querySelector / querySelectorAll helpers</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for the querySelector / querySelectorAll helpers</p> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); +let gState; +let gWin; + +function evaluateJS(input) { + return new Promise((resolve) => gState.client.evaluateJS(input, resolve)); +} + +function startTest() { + info ("Content window opened, attaching console to it"); + + let systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal); + ok (!gWin.document.nodePrincipal.equals(systemPrincipal), + "The test document is not using the system principal"); + + attachConsoleToTab([], state => { + gState = state; + let tests = [ + setupWindow, + checkQuerySelector, + checkQuerySelectorAll, + checkQuerySelectorAllNotExist, + checkQuerySelectorAllException + ]; + runTests(tests, testEnd); + }); +} + +let setupWindow = Task.async(function*() { + info ("Shimming window functions for the content privileged tab"); + yield evaluateJS("document.querySelector = function() { throw 'should not call qS'; }"); + yield evaluateJS("document.querySelectorAll = function() { throw 'should not call qSA'; }"); + nextTest(); +}); + +let checkQuerySelector = Task.async(function*() { + info ("$ returns an DOMNode"); + let response = yield evaluateJS("$('body')"); + basicResultCheck(response, "$('body')", { + type: "object", + class: "HTMLBodyElement", + preview: { + kind: "DOMNode", + nodeName: "body" + } + }); + nextTest(); +}); + +let checkQuerySelectorAll = Task.async(function*() { + info ("$$ returns an array"); + let response = yield evaluateJS("$$('body')"); + basicResultCheck(response, "$$('body')", { + type: "object", + class: "Array", + preview: { + length: 1 + } + }); + nextTest(); +}); + +let checkQuerySelectorAllNotExist = Task.async(function*() { + info ("$$ returns an array even if query yields no results"); + let response = yield evaluateJS("$$('foobar')"); + basicResultCheck(response, "$$('foobar')", { + type: "object", + class: "Array", + preview: { + length: 0 + } + }); + nextTest(); +}); + +let checkQuerySelectorAllException = Task.async(function*() { + info ("$$ returns an exception if an invalid selector was provided"); + let response = yield evaluateJS("$$(':foo')"); + checkObject(response, { + input: "$$(':foo')", + exceptionMessage: "SyntaxError: ':foo' is not a valid selector", + exception: { + preview: { + kind: "DOMException", + name: "SyntaxError", + message: "':foo' is not a valid selector" + } + } + }); + nextTest(); +}); + +function basicResultCheck(response, input, output) { + checkObject(response, { + from: gState.actor, + input: input, + result: output, + }); + ok(!response.exception, "no eval exception"); + ok(!response.helperResult, "no helper result"); +} + +function testEnd() { + gWin.close(); + gWin = null; + closeDebugger(gState, function() { + gState = null; + SimpleTest.finish(); + }); +} + +window.onload = function() { + // Open a content window to test XRay functionality on built in functions. + gWin = window.open("data:text/html,"); + info ("Waiting for content window to load"); + gWin.onload = startTest; +} +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_network_get.html b/devtools/shared/webconsole/test/test_network_get.html new file mode 100644 index 000000000..710c9b0d7 --- /dev/null +++ b/devtools/shared/webconsole/test/test_network_get.html @@ -0,0 +1,260 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for the network actor (GET request)</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for the network actor (GET request)</p> + +<iframe src="http://example.com/chrome/devtools/shared/webconsole/test/network_requests_iframe.html"></iframe> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +function startTest() +{ + removeEventListener("load", startTest); + attachConsoleToTab(["NetworkActivity"], onAttach); +} + +function onAttach(aState, aResponse) +{ + info("test network GET request"); + + onNetworkEvent = onNetworkEvent.bind(null, aState); + aState.dbgClient.addListener("networkEvent", onNetworkEvent); + onNetworkEventUpdate = onNetworkEventUpdate.bind(null, aState); + aState.dbgClient.addListener("networkEventUpdate", onNetworkEventUpdate); + + let iframe = document.querySelector("iframe").contentWindow; + iframe.wrappedJSObject.testXhrGet(); +} + +function onNetworkEvent(aState, aType, aPacket) +{ + is(aPacket.from, aState.actor, "network event actor"); + + info("checking the network event packet"); + + let netActor = aPacket.eventActor; + + checkObject(netActor, { + actor: /[a-z]/, + startedDateTime: /^\d+\-\d+\-\d+T.+$/, + url: /data\.json/, + method: "GET", + }); + + aState.netActor = netActor.actor; + + aState.dbgClient.removeListener("networkEvent", onNetworkEvent); +} + +let updates = []; + +function onNetworkEventUpdate(aState, aType, aPacket) +{ + info("received networkEventUpdate " + aPacket.updateType); + is(aPacket.from, aState.netActor, "networkEventUpdate actor"); + + updates.push(aPacket.updateType); + + let expectedPacket = null; + + switch (aPacket.updateType) { + case "requestHeaders": + case "responseHeaders": + ok(aPacket.headers > 0, "headers > 0"); + ok(aPacket.headersSize > 0, "headersSize > 0"); + break; + case "requestCookies": + expectedPacket = { + cookies: 3, + }; + break; + case "requestPostData": + ok(false, "got unexpected requestPostData"); + break; + case "responseStart": + expectedPacket = { + response: { + httpVersion: /^HTTP\/\d\.\d$/, + status: "200", + statusText: "OK", + headersSize: /^\d+$/, + discardResponseBody: false, + }, + }; + break; + case "securityInfo": + expectedPacket = { + state: "insecure", + }; + break; + case "responseCookies": + expectedPacket = { + cookies: 0, + }; + break; + case "responseContent": + expectedPacket = { + mimeType: "application/json", + contentSize: 1070, + discardResponseBody: false, + }; + break; + case "eventTimings": + expectedPacket = { + totalTime: /^\d+$/, + }; + break; + default: + ok(false, "unknown network event update type: " + + aPacket.updateType); + return; + } + + if (expectedPacket) { + info("checking the packet content"); + checkObject(aPacket, expectedPacket); + } + + if (updates.indexOf("responseContent") > -1 && + updates.indexOf("eventTimings") > -1) { + aState.dbgClient.removeListener("networkEventUpdate", + onNetworkEvent); + + onRequestHeaders = onRequestHeaders.bind(null, aState); + aState.client.getRequestHeaders(aState.netActor, + onRequestHeaders); + } +} + +function onRequestHeaders(aState, aResponse) +{ + info("checking request headers"); + + ok(aResponse.headers.length > 0, "request headers > 0"); + ok(aResponse.headersSize > 0, "request headersSize > 0"); + ok(!!aResponse.rawHeaders, "request rawHeaders available"); + + checkHeadersOrCookies(aResponse.headers, { + Referer: /network_requests_iframe\.html/, + Cookie: /bug768096/, + }); + + checkRawHeaders(aResponse.rawHeaders, { + Referer: /network_requests_iframe\.html/, + Cookie: /bug768096/, + }); + + onRequestCookies = onRequestCookies.bind(null, aState); + aState.client.getRequestCookies(aState.netActor, + onRequestCookies); +} + +function onRequestCookies(aState, aResponse) +{ + info("checking request cookies"); + + is(aResponse.cookies.length, 3, "request cookies length"); + + checkHeadersOrCookies(aResponse.cookies, { + foobar: "fooval", + omgfoo: "bug768096", + badcookie: "bug826798=st3fan", + }); + + onRequestPostData = onRequestPostData.bind(null, aState); + aState.client.getRequestPostData(aState.netActor, + onRequestPostData); +} + +function onRequestPostData(aState, aResponse) +{ + info("checking request POST data"); + + ok(!aResponse.postData.text, "no request POST data"); + ok(!aResponse.postDataDiscarded, "request POST data was not discarded"); + + onResponseHeaders = onResponseHeaders.bind(null, aState); + aState.client.getResponseHeaders(aState.netActor, + onResponseHeaders); +} + +function onResponseHeaders(aState, aResponse) +{ + info("checking response headers"); + + ok(aResponse.headers.length > 0, "response headers > 0"); + ok(aResponse.headersSize > 0, "response headersSize > 0"); + ok(!!aResponse.rawHeaders, "response rawHeaders available"); + + checkHeadersOrCookies(aResponse.headers, { + "Content-Type": /^application\/(json|octet-stream)$/, + "Content-Length": /^\d+$/, + }); + + checkRawHeaders(aResponse.rawHeaders, { + "Content-Type": /^application\/(json|octet-stream)$/, + "Content-Length": /^\d+$/, + }); + + onResponseCookies = onResponseCookies.bind(null, aState); + aState.client.getResponseCookies(aState.netActor, + onResponseCookies); +} + +function onResponseCookies(aState, aResponse) +{ + info("checking response cookies"); + + is(aResponse.cookies.length, 0, "response cookies length"); + + onResponseContent = onResponseContent.bind(null, aState); + aState.client.getResponseContent(aState.netActor, + onResponseContent); +} + +function onResponseContent(aState, aResponse) +{ + info("checking response content"); + + ok(aResponse.content.text, "response content text"); + ok(!aResponse.contentDiscarded, "response content was not discarded"); + + onEventTimings = onEventTimings.bind(null, aState); + aState.client.getEventTimings(aState.netActor, + onEventTimings); +} + +function onEventTimings(aState, aResponse) +{ + info("checking event timings"); + + checkObject(aResponse, { + timings: { + blocked: /^-1|\d+$/, + dns: /^-1|\d+$/, + connect: /^-1|\d+$/, + send: /^-1|\d+$/, + wait: /^-1|\d+$/, + receive: /^-1|\d+$/, + }, + totalTime: /^\d+$/, + }); + + closeDebugger(aState, function() { + SimpleTest.finish(); + }); +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_network_longstring.html b/devtools/shared/webconsole/test/test_network_longstring.html new file mode 100644 index 000000000..d55136896 --- /dev/null +++ b/devtools/shared/webconsole/test/test_network_longstring.html @@ -0,0 +1,293 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test that the network actor uses the LongStringActor</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test that the network actor uses the LongStringActor</p> + +<iframe src="http://example.com/chrome/devtools/shared/webconsole/test/network_requests_iframe.html"></iframe> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +function startTest() +{ + removeEventListener("load", startTest); + + attachConsoleToTab(["NetworkActivity"], onAttach); +} + +function onAttach(aState, aResponse) +{ + info("set long string length"); + + window.ORIGINAL_LONG_STRING_LENGTH = DebuggerServer.LONG_STRING_LENGTH; + window.ORIGINAL_LONG_STRING_INITIAL_LENGTH = + DebuggerServer.LONG_STRING_INITIAL_LENGTH; + + DebuggerServer.LONG_STRING_LENGTH = 400; + DebuggerServer.LONG_STRING_INITIAL_LENGTH = 400; + + info("test network POST request"); + + onNetworkEvent = onNetworkEvent.bind(null, aState); + aState.dbgClient.addListener("networkEvent", onNetworkEvent); + onNetworkEventUpdate = onNetworkEventUpdate.bind(null, aState); + aState.dbgClient.addListener("networkEventUpdate", onNetworkEventUpdate); + + let iframe = document.querySelector("iframe").contentWindow; + iframe.wrappedJSObject.testXhrPost(); +} + +function onNetworkEvent(aState, aType, aPacket) +{ + is(aPacket.from, aState.actor, "network event actor"); + + info("checking the network event packet"); + + let netActor = aPacket.eventActor; + + checkObject(netActor, { + actor: /[a-z]/, + startedDateTime: /^\d+\-\d+\-\d+T.+$/, + url: /data\.json/, + method: "POST", + }); + + aState.netActor = netActor.actor; + + aState.dbgClient.removeListener("networkEvent", onNetworkEvent); +} + +let updates = []; + +function onNetworkEventUpdate(aState, aType, aPacket) +{ + info("received networkEventUpdate " + aPacket.updateType); + is(aPacket.from, aState.netActor, "networkEventUpdate actor"); + + updates.push(aPacket.updateType); + + let expectedPacket = null; + + switch (aPacket.updateType) { + case "requestHeaders": + case "responseHeaders": + ok(aPacket.headers > 0, "headers > 0"); + ok(aPacket.headersSize > 0, "headersSize > 0"); + break; + case "requestCookies": + expectedPacket = { + cookies: 3, + }; + break; + case "requestPostData": + ok(aPacket.dataSize > 0, "dataSize > 0"); + ok(!aPacket.discardRequestBody, "discardRequestBody"); + break; + case "responseStart": + expectedPacket = { + response: { + httpVersion: /^HTTP\/\d\.\d$/, + status: "200", + statusText: "OK", + headersSize: /^\d+$/, + discardResponseBody: false, + }, + }; + break; + case "securityInfo": + expectedPacket = { + state: "insecure", + }; + break; + case "responseCookies": + expectedPacket = { + cookies: 0, + }; + break; + case "responseContent": + expectedPacket = { + mimeType: "application/json", + contentSize: /^\d+$/, + discardResponseBody: false, + }; + break; + case "eventTimings": + expectedPacket = { + totalTime: /^\d+$/, + }; + break; + default: + ok(false, "unknown network event update type: " + + aPacket.updateType); + return; + } + + if (expectedPacket) { + info("checking the packet content"); + checkObject(aPacket, expectedPacket); + } + + if (updates.indexOf("responseContent") > -1 && + updates.indexOf("eventTimings") > -1) { + aState.dbgClient.removeListener("networkEventUpdate", + onNetworkEvent); + + onRequestHeaders = onRequestHeaders.bind(null, aState); + aState.client.getRequestHeaders(aState.netActor, + onRequestHeaders); + } +} + +function onRequestHeaders(aState, aResponse) +{ + info("checking request headers"); + + ok(aResponse.headers.length > 0, "request headers > 0"); + ok(aResponse.headersSize > 0, "request headersSize > 0"); + + checkHeadersOrCookies(aResponse.headers, { + Referer: /network_requests_iframe\.html/, + Cookie: /bug768096/, + }); + + onRequestCookies = onRequestCookies.bind(null, aState); + aState.client.getRequestCookies(aState.netActor, + onRequestCookies); +} + +function onRequestCookies(aState, aResponse) +{ + info("checking request cookies"); + + is(aResponse.cookies.length, 3, "request cookies length"); + + checkHeadersOrCookies(aResponse.cookies, { + foobar: "fooval", + omgfoo: "bug768096", + badcookie: "bug826798=st3fan", + }); + + onRequestPostData = onRequestPostData.bind(null, aState); + aState.client.getRequestPostData(aState.netActor, + onRequestPostData); +} + +function onRequestPostData(aState, aResponse) +{ + info("checking request POST data"); + + checkObject(aResponse, { + postData: { + text: { + type: "longString", + initial: /^Hello world! foobaz barr.+foobaz barrfo$/, + length: 552, + actor: /[a-z]/, + }, + }, + postDataDiscarded: false, + }); + + is(aResponse.postData.text.initial.length, + DebuggerServer.LONG_STRING_INITIAL_LENGTH, "postData text initial length"); + + onResponseHeaders = onResponseHeaders.bind(null, aState); + aState.client.getResponseHeaders(aState.netActor, + onResponseHeaders); +} + +function onResponseHeaders(aState, aResponse) +{ + info("checking response headers"); + + ok(aResponse.headers.length > 0, "response headers > 0"); + ok(aResponse.headersSize > 0, "response headersSize > 0"); + + checkHeadersOrCookies(aResponse.headers, { + "Content-Type": /^application\/(json|octet-stream)$/, + "Content-Length": /^\d+$/, + "x-very-short": "hello world", + "x-very-long": { + "type": "longString", + "length": 521, + "initial": /^Lorem ipsum.+\. Donec vitae d$/, + "actor": /[a-z]/, + }, + }); + + onResponseCookies = onResponseCookies.bind(null, aState); + aState.client.getResponseCookies(aState.netActor, + onResponseCookies); +} + +function onResponseCookies(aState, aResponse) +{ + info("checking response cookies"); + + is(aResponse.cookies.length, 0, "response cookies length"); + + onResponseContent = onResponseContent.bind(null, aState); + aState.client.getResponseContent(aState.netActor, + onResponseContent); +} + +function onResponseContent(aState, aResponse) +{ + info("checking response content"); + + checkObject(aResponse, { + content: { + text: { + type: "longString", + initial: /^\{ id: "test JSON data"(.|\r|\n)+ barfoo ba$/g, + length: 1070, + actor: /[a-z]/, + }, + }, + contentDiscarded: false, + }); + + is(aResponse.content.text.initial.length, + DebuggerServer.LONG_STRING_INITIAL_LENGTH, "content initial length"); + + onEventTimings = onEventTimings.bind(null, aState); + aState.client.getEventTimings(aState.netActor, + onEventTimings); +} + +function onEventTimings(aState, aResponse) +{ + info("checking event timings"); + + checkObject(aResponse, { + timings: { + blocked: /^-1|\d+$/, + dns: /^-1|\d+$/, + connect: /^-1|\d+$/, + send: /^-1|\d+$/, + wait: /^-1|\d+$/, + receive: /^-1|\d+$/, + }, + totalTime: /^\d+$/, + }); + + closeDebugger(aState, function() { + DebuggerServer.LONG_STRING_LENGTH = ORIGINAL_LONG_STRING_LENGTH; + DebuggerServer.LONG_STRING_INITIAL_LENGTH = ORIGINAL_LONG_STRING_INITIAL_LENGTH; + + SimpleTest.finish(); + }); +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_network_post.html b/devtools/shared/webconsole/test/test_network_post.html new file mode 100644 index 000000000..d96b9b0b7 --- /dev/null +++ b/devtools/shared/webconsole/test/test_network_post.html @@ -0,0 +1,272 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for the network actor (POST request)</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for the network actor (POST request)</p> + +<iframe src="http://example.com/chrome/devtools/shared/webconsole/test/network_requests_iframe.html"></iframe> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +function startTest() +{ + removeEventListener("load", startTest); + + attachConsoleToTab(["NetworkActivity"], onAttach); +} + +function onAttach(aState, aResponse) +{ + info("test network POST request"); + + onNetworkEvent = onNetworkEvent.bind(null, aState); + aState.dbgClient.addListener("networkEvent", onNetworkEvent); + onNetworkEventUpdate = onNetworkEventUpdate.bind(null, aState); + aState.dbgClient.addListener("networkEventUpdate", onNetworkEventUpdate); + + let iframe = document.querySelector("iframe").contentWindow; + iframe.wrappedJSObject.testXhrPost(); +} + +function onNetworkEvent(aState, aType, aPacket) +{ + is(aPacket.from, aState.actor, "network event actor"); + + info("checking the network event packet"); + + let netActor = aPacket.eventActor; + + checkObject(netActor, { + actor: /[a-z]/, + startedDateTime: /^\d+\-\d+\-\d+T.+$/, + url: /data\.json/, + method: "POST", + }); + + aState.netActor = netActor.actor; + + aState.dbgClient.removeListener("networkEvent", onNetworkEvent); +} + +let updates = []; + +function onNetworkEventUpdate(aState, aType, aPacket) +{ + info("received networkEventUpdate " + aPacket.updateType); + is(aPacket.from, aState.netActor, "networkEventUpdate actor"); + + updates.push(aPacket.updateType); + + let expectedPacket = null; + + switch (aPacket.updateType) { + case "requestHeaders": + case "responseHeaders": + ok(aPacket.headers > 0, "headers > 0"); + ok(aPacket.headersSize > 0, "headersSize > 0"); + break; + case "requestCookies": + expectedPacket = { + cookies: 3, + }; + break; + case "requestPostData": + ok(aPacket.dataSize > 0, "dataSize > 0"); + ok(!aPacket.discardRequestBody, "discardRequestBody"); + break; + case "responseStart": + expectedPacket = { + response: { + httpVersion: /^HTTP\/\d\.\d$/, + status: "200", + statusText: "OK", + headersSize: /^\d+$/, + discardResponseBody: false, + }, + }; + break; + case "securityInfo": + expectedPacket = { + state: "insecure", + }; + break; + case "responseCookies": + expectedPacket = { + cookies: 0, + }; + break; + case "responseContent": + expectedPacket = { + mimeType: "application/json", + contentSize: /^\d+$/, + discardResponseBody: false, + }; + break; + case "eventTimings": + expectedPacket = { + totalTime: /^\d+$/, + }; + break; + default: + ok(false, "unknown network event update type: " + + aPacket.updateType); + return; + } + + if (expectedPacket) { + info("checking the packet content"); + checkObject(aPacket, expectedPacket); + } + + if (updates.indexOf("responseContent") > -1 && + updates.indexOf("eventTimings") > -1) { + aState.dbgClient.removeListener("networkEventUpdate", + onNetworkEvent); + + onRequestHeaders = onRequestHeaders.bind(null, aState); + aState.client.getRequestHeaders(aState.netActor, + onRequestHeaders); + } +} + +function onRequestHeaders(aState, aResponse) +{ + info("checking request headers"); + + ok(aResponse.headers.length > 0, "request headers > 0"); + ok(aResponse.headersSize > 0, "request headersSize > 0"); + ok(!!aResponse.rawHeaders.length, "request rawHeaders available"); + + checkHeadersOrCookies(aResponse.headers, { + Referer: /network_requests_iframe\.html/, + Cookie: /bug768096/, + }); + + checkRawHeaders(aResponse.rawHeaders, { + Referer: /network_requests_iframe\.html/, + Cookie: /bug768096/, + }); + + onRequestCookies = onRequestCookies.bind(null, aState); + aState.client.getRequestCookies(aState.netActor, + onRequestCookies); +} + +function onRequestCookies(aState, aResponse) +{ + info("checking request cookies"); + + is(aResponse.cookies.length, 3, "request cookies length"); + + checkHeadersOrCookies(aResponse.cookies, { + foobar: "fooval", + omgfoo: "bug768096", + badcookie: "bug826798=st3fan", + }); + + onRequestPostData = onRequestPostData.bind(null, aState); + aState.client.getRequestPostData(aState.netActor, + onRequestPostData); +} + +function onRequestPostData(aState, aResponse) +{ + info("checking request POST data"); + + checkObject(aResponse, { + postData: { + text: /^Hello world! foobaz barr.+foobaz barr$/, + }, + postDataDiscarded: false, + }); + + is(aResponse.postData.text.length, 552, "postData text length"); + + onResponseHeaders = onResponseHeaders.bind(null, aState); + aState.client.getResponseHeaders(aState.netActor, + onResponseHeaders); +} + +function onResponseHeaders(aState, aResponse) +{ + info("checking response headers"); + + ok(aResponse.headers.length > 0, "response headers > 0"); + ok(aResponse.headersSize > 0, "response headersSize > 0"); + ok(!!aResponse.rawHeaders, "response rawHeaders available"); + + checkHeadersOrCookies(aResponse.headers, { + "Content-Type": /^application\/(json|octet-stream)$/, + "Content-Length": /^\d+$/, + }); + + checkRawHeaders(aResponse.rawHeaders, { + "Content-Type": /^application\/(json|octet-stream)$/, + "Content-Length": /^\d+$/, + }); + + onResponseCookies = onResponseCookies.bind(null, aState); + aState.client.getResponseCookies(aState.netActor, + onResponseCookies); +} + +function onResponseCookies(aState, aResponse) +{ + info("checking response cookies"); + + is(aResponse.cookies.length, 0, "response cookies length"); + + onResponseContent = onResponseContent.bind(null, aState); + aState.client.getResponseContent(aState.netActor, + onResponseContent); +} + +function onResponseContent(aState, aResponse) +{ + info("checking response content"); + + checkObject(aResponse, { + content: { + text: /"test JSON data"/, + }, + contentDiscarded: false, + }); + + onEventTimings = onEventTimings.bind(null, aState); + aState.client.getEventTimings(aState.netActor, + onEventTimings); +} + +function onEventTimings(aState, aResponse) +{ + info("checking event timings"); + + checkObject(aResponse, { + timings: { + blocked: /^-1|\d+$/, + dns: /^-1|\d+$/, + connect: /^-1|\d+$/, + send: /^-1|\d+$/, + wait: /^-1|\d+$/, + receive: /^-1|\d+$/, + }, + totalTime: /^\d+$/, + }); + + closeDebugger(aState, function() { + SimpleTest.finish(); + }); +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_network_security-hpkp.html b/devtools/shared/webconsole/test/test_network_security-hpkp.html new file mode 100644 index 000000000..55e2621a8 --- /dev/null +++ b/devtools/shared/webconsole/test/test_network_security-hpkp.html @@ -0,0 +1,108 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for the network actor (HPKP detection)</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for the network actor (HPKP detection)</p> + +<iframe src="https://example.com/chrome/devtools/shared/webconsole/test/network_requests_iframe.html"></iframe> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +let gCurrentTestCase = -1; +const HPKP_PREF = "security.cert_pinning.process_headers_from_non_builtin_roots"; + +// Static pins tested by unit/test_security-info-static-hpkp.js. +const TEST_CASES = [ + { + desc: "no Public Key Pinning", + url: "https://example.com", + usesPinning: false, + }, + { + desc: "dynamic Public Key Pinning with this request", + url: "https://include-subdomains.pinning-dynamic.example.com/" + + "browser/browser/base/content/test/general/pinning_headers.sjs", + usesPinning: true, + }, + { + desc: "dynamic Public Key Pinning with previous request", + url: "https://include-subdomains.pinning-dynamic.example.com/", + usesPinning: true, + } +]; + +function startTest() +{ + // Need to enable this pref or pinning headers are rejected due test + // certificate. + Services.prefs.setBoolPref(HPKP_PREF, true); + SimpleTest.registerCleanupFunction(() => { + Services.prefs.setBoolPref(HPKP_PREF, false); + + // Reset pinning state. + let gSSService = Cc["@mozilla.org/ssservice;1"] + .getService(Ci.nsISiteSecurityService); + + let gIOService = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + for (let {url} of TEST_CASES) { + let uri = gIOService.newURI(url, null, null); + gSSService.removeState(Ci.nsISiteSecurityService.HEADER_HPKP, uri, 0); + } + }); + + info("Test detection of Public Key Pinning."); + removeEventListener("load", startTest); + attachConsoleToTab(["NetworkActivity"], onAttach); +} + +function onAttach(aState, aResponse) +{ + onNetworkEventUpdate = onNetworkEventUpdate.bind(null, aState); + aState.dbgClient.addListener("networkEventUpdate", onNetworkEventUpdate); + + runNextCase(aState); +} + +function runNextCase(aState) { + gCurrentTestCase++; + if (gCurrentTestCase === TEST_CASES.length) { + info("Tests ran. Cleaning up."); + closeDebugger(aState, SimpleTest.finish); + return; + } + + let { desc, url } = TEST_CASES[gCurrentTestCase]; + info("Testing site with " + desc); + + let iframe = document.querySelector("iframe").contentWindow; + iframe.wrappedJSObject.makeXhrCallback("GET", url); +} + +function onNetworkEventUpdate(aState, aType, aPacket) +{ + function onSecurityInfo(packet) { + let data = TEST_CASES[gCurrentTestCase]; + is(packet.securityInfo.hpkp, data.usesPinning, + "Public Key Pinning detected correctly."); + + runNextCase(aState); + } + + if (aPacket.updateType === "securityInfo") { + aState.client.getSecurityInfo(aPacket.from, onSecurityInfo); + } +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_network_security-hsts.html b/devtools/shared/webconsole/test/test_network_security-hsts.html new file mode 100644 index 000000000..f69244d8d --- /dev/null +++ b/devtools/shared/webconsole/test/test_network_security-hsts.html @@ -0,0 +1,100 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for the network actor (HSTS detection)</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for the network actor (HSTS detection)</p> + +<iframe src="https://example.com/chrome/devtools/shared/webconsole/test/network_requests_iframe.html"></iframe> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +let gCurrentTestCase = -1; +const TEST_CASES = [ + { + desc: "no HSTS", + url: "https://example.com", + usesHSTS: false, + }, + { + desc: "HSTS from this response", + url: "https://example.com/"+ + "browser/browser/base/content/test/general/browser_star_hsts.sjs", + usesHSTS: true, + }, + { + desc: "stored HSTS from previous response", + url: "https://example.com/", + usesHSTS: true, + } +]; + +function startTest() +{ + + SimpleTest.registerCleanupFunction(() => { + // Reset HSTS state. + let gSSService = Cc["@mozilla.org/ssservice;1"] + .getService(Ci.nsISiteSecurityService); + + let gIOService = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + + let uri = gIOService.newURI(TEST_CASES[0].url, null, null); + gSSService.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, 0); + }); + + info("Test detection of HTTP Strict Transport Security."); + removeEventListener("load", startTest); + attachConsoleToTab(["NetworkActivity"], onAttach); +} + +function onAttach(aState, aResponse) +{ + onNetworkEventUpdate = onNetworkEventUpdate.bind(null, aState); + aState.dbgClient.addListener("networkEventUpdate", onNetworkEventUpdate); + + runNextCase(aState); +} + +function runNextCase(aState) { + gCurrentTestCase++; + if (gCurrentTestCase === TEST_CASES.length) { + info("Tests ran. Cleaning up."); + closeDebugger(aState, SimpleTest.finish); + return; + } + + let { desc, url } = TEST_CASES[gCurrentTestCase]; + info("Testing site with " + desc); + + let iframe = document.querySelector("iframe").contentWindow; + iframe.wrappedJSObject.makeXhrCallback("GET", url); +} + +function onNetworkEventUpdate(aState, aType, aPacket) +{ + function onSecurityInfo(packet) { + let data = TEST_CASES[gCurrentTestCase]; + is(packet.securityInfo.hsts, data.usesHSTS, + "Strict Transport Security detected correctly."); + + runNextCase(aState); + } + + if (aPacket.updateType === "securityInfo") { + aState.client.getSecurityInfo(aPacket.from, onSecurityInfo); + } +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_nsiconsolemessage.html b/devtools/shared/webconsole/test/test_nsiconsolemessage.html new file mode 100644 index 000000000..ef8b8067e --- /dev/null +++ b/devtools/shared/webconsole/test/test_nsiconsolemessage.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for nsIConsoleMessages</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Make sure that nsIConsoleMessages are logged. See bug 859756.</p> + +<script class="testbody" type="text/javascript;version=1.8"> +"use strict"; +SimpleTest.waitForExplicitFinish(); + +let expectedMessages = []; + +function startTest() +{ + removeEventListener("load", startTest); + attachConsole(["PageError"], onAttach); +} + +function onAttach(aState, aResponse) +{ + onLogMessage = onLogMessage.bind(null, aState); + aState.dbgClient.addListener("logMessage", onLogMessage); + + expectedMessages = [{ + message: "hello world! bug859756", + timeStamp: /^\d+$/, + }]; + + Services.console.logStringMessage("hello world! bug859756"); + + info("waiting for messages"); +} + +let receivedMessages = []; + +function onLogMessage(aState, aType, aPacket) +{ + is(aPacket.from, aState.actor, "packet actor"); + info("received message: " + aPacket.message); + + let found = false; + for (let expected of expectedMessages) { + if (expected.message == aPacket.message) { + found = true; + break; + } + } + if (!found) { + return; + } + + receivedMessages.push(aPacket); + if (receivedMessages.length != expectedMessages.length) { + return; + } + + aState.dbgClient.removeListener("logMessage", onLogMessage); + + checkObject(receivedMessages, expectedMessages); + + closeDebugger(aState, () => SimpleTest.finish()); +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_object_actor.html b/devtools/shared/webconsole/test/test_object_actor.html new file mode 100644 index 000000000..09176a5aa --- /dev/null +++ b/devtools/shared/webconsole/test/test_object_actor.html @@ -0,0 +1,178 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for the object actor</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for the object actor</p> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +let expectedProps = []; + +function startTest() +{ + removeEventListener("load", startTest); + + attachConsoleToTab(["ConsoleAPI"], onAttach); +} + +function onAttach(aState, aResponse) +{ + onConsoleCall = onConsoleCall.bind(null, aState); + aState.dbgClient.addListener("consoleAPICall", onConsoleCall); + + let longString = (new Array(DebuggerServer.LONG_STRING_LENGTH + 3)).join("\u0629"); + + // Here we put the objects in the correct window, to avoid having them all + // wrapped by proxies for cross-compartment access. + + let foobarObject = top.Object.create(null); + foobarObject.tamarbuta = longString; + foobarObject.foo = 1; + foobarObject.foobar = "hello"; + foobarObject.omg = null; + foobarObject.testfoo = false; + foobarObject.notInspectable = top.Object.create(null); + foobarObject.omgfn = new top.Function("return 'myResult'"); + foobarObject.abArray = new top.Array("a", "b"); + foobarObject.foobaz = top.document; + + top.Object.defineProperty(foobarObject, "getterAndSetter", { + enumerable: true, + get: new top.Function("return 'foo';"), + set: new top.Function("1+2"), + }); + + foobarObject.longStringObj = top.Object.create(null); + foobarObject.longStringObj.toSource = new top.Function("'" + longString + "'"); + foobarObject.longStringObj.toString = new top.Function("'" + longString + "'"); + foobarObject.longStringObj.boom = "explode"; + + top.wrappedJSObject.foobarObject = foobarObject; + top.console.log("hello", top.wrappedJSObject.foobarObject); + + expectedProps = { + "abArray": { + value: { + type: "object", + class: "Array", + actor: /[a-z]/, + }, + }, + "foo": { + configurable: true, + enumerable: true, + writable: true, + value: 1, + }, + "foobar": { + value: "hello", + }, + "foobaz": { + value: { + type: "object", + class: "XULDocument", + actor: /[a-z]/, + }, + }, + "getterAndSetter": { + get: { + type: "object", + class: "Function", + actor: /[a-z]/, + }, + set: { + type: "object", + class: "Function", + actor: /[a-z]/, + }, + }, + "longStringObj": { + value: { + type: "object", + class: "Object", + actor: /[a-z]/, + }, + }, + "notInspectable": { + value: { + type: "object", + class: "Object", + actor: /[a-z]/, + }, + }, + "omg": { + value: { type: "null" }, + }, + "omgfn": { + value: { + type: "object", + class: "Function", + actor: /[a-z]/, + }, + }, + "tamarbuta": { + value: { + type: "longString", + initial: longString.substring(0, + DebuggerServer.LONG_STRING_INITIAL_LENGTH), + length: longString.length, + }, + }, + "testfoo": { + value: false, + }, + }; +} + +function onConsoleCall(aState, aType, aPacket) +{ + is(aPacket.from, aState.actor, "console API call actor"); + + info("checking the console API call packet"); + + checkConsoleAPICall(aPacket.message, { + level: "log", + filename: /test_object_actor/, + functionName: "onAttach", + arguments: ["hello", { + type: "object", + actor: /[a-z]/, + }], + }); + + aState.dbgClient.removeListener("consoleAPICall", onConsoleCall); + + info("inspecting object properties"); + let args = aPacket.message.arguments; + onProperties = onProperties.bind(null, aState); + + let client = new ObjectClient(aState.dbgClient, args[1]); + client.getPrototypeAndProperties(onProperties); +} + +function onProperties(aState, aResponse) +{ + let props = aResponse.ownProperties; + is(Object.keys(props).length, Object.keys(expectedProps).length, + "number of enumerable properties"); + checkObject(props, expectedProps); + + expectedProps = []; + + closeDebugger(aState, function() { + SimpleTest.finish(); + }); +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_object_actor_native_getters.html b/devtools/shared/webconsole/test/test_object_actor_native_getters.html new file mode 100644 index 000000000..e22eb8cd5 --- /dev/null +++ b/devtools/shared/webconsole/test/test_object_actor_native_getters.html @@ -0,0 +1,106 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for the native getters in object actors</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for the native getters in object actors</p> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +let expectedProps = []; +let expectedSafeGetters = []; + +function startTest() +{ + removeEventListener("load", startTest); + + attachConsoleToTab(["ConsoleAPI"], onAttach); +} + +function onAttach(aState, aResponse) +{ + onConsoleCall = onConsoleCall.bind(null, aState); + aState.dbgClient.addListener("consoleAPICall", onConsoleCall); + + top.console.log("hello", document); + + expectedProps = { + "location": { + get: { + type: "object", + class: "Function", + actor: /[a-z]/, + }, + }, + }; + + expectedSafeGetters = { + "title": { + getterValue: /native getters in object actors/, + getterPrototypeLevel: 2, + }, + "styleSheets": { + getterValue: /\[object Object\]/, + getterPrototypeLevel: 2, + }, + }; +} + +function onConsoleCall(aState, aType, aPacket) +{ + is(aPacket.from, aState.actor, "console API call actor"); + + info("checking the console API call packet"); + + checkConsoleAPICall(aPacket.message, { + level: "log", + filename: /test_object_actor/, + functionName: "onAttach", + arguments: ["hello", { + type: "object", + actor: /[a-z]/, + }], + }); + + aState.dbgClient.removeListener("consoleAPICall", onConsoleCall); + + info("inspecting object properties"); + let args = aPacket.message.arguments; + onProperties = onProperties.bind(null, aState); + + let client = new ObjectClient(aState.dbgClient, args[1]); + client.getPrototypeAndProperties(onProperties); +} + +function onProperties(aState, aResponse) +{ + let props = aResponse.ownProperties; + let keys = Object.keys(props); + info(keys.length + " ownProperties: " + keys); + + ok(keys.length >= Object.keys(expectedProps).length, "number of properties"); + + info("check ownProperties"); + checkObject(props, expectedProps); + info("check safeGetterValues"); + checkObject(aResponse.safeGetterValues, expectedSafeGetters); + + expectedProps = []; + expectedSafeGetters = []; + + closeDebugger(aState, function() { + SimpleTest.finish(); + }); +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_object_actor_native_getters_lenient_this.html b/devtools/shared/webconsole/test/test_object_actor_native_getters_lenient_this.html new file mode 100644 index 000000000..c4197a5b8 --- /dev/null +++ b/devtools/shared/webconsole/test/test_object_actor_native_getters_lenient_this.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test that WebIDL attributes with the LenientThis extended attribute + do not appear in the wrong objects</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for the native getters in object actors</p> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +function startTest() +{ + removeEventListener("load", startTest); + + attachConsoleToTab(["ConsoleAPI"], onAttach); +} + +function onAttach(aState, aResponse) +{ + onConsoleCall = onConsoleCall.bind(null, aState); + aState.dbgClient.addListener("consoleAPICall", onConsoleCall); + + let docAsProto = Object.create(document); + + top.console.log("hello", docAsProto); +} + +function onConsoleCall(aState, aType, aPacket) +{ + is(aPacket.from, aState.actor, "console API call actor"); + + info("checking the console API call packet"); + + checkConsoleAPICall(aPacket.message, { + level: "log", + filename: /test_object_actor/, + functionName: "onAttach", + arguments: ["hello", { + type: "object", + actor: /[a-z]/, + }], + }); + + aState.dbgClient.removeListener("consoleAPICall", onConsoleCall); + + info("inspecting object properties"); + let args = aPacket.message.arguments; + onProperties = onProperties.bind(null, aState); + + let client = new ObjectClient(aState.dbgClient, args[1]); + client.getPrototypeAndProperties(onProperties); +} + +function onProperties(aState, aResponse) +{ + let props = aResponse.ownProperties; + let keys = Object.keys(props); + info(keys.length + " ownProperties: " + keys); + + is(keys.length, 0, "number of properties"); + keys = Object.keys(aResponse.safeGetterValues); + is(keys.length, 0, "number of safe getters"); + + closeDebugger(aState, function() { + SimpleTest.finish(); + }); +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_page_errors.html b/devtools/shared/webconsole/test/test_page_errors.html new file mode 100644 index 000000000..19e5ba4b4 --- /dev/null +++ b/devtools/shared/webconsole/test/test_page_errors.html @@ -0,0 +1,186 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for page errors</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for page errors</p> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +let expectedPageErrors = []; + +function doPageErrors() +{ + expectedPageErrors = { + "document.body.style.color = 'fooColor';": { + errorMessage: /fooColor/, + errorMessageName: undefined, + sourceName: /test_page_errors/, + category: "CSS Parser", + timeStamp: /^\d+$/, + error: false, + warning: true, + exception: false, + strict: false, + }, + "document.doTheImpossible();": { + errorMessage: /doTheImpossible/, + errorMessageName: undefined, + sourceName: /test_page_errors/, + category: "chrome javascript", + timeStamp: /^\d+$/, + error: false, + warning: false, + exception: true, + strict: false, + }, + "(42).toString(0);": { + errorMessage: /radix/, + errorMessageName: "JSMSG_BAD_RADIX", + sourceName: /test_page_errors/, + category: "chrome javascript", + timeStamp: /^\d+$/, + error: false, + warning: false, + exception: true, + strict: false, + }, + "'use strict'; (Object.freeze({name: 'Elsa', score: 157})).score = 0;": { + errorMessage: /read.only/, + errorMessageName: "JSMSG_READ_ONLY", + sourceName: /test_page_errors/, + category: "chrome javascript", + timeStamp: /^\d+$/, + error: false, + warning: false, + exception: true, + }, + "([]).length = -1": { + errorMessage: /array length/, + errorMessageName: "JSMSG_BAD_ARRAY_LENGTH", + sourceName: /test_page_errors/, + category: "chrome javascript", + timeStamp: /^\d+$/, + error: false, + warning: false, + exception: true, + }, + "'abc'.repeat(-1);": { + errorMessage: /repeat count.*non-negative/, + errorMessageName: "JSMSG_NEGATIVE_REPETITION_COUNT", + sourceName: /test_page_errors/, + category: "chrome javascript", + timeStamp: /^\d+$/, + error: false, + warning: false, + exception: true, + }, + "'a'.repeat(2e28);": { + errorMessage: /repeat count.*less than infinity/, + errorMessageName: "JSMSG_RESULTING_STRING_TOO_LARGE", + sourceName: /test_page_errors/, + category: "chrome javascript", + timeStamp: /^\d+$/, + error: false, + warning: false, + exception: true, + }, + "77.1234.toExponential(-1);": { + errorMessage: /out of range/, + errorMessageName: "JSMSG_PRECISION_RANGE", + sourceName: /test_page_errors/, + category: "chrome javascript", + timeStamp: /^\d+$/, + error: false, + warning: false, + exception: true, + }, + "var f = Function('x y', 'return x + y;');": { + errorMessage: /malformed formal/, + errorMessageName: "JSMSG_BAD_FORMAL", + sourceName: /test_page_errors/, + category: "chrome javascript", + timeStamp: /^\d+$/, + error: false, + warning: false, + exception: true, + }, + "function a() { return; 1 + 1; }": { + errorMessage: /unreachable code/, + errorMessageName: "JSMSG_STMT_AFTER_RETURN", + sourceName: /test_page_errors/, + category: "chrome javascript", + timeStamp: /^\d+$/, + error: false, + warning: true, + exception: false, + }, + }; + + let container = document.createElement("script"); + for (let stmt of Object.keys(expectedPageErrors)) { + if (expectedPageErrors[stmt].exception) { + SimpleTest.expectUncaughtException(); + } + info("starting stmt: " + stmt); + container = document.createElement("script"); + document.body.appendChild(container); + container.textContent = stmt; + document.body.removeChild(container); + info("ending stmt: " + stmt); + } +} + +function startTest() +{ + removeEventListener("load", startTest); + + attachConsole(["PageError"], onAttach); +} + +function onAttach(aState, aResponse) +{ + onPageError = onPageError.bind(null, aState); + aState.dbgClient.addListener("pageError", onPageError); + doPageErrors(); +} + +let pageErrors = []; + +function onPageError(aState, aType, aPacket) +{ + if (!aPacket.pageError.sourceName.includes("test_page_errors")) { + info("Ignoring error from unknown source: " + aPacket.pageError.sourceName); + return; + } + + is(aPacket.from, aState.actor, "page error actor"); + + pageErrors.push(aPacket.pageError); + if (pageErrors.length != Object.keys(expectedPageErrors).length) { + return; + } + + aState.dbgClient.removeListener("pageError", onPageError); + + Object.values(expectedPageErrors).forEach(function(aMessage, aIndex) { + info("checking received page error #" + aIndex); + checkObject(pageErrors[aIndex], Object.values(expectedPageErrors)[aIndex]); + }); + + closeDebugger(aState, function() { + SimpleTest.finish(); + }); +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_reflow.html b/devtools/shared/webconsole/test/test_reflow.html new file mode 100644 index 000000000..2ac2ca509 --- /dev/null +++ b/devtools/shared/webconsole/test/test_reflow.html @@ -0,0 +1,94 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Test for the Reflow Activity</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Test for reflow events</p> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +let client; + +function generateReflow() +{ + top.document.documentElement.style.display = "none"; + top.document.documentElement.getBoundingClientRect(); + top.document.documentElement.style.display = "block"; +} + +function startTest() +{ + removeEventListener("load", startTest); + attachConsoleToTab(["ReflowActivity"], onAttach); +} + +function onAttach(aState, aResponse) +{ + client = aState.dbgClient; + + onReflowActivity = onReflowActivity.bind(null, aState); + client.addListener("reflowActivity", onReflowActivity); + generateReflow(); +} + +// We are expecting 3 reflow events. +let expectedEvents = [ + { + interruptible: false, + sourceURL: "chrome://mochitests/content/chrome/devtools/shared/webconsole/test/test_reflow.html", + functionName: "generateReflow" + }, + { + interruptible: true, + sourceURL: null, + functionName: null + }, + { + interruptible: true, + sourceURL: null, + functionName: null + }, +]; + +let receivedEvents = []; + + +function onReflowActivity(aState, aType, aPacket) +{ + info("packet: " + aPacket.message); + receivedEvents.push(aPacket); + if (receivedEvents.length == expectedEvents.length) { + checkEvents(); + finish(aState); + } +} + +function checkEvents() { + for (let i = 0; i < expectedEvents.length; i++) { + let a = expectedEvents[i]; + let b = receivedEvents[i]; + for (let key in a) { + is(a[key], b[key], "field " + key + " is valid"); + } + } +} + +function finish(aState) { + client.removeListener("reflowActivity", onReflowActivity); + closeDebugger(aState, function() { + SimpleTest.finish(); + }); +} + +addEventListener("load", startTest); + +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/test_throw.html b/devtools/shared/webconsole/test/test_throw.html new file mode 100644 index 000000000..7d7ea7b31 --- /dev/null +++ b/devtools/shared/webconsole/test/test_throw.html @@ -0,0 +1,93 @@ +<!DOCTYPE HTML> +<html lang="en"> +<head> + <meta charset="utf8"> + <title>Web Console throw tests</title> + <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript;version=1.8" src="common.js"></script> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +<body> +<p>Web Console throw tests</p> + +<script class="testbody" type="text/javascript;version=1.8"> +SimpleTest.waitForExplicitFinish(); + +function startTest() +{ + removeEventListener("load", startTest); + attachConsoleToTab([], onAttach); +} + +function onAttach(aState, aResponse) +{ + let tests = []; + + let falsyValues = ["-0", "null", "undefined", "Infinity", "-Infinity", "NaN"]; + falsyValues.forEach(function(value) { + tests.push(function() { + aState.client.evaluateJS("throw " + value + ";", function(aResponse) { + let type = aResponse.exception.type; + is(type, value, "exception.type for throw " + value); + nextTest(); + }); + }); + }); + + let identityTestValues = [false, 0]; + identityTestValues.forEach(function(value) { + tests.push(function() { + aState.client.evaluateJS("throw " + value + ";", function(aResponse) { + let exception = aResponse.exception; + is(exception, value, "response.exception for throw " + value); + nextTest(); + }); + }); + }); + + let longString = Array(DebuggerServer.LONG_STRING_LENGTH + 1).join("a"), + shortedString = longString.substring(0, + DebuggerServer.LONG_STRING_INITIAL_LENGTH + ); + tests.push(function() { + aState.client.evaluateJS("throw '" + longString + "';", function(aResponse) { + is(aResponse.exception.initial, shortedString, + "exception.initial for throw longString" + ); + is(aResponse.exceptionMessage.initial, shortedString, + "exceptionMessage.initial for throw longString" + ); + nextTest(); + }); + }); + + let symbolTestValues = [ + ["Symbol.iterator", "Symbol(Symbol.iterator)"], + ["Symbol('foo')", "Symbol(foo)"], + ["Symbol()", "Symbol()"], + ]; + symbolTestValues.forEach(function([expr, message]) { + tests.push(function() { + aState.client.evaluateJS("throw " + expr + ";", function(aResponse) { + is(aResponse.exceptionMessage, message, + "response.exception for throw " + expr); + nextTest(); + }); + }); + }); + + runTests(tests, endTest.bind(null, aState)); +} + +function endTest(aState) +{ + closeDebugger(aState, function() { + SimpleTest.finish(); + }); +} + +addEventListener("load", startTest); +</script> +</body> +</html> diff --git a/devtools/shared/webconsole/test/unit/.eslintrc.js b/devtools/shared/webconsole/test/unit/.eslintrc.js new file mode 100644 index 000000000..59adf410a --- /dev/null +++ b/devtools/shared/webconsole/test/unit/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the common devtools xpcshell eslintrc config. + "extends": "../../../../.eslintrc.xpcshell.js" +}; diff --git a/devtools/shared/webconsole/test/unit/test_js_property_provider.js b/devtools/shared/webconsole/test/unit/test_js_property_provider.js new file mode 100644 index 000000000..c360cf96d --- /dev/null +++ b/devtools/shared/webconsole/test/unit/test_js_property_provider.js @@ -0,0 +1,170 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + +"use strict"; +const { require } = Components.utils.import("resource://devtools/shared/Loader.jsm", {}); +const { FallibleJSPropertyProvider: JSPropertyProvider } = + require("devtools/shared/webconsole/js-property-provider"); + +Components.utils.import("resource://gre/modules/jsdebugger.jsm"); +addDebuggerToGlobal(this); + +function run_test() { + const testArray = `var testArray = [ + {propA: "A"}, + { + propB: "B", + propC: [ + "D" + ] + }, + [ + {propE: "E"} + ] + ]`; + + const testObject = 'var testObject = {"propA": [{"propB": "B"}]}'; + const testHyphenated = 'var testHyphenated = {"prop-A": "res-A"}'; + const testLet = "let foobar = {a: ''}; const blargh = {a: 1};"; + + let sandbox = Components.utils.Sandbox("http://example.com"); + let dbg = new Debugger; + let dbgObject = dbg.addDebuggee(sandbox); + let dbgEnv = dbgObject.asEnvironment(); + Components.utils.evalInSandbox(testArray, sandbox); + Components.utils.evalInSandbox(testObject, sandbox); + Components.utils.evalInSandbox(testHyphenated, sandbox); + Components.utils.evalInSandbox(testLet, sandbox); + + do_print("Running tests with dbgObject"); + runChecks(dbgObject, null); + + do_print("Running tests with dbgEnv"); + runChecks(null, dbgEnv); + +} + +function runChecks(dbgObject, dbgEnv) { + do_print("Test that suggestions are given for 'this'"); + let results = JSPropertyProvider(dbgObject, dbgEnv, "t"); + test_has_result(results, "this"); + + if (dbgObject != null) { + do_print("Test that suggestions are given for 'this.'"); + results = JSPropertyProvider(dbgObject, dbgEnv, "this."); + test_has_result(results, "testObject"); + + do_print("Test that no suggestions are given for 'this.this'"); + results = JSPropertyProvider(dbgObject, dbgEnv, "this.this"); + test_has_no_results(results); + } + + do_print("Testing lexical scope issues (Bug 1207868)"); + results = JSPropertyProvider(dbgObject, dbgEnv, "foobar"); + test_has_result(results, "foobar"); + + results = JSPropertyProvider(dbgObject, dbgEnv, "foobar."); + test_has_result(results, "a"); + + results = JSPropertyProvider(dbgObject, dbgEnv, "blargh"); + test_has_result(results, "blargh"); + + results = JSPropertyProvider(dbgObject, dbgEnv, "blargh."); + test_has_result(results, "a"); + + do_print("Test that suggestions are given for 'foo[n]' where n is an integer."); + results = JSPropertyProvider(dbgObject, dbgEnv, "testArray[0]."); + test_has_result(results, "propA"); + + do_print("Test that suggestions are given for multidimensional arrays."); + results = JSPropertyProvider(dbgObject, dbgEnv, "testArray[2][0]."); + test_has_result(results, "propE"); + + do_print("Test that suggestions are given for nested arrays."); + results = JSPropertyProvider(dbgObject, dbgEnv, "testArray[1].propC[0]."); + test_has_result(results, "indexOf"); + + do_print("Test that suggestions are given for literal arrays."); + results = JSPropertyProvider(dbgObject, dbgEnv, "[1,2,3]."); + test_has_result(results, "indexOf"); + + do_print("Test that suggestions are given for literal arrays with newlines."); + results = JSPropertyProvider(dbgObject, dbgEnv, "[1,2,3,\n4\n]."); + test_has_result(results, "indexOf"); + + do_print("Test that suggestions are given for literal strings."); + results = JSPropertyProvider(dbgObject, dbgEnv, "'foo'."); + test_has_result(results, "charAt"); + results = JSPropertyProvider(dbgObject, dbgEnv, '"foo".'); + test_has_result(results, "charAt"); + results = JSPropertyProvider(dbgObject, dbgEnv, "`foo`."); + test_has_result(results, "charAt"); + results = JSPropertyProvider(dbgObject, dbgEnv, "'[1,2,3]'."); + test_has_result(results, "charAt"); + + do_print("Test that suggestions are not given for syntax errors."); + results = JSPropertyProvider(dbgObject, dbgEnv, "'foo\""); + do_check_null(results); + results = JSPropertyProvider(dbgObject, dbgEnv, "[1,',2]"); + do_check_null(results); + results = JSPropertyProvider(dbgObject, dbgEnv, "'[1,2]."); + do_check_null(results); + results = JSPropertyProvider(dbgObject, dbgEnv, "'foo'.."); + do_check_null(results); + + do_print("Test that suggestions are not given without a dot."); + results = JSPropertyProvider(dbgObject, dbgEnv, "'foo'"); + test_has_no_results(results); + results = JSPropertyProvider(dbgObject, dbgEnv, "[1,2,3]"); + test_has_no_results(results); + results = JSPropertyProvider(dbgObject, dbgEnv, "[1,2,3].\n'foo'"); + test_has_no_results(results); + + do_print("Test that suggestions are not given for numeric literals."); + results = JSPropertyProvider(dbgObject, dbgEnv, "1."); + do_check_null(results); + + do_print("Test that suggestions are not given for index that's out of bounds."); + results = JSPropertyProvider(dbgObject, dbgEnv, "testArray[10]."); + do_check_null(results); + + do_print("Test that no suggestions are given if an index is not numerical somewhere in the chain."); + results = JSPropertyProvider(dbgObject, dbgEnv, "testArray[0]['propC'][0]."); + do_check_null(results); + + results = JSPropertyProvider(dbgObject, dbgEnv, "testObject['propA'][0]."); + do_check_null(results); + + results = JSPropertyProvider(dbgObject, dbgEnv, "testArray[0]['propC']."); + do_check_null(results); + + results = JSPropertyProvider(dbgObject, dbgEnv, "testArray[][1]."); + do_check_null(results); + + do_print("Test that suggestions are not given if there is an hyphen in the chain."); + results = JSPropertyProvider(dbgObject, dbgEnv, "testHyphenated['prop-A']."); + do_check_null(results); +} + +/** + * A helper that ensures an empty array of results were found. + * @param Object aResults + * The results returned by JSPropertyProvider. + */ +function test_has_no_results(aResults) { + do_check_neq(aResults, null); + do_check_eq(aResults.matches.length, 0); +} +/** + * A helper that ensures (required) results were found. + * @param Object aResults + * The results returned by JSPropertyProvider. + * @param String aRequiredSuggestion + * A suggestion that must be found from the results. + */ +function test_has_result(aResults, aRequiredSuggestion) { + do_check_neq(aResults, null); + do_check_true(aResults.matches.length > 0); + do_check_true(aResults.matches.indexOf(aRequiredSuggestion) !== -1); +} diff --git a/devtools/shared/webconsole/test/unit/test_network_helper.js b/devtools/shared/webconsole/test/unit/test_network_helper.js new file mode 100644 index 000000000..3a43ff432 --- /dev/null +++ b/devtools/shared/webconsole/test/unit/test_network_helper.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +var Cu = Components.utils; +const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); + +Object.defineProperty(this, "NetworkHelper", { + get: function () { + return require("devtools/shared/webconsole/network-helper"); + }, + configurable: true, + writeable: false, + enumerable: true +}); + +function run_test() { + test_isTextMimeType(); +} + +function test_isTextMimeType() { + do_check_eq(NetworkHelper.isTextMimeType("text/plain"), true); + do_check_eq(NetworkHelper.isTextMimeType("application/javascript"), true); + do_check_eq(NetworkHelper.isTextMimeType("application/json"), true); + do_check_eq(NetworkHelper.isTextMimeType("text/css"), true); + do_check_eq(NetworkHelper.isTextMimeType("text/html"), true); + do_check_eq(NetworkHelper.isTextMimeType("image/svg+xml"), true); + do_check_eq(NetworkHelper.isTextMimeType("application/xml"), true); + + // Test custom JSON subtype + do_check_eq(NetworkHelper.isTextMimeType("application/vnd.tent.posts-feed.v0+json"), true); + do_check_eq(NetworkHelper.isTextMimeType("application/vnd.tent.posts-feed.v0-json"), true); + // Test custom XML subtype + do_check_eq(NetworkHelper.isTextMimeType("application/vnd.tent.posts-feed.v0+xml"), true); + do_check_eq(NetworkHelper.isTextMimeType("application/vnd.tent.posts-feed.v0-xml"), false); + // Test case-insensitive + do_check_eq(NetworkHelper.isTextMimeType("application/vnd.BIG-CORP+json"), true); + // Test non-text type + do_check_eq(NetworkHelper.isTextMimeType("image/png"), false); + // Test invalid types + do_check_eq(NetworkHelper.isTextMimeType("application/foo-+json"), false); + do_check_eq(NetworkHelper.isTextMimeType("application/-foo+json"), false); + do_check_eq(NetworkHelper.isTextMimeType("application/foo--bar+json"), false); + + // Test we do not cause internal errors with unoptimized regex. Bug 961097 + do_check_eq(NetworkHelper.isTextMimeType("application/vnd.google.safebrowsing-chunk"), false); +} diff --git a/devtools/shared/webconsole/test/unit/test_security-info-certificate.js b/devtools/shared/webconsole/test/unit/test_security-info-certificate.js new file mode 100644 index 000000000..be223d87e --- /dev/null +++ b/devtools/shared/webconsole/test/unit/test_security-info-certificate.js @@ -0,0 +1,68 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Tests that NetworkHelper.parseCertificateInfo parses certificate information +// correctly. + +const { require } = Components.utils.import("resource://devtools/shared/Loader.jsm", {}); + +Object.defineProperty(this, "NetworkHelper", { + get: function () { + return require("devtools/shared/webconsole/network-helper"); + }, + configurable: true, + writeable: false, + enumerable: true +}); + +var Ci = Components.interfaces; +const DUMMY_CERT = { + commonName: "cn", + organization: "o", + organizationalUnit: "ou", + issuerCommonName: "issuerCN", + issuerOrganization: "issuerO", + issuerOrganizationUnit: "issuerOU", + sha256Fingerprint: "qwertyuiopoiuytrewq", + sha1Fingerprint: "qwertyuiop", + validity: { + notBeforeLocalDay: "yesterday", + notAfterLocalDay: "tomorrow", + } +}; + +function run_test() { + do_print("Testing NetworkHelper.parseCertificateInfo."); + + let result = NetworkHelper.parseCertificateInfo(DUMMY_CERT); + + // Subject + equal(result.subject.commonName, DUMMY_CERT.commonName, + "Common name is correct."); + equal(result.subject.organization, DUMMY_CERT.organization, + "Organization is correct."); + equal(result.subject.organizationUnit, DUMMY_CERT.organizationUnit, + "Organizational unit is correct."); + + // Issuer + equal(result.issuer.commonName, DUMMY_CERT.issuerCommonName, + "Common name of the issuer is correct."); + equal(result.issuer.organization, DUMMY_CERT.issuerOrganization, + "Organization of the issuer is correct."); + equal(result.issuer.organizationUnit, DUMMY_CERT.issuerOrganizationUnit, + "Organizational unit of the issuer is correct."); + + // Validity + equal(result.validity.start, DUMMY_CERT.validity.notBeforeLocalDay, + "Start of the validity period is correct."); + equal(result.validity.end, DUMMY_CERT.validity.notAfterLocalDay, + "End of the validity period is correct."); + + // Fingerprints + equal(result.fingerprint.sha1, DUMMY_CERT.sha1Fingerprint, + "Certificate SHA1 fingerprint is correct."); + equal(result.fingerprint.sha256, DUMMY_CERT.sha256Fingerprint, + "Certificate SHA256 fingerprint is correct."); +} diff --git a/devtools/shared/webconsole/test/unit/test_security-info-parser.js b/devtools/shared/webconsole/test/unit/test_security-info-parser.js new file mode 100644 index 000000000..a5682e209 --- /dev/null +++ b/devtools/shared/webconsole/test/unit/test_security-info-parser.js @@ -0,0 +1,64 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that NetworkHelper.parseSecurityInfo returns correctly formatted object. + +const { require } = Components.utils.import("resource://devtools/shared/Loader.jsm", {}); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +Object.defineProperty(this, "NetworkHelper", { + get: function () { + return require("devtools/shared/webconsole/network-helper"); + }, + configurable: true, + writeable: false, + enumerable: true +}); + +var Ci = Components.interfaces; +const wpl = Ci.nsIWebProgressListener; +const MockCertificate = { + commonName: "cn", + organization: "o", + organizationalUnit: "ou", + issuerCommonName: "issuerCN", + issuerOrganization: "issuerO", + issuerOrganizationUnit: "issuerOU", + sha256Fingerprint: "qwertyuiopoiuytrewq", + sha1Fingerprint: "qwertyuiop", + validity: { + notBeforeLocalDay: "yesterday", + notAfterLocalDay: "tomorrow", + } +}; + +const MockSecurityInfo = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsITransportSecurityInfo, + Ci.nsISSLStatusProvider]), + securityState: wpl.STATE_IS_SECURE, + errorCode: 0, + SSLStatus: { + cipherSuite: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", + protocolVersion: 3, // TLS_VERSION_1_2 + serverCert: MockCertificate, + } +}; + +function run_test() { + let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {}); + + equal(result.state, "secure", "State is correct."); + + equal(result.cipherSuite, MockSecurityInfo.cipherSuite, + "Cipher suite is correct."); + + equal(result.protocolVersion, "TLSv1.2", "Protocol version is correct."); + + deepEqual(result.cert, NetworkHelper.parseCertificateInfo(MockCertificate), + "Certificate information is correct."); + + equal(result.hpkp, false, "HPKP is false when URI is not available."); + equal(result.hsts, false, "HSTS is false when URI is not available."); +} diff --git a/devtools/shared/webconsole/test/unit/test_security-info-protocol-version.js b/devtools/shared/webconsole/test/unit/test_security-info-protocol-version.js new file mode 100644 index 000000000..b84002131 --- /dev/null +++ b/devtools/shared/webconsole/test/unit/test_security-info-protocol-version.js @@ -0,0 +1,54 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Tests that NetworkHelper.formatSecurityProtocol returns correct +// protocol version strings. + +const { require } = Components.utils.import("resource://devtools/shared/Loader.jsm", {}); + +Object.defineProperty(this, "NetworkHelper", { + get: function () { + return require("devtools/shared/webconsole/network-helper"); + }, + configurable: true, + writeable: false, + enumerable: true +}); + +var Ci = Components.interfaces; +const TEST_CASES = [ + { + description: "TLS_VERSION_1", + input: 1, + expected: "TLSv1" + }, { + description: "TLS_VERSION_1.1", + input: 2, + expected: "TLSv1.1" + }, { + description: "TLS_VERSION_1.2", + input: 3, + expected: "TLSv1.2" + }, { + description: "TLS_VERSION_1.3", + input: 4, + expected: "TLSv1.3" + }, { + description: "invalid version", + input: -1, + expected: "Unknown" + }, +]; + +function run_test() { + do_print("Testing NetworkHelper.formatSecurityProtocol."); + + for (let {description, input, expected} of TEST_CASES) { + do_print("Testing " + description); + + equal(NetworkHelper.formatSecurityProtocol(input), expected, + "Got the expected protocol string."); + } +} diff --git a/devtools/shared/webconsole/test/unit/test_security-info-state.js b/devtools/shared/webconsole/test/unit/test_security-info-state.js new file mode 100644 index 000000000..efa493a95 --- /dev/null +++ b/devtools/shared/webconsole/test/unit/test_security-info-state.js @@ -0,0 +1,100 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Tests that security info parser gives correct general security state for +// different cases. + +const { require } = Components.utils.import("resource://devtools/shared/Loader.jsm", {}); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +Object.defineProperty(this, "NetworkHelper", { + get: function () { + return require("devtools/shared/webconsole/network-helper"); + }, + configurable: true, + writeable: false, + enumerable: true +}); + +var Ci = Components.interfaces; +const wpl = Ci.nsIWebProgressListener; +const MockSecurityInfo = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsITransportSecurityInfo, + Ci.nsISSLStatusProvider]), + securityState: wpl.STATE_IS_BROKEN, + errorCode: 0, + SSLStatus: { + protocolVersion: 3, // nsISSLStatus.TLS_VERSION_1_2 + cipherSuite: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", + } +}; + +function run_test() { + test_nullSecurityInfo(); + test_insecureSecurityInfoWithNSSError(); + test_insecureSecurityInfoWithoutNSSError(); + test_brokenSecurityInfo(); + test_secureSecurityInfo(); +} + +/** + * Test that undefined security information is returns "insecure". + */ +function test_nullSecurityInfo() { + let result = NetworkHelper.parseSecurityInfo(null, {}); + equal(result.state, "insecure", + "state == 'insecure' when securityInfo was undefined"); +} + +/** + * Test that STATE_IS_INSECURE with NSSError returns "broken" + */ +function test_insecureSecurityInfoWithNSSError() { + MockSecurityInfo.securityState = wpl.STATE_IS_INSECURE; + + // Taken from security/manager/ssl/tests/unit/head_psm.js. + MockSecurityInfo.errorCode = -8180; + + let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {}); + equal(result.state, "broken", + "state == 'broken' if securityState contains STATE_IS_INSECURE flag AND " + + "errorCode is NSS error."); + + MockSecurityInfo.errorCode = 0; +} + +/** + * Test that STATE_IS_INSECURE without NSSError returns "insecure" + */ +function test_insecureSecurityInfoWithoutNSSError() { + MockSecurityInfo.securityState = wpl.STATE_IS_INSECURE; + + let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {}); + equal(result.state, "insecure", + "state == 'insecure' if securityState contains STATE_IS_INSECURE flag BUT " + + "errorCode is not NSS error."); +} + +/** + * Test that STATE_IS_SECURE returns "secure" + */ +function test_secureSecurityInfo() { + MockSecurityInfo.securityState = wpl.STATE_IS_SECURE; + + let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {}); + equal(result.state, "secure", + "state == 'secure' if securityState contains STATE_IS_SECURE flag"); +} + +/** + * Test that STATE_IS_BROKEN returns "weak" + */ +function test_brokenSecurityInfo() { + MockSecurityInfo.securityState = wpl.STATE_IS_BROKEN; + + let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {}); + equal(result.state, "weak", + "state == 'weak' if securityState contains STATE_IS_BROKEN flag"); +} diff --git a/devtools/shared/webconsole/test/unit/test_security-info-static-hpkp.js b/devtools/shared/webconsole/test/unit/test_security-info-static-hpkp.js new file mode 100644 index 000000000..b76fa141a --- /dev/null +++ b/devtools/shared/webconsole/test/unit/test_security-info-static-hpkp.js @@ -0,0 +1,47 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that NetworkHelper.parseSecurityInfo correctly detects static hpkp pins + +const { require } = Components.utils.import("resource://devtools/shared/Loader.jsm", {}); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +const Services = require("Services"); + +Object.defineProperty(this, "NetworkHelper", { + get: function () { + return require("devtools/shared/webconsole/network-helper"); + }, + configurable: true, + writeable: false, + enumerable: true +}); + +var Ci = Components.interfaces; +const wpl = Ci.nsIWebProgressListener; + +const MockSecurityInfo = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsITransportSecurityInfo, + Ci.nsISSLStatusProvider]), + securityState: wpl.STATE_IS_SECURE, + errorCode: 0, + SSLStatus: { + cipherSuite: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", + protocolVersion: 3, // TLS_VERSION_1_2 + serverCert: { + validity: {} + }, + } +}; + +const MockHttpInfo = { + hostname: "include-subdomains.pinning.example.com", + private: false, +}; + +function run_test() { + Services.prefs.setIntPref("security.cert_pinning.enforcement_level", 1); + let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, MockHttpInfo); + equal(result.hpkp, true, "Static HPKP detected."); +} diff --git a/devtools/shared/webconsole/test/unit/test_security-info-weakness-reasons.js b/devtools/shared/webconsole/test/unit/test_security-info-weakness-reasons.js new file mode 100644 index 000000000..f91d8049e --- /dev/null +++ b/devtools/shared/webconsole/test/unit/test_security-info-weakness-reasons.js @@ -0,0 +1,47 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Tests that NetworkHelper.getReasonsForWeakness returns correct reasons for +// weak requests. + +const { require } = Components.utils.import("resource://devtools/shared/Loader.jsm", {}); + +Object.defineProperty(this, "NetworkHelper", { + get: function () { + return require("devtools/shared/webconsole/network-helper"); + }, + configurable: true, + writeable: false, + enumerable: true +}); + +var Ci = Components.interfaces; +const wpl = Ci.nsIWebProgressListener; +const TEST_CASES = [ + { + description: "weak cipher", + input: wpl.STATE_IS_BROKEN | wpl.STATE_USES_WEAK_CRYPTO, + expected: ["cipher"] + }, { + description: "only STATE_IS_BROKEN flag", + input: wpl.STATE_IS_BROKEN, + expected: [] + }, { + description: "only STATE_IS_SECURE flag", + input: wpl.STATE_IS_SECURE, + expected: [] + }, +]; + +function run_test() { + do_print("Testing NetworkHelper.getReasonsForWeakness."); + + for (let {description, input, expected} of TEST_CASES) { + do_print("Testing " + description); + + deepEqual(NetworkHelper.getReasonsForWeakness(input), expected, + "Got the expected reasons for weakness."); + } +} diff --git a/devtools/shared/webconsole/test/unit/test_throttle.js b/devtools/shared/webconsole/test/unit/test_throttle.js new file mode 100644 index 000000000..fa8b26b61 --- /dev/null +++ b/devtools/shared/webconsole/test/unit/test_throttle.js @@ -0,0 +1,140 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; +const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const promise = require("promise"); +const { NetworkThrottleManager } = + require("devtools/shared/webconsole/throttle"); +const nsIScriptableInputStream = Ci.nsIScriptableInputStream; + +function TestStreamListener() { + this.state = "initial"; +} +TestStreamListener.prototype = { + onStartRequest: function() { + this.setState("start"); + }, + + onStopRequest: function() { + this.setState("stop"); + }, + + onDataAvailable: function(request, context, inputStream, offset, count) { + const sin = Components.classes["@mozilla.org/scriptableinputstream;1"] + .createInstance(nsIScriptableInputStream); + sin.init(inputStream); + this.data = sin.read(count); + this.setState("data"); + }, + + setState: function(state) { + this.state = state; + if (this._deferred) { + this._deferred.resolve(state); + this._deferred = null; + } + }, + + onStateChanged: function() { + if (!this._deferred) { + this._deferred = promise.defer(); + } + return this._deferred.promise; + } +}; + +function TestChannel() { + this.state = "initial"; + this.testListener = new TestStreamListener(); + this._throttleQueue = null; +} +TestChannel.prototype = { + QueryInterface: function() { + return this; + }, + + get throttleQueue() { + return this._throttleQueue; + }, + + set throttleQueue(q) { + this._throttleQueue = q; + this.state = "throttled"; + }, + + setNewListener: function(listener) { + this.listener = listener; + this.state = "listener"; + return this.testListener; + }, +}; + +add_task(function*() { + let throttler = new NetworkThrottleManager({ + roundTripTimeMean: 1, + roundTripTimeMax: 1, + downloadBPSMean: 500, + downloadBPSMax: 500, + uploadBPSMean: 500, + uploadBPSMax: 500, + }); + + let uploadChannel = new TestChannel(); + throttler.manageUpload(uploadChannel); + equal(uploadChannel.state, "throttled", + "NetworkThrottleManager set throttleQueue"); + + let downloadChannel = new TestChannel(); + let testListener = downloadChannel.testListener; + + let listener = throttler.manage(downloadChannel); + equal(downloadChannel.state, "listener", + "NetworkThrottleManager called setNewListener"); + + equal(testListener.state, "initial", "test listener in initial state"); + + // This method must be passed through immediately. + listener.onStartRequest(null, null); + equal(testListener.state, "start", "test listener started"); + + const TEST_INPUT = "hi bob"; + + let testStream = Cc["@mozilla.org/storagestream;1"] + .createInstance(Ci.nsIStorageStream); + testStream.init(512, 512); + let out = testStream.getOutputStream(0); + out.write(TEST_INPUT, TEST_INPUT.length); + out.close(); + let testInputStream = testStream.newInputStream(0); + + let activityDistributor = + Cc["@mozilla.org/network/http-activity-distributor;1"] + .getService(Ci.nsIHttpActivityDistributor); + let activitySeen = false; + listener.addActivityCallback(() => activitySeen = true, null, null, null, + activityDistributor + .ACTIVITY_SUBTYPE_RESPONSE_COMPLETE, + null, TEST_INPUT.length, null); + + // onDataAvailable is required to immediately read the data. + listener.onDataAvailable(null, null, testInputStream, 0, 6); + equal(testInputStream.available(), 0, "no more data should be available"); + equal(testListener.state, "start", + "test listener should not have received data"); + equal(activitySeen, false, "activity not distributed yet"); + + let newState = yield testListener.onStateChanged(); + equal(newState, "data", "test listener received data"); + equal(testListener.data, TEST_INPUT, "test listener received all the data"); + equal(activitySeen, true, "activity has been distributed"); + + let onChange = testListener.onStateChanged(); + listener.onStopRequest(null, null, null); + newState = yield onChange; + equal(newState, "stop", "onStateChanged reported"); +}); diff --git a/devtools/shared/webconsole/test/unit/xpcshell.ini b/devtools/shared/webconsole/test/unit/xpcshell.ini new file mode 100644 index 000000000..083950834 --- /dev/null +++ b/devtools/shared/webconsole/test/unit/xpcshell.ini @@ -0,0 +1,17 @@ +[DEFAULT] +tags = devtools +head = +tail = +firefox-appdir = browser +skip-if = toolkit == 'android' +support-files = + +[test_js_property_provider.js] +[test_network_helper.js] +[test_security-info-certificate.js] +[test_security-info-parser.js] +[test_security-info-protocol-version.js] +[test_security-info-state.js] +[test_security-info-static-hpkp.js] +[test_security-info-weakness-reasons.js] +[test_throttle.js] diff --git a/devtools/shared/webconsole/throttle.js b/devtools/shared/webconsole/throttle.js new file mode 100644 index 000000000..3a875ee24 --- /dev/null +++ b/devtools/shared/webconsole/throttle.js @@ -0,0 +1,418 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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, Ci, Cu, Cc} = require("chrome"); + +const ArrayBufferInputStream = CC("@mozilla.org/io/arraybuffer-input-stream;1", + "nsIArrayBufferInputStream"); +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", "setInputStream"); + +loader.lazyServiceGetter(this, "gActivityDistributor", + "@mozilla.org/network/http-activity-distributor;1", + "nsIHttpActivityDistributor"); + +const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm"); +const {setTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {}); + +/** + * Construct a new nsIStreamListener that buffers data and provides a + * method to notify another listener when data is available. This is + * used to throttle network data on a per-channel basis. + * + * After construction, @see setOriginalListener must be called on the + * new object. + * + * @param {NetworkThrottleQueue} queue the NetworkThrottleQueue to + * which status changes should be reported + */ +function NetworkThrottleListener(queue) { + this.queue = queue; + this.pendingData = []; + this.pendingException = null; + this.offset = 0; + this.responseStarted = false; + this.activities = {}; +} + +NetworkThrottleListener.prototype = { + QueryInterface: + XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIInterfaceRequestor, + Ci.nsISupports]), + + /** + * Set the original listener for this object. The original listener + * will receive requests from this object when the queue allows data + * through. + * + * @param {nsIStreamListener} originalListener the original listener + * for the channel, to which all requests will be sent + */ + setOriginalListener: function (originalListener) { + this.originalListener = originalListener; + }, + + /** + * @see nsIStreamListener.onStartRequest. + */ + onStartRequest: function (request, context) { + this.originalListener.onStartRequest(request, context); + this.queue.start(this); + }, + + /** + * @see nsIStreamListener.onStopRequest. + */ + onStopRequest: function (request, context, statusCode) { + this.pendingData.push({request, context, statusCode}); + this.queue.dataAvailable(this); + }, + + /** + * @see nsIStreamListener.onDataAvailable. + */ + onDataAvailable: function (request, context, inputStream, offset, count) { + if (this.pendingException) { + throw this.pendingException; + } + + const bin = new BinaryInputStream(inputStream); + const bytes = new ArrayBuffer(count); + bin.readArrayBuffer(count, bytes); + + const stream = new ArrayBufferInputStream(); + stream.setData(bytes, 0, count); + + this.pendingData.push({request, context, stream, count}); + this.queue.dataAvailable(this); + }, + + /** + * Allow some buffered data from this object to be forwarded to this + * object's originalListener. + * + * @param {Number} bytesPermitted The maximum number of bytes + * permitted to be sent. + * @return {Object} an object of the form {length, done}, where + * |length| is the number of bytes actually forwarded, and + * |done| is a boolean indicating whether this particular + * request has been completed. (A NetworkThrottleListener + * may be queued multiple times, so this does not mean that + * all available data has been sent.) + */ + sendSomeData: function (bytesPermitted) { + if (this.pendingData.length === 0) { + // Shouldn't happen. + return {length: 0, done: true}; + } + + const {request, context, stream, count, statusCode} = this.pendingData[0]; + + if (statusCode !== undefined) { + this.pendingData.shift(); + this.originalListener.onStopRequest(request, context, statusCode); + return {length: 0, done: true}; + } + + if (bytesPermitted > count) { + bytesPermitted = count; + } + + try { + this.originalListener.onDataAvailable(request, context, stream, + this.offset, bytesPermitted); + } catch (e) { + this.pendingException = e; + } + + let done = false; + if (bytesPermitted === count) { + this.pendingData.shift(); + done = true; + } else { + this.pendingData[0].count -= bytesPermitted; + } + + this.offset += bytesPermitted; + // Maybe our state has changed enough to emit an event. + this.maybeEmitEvents(); + + return {length: bytesPermitted, done}; + }, + + /** + * Return the number of pending data requests available for this + * listener. + */ + pendingCount: function () { + return this.pendingData.length; + }, + + /** + * This is called when an http activity event is delivered. This + * object delays the event until the appropriate moment. + */ + addActivityCallback: function (callback, httpActivity, channel, activityType, + activitySubtype, timestamp, extraSizeData, + extraStringData) { + let datum = {callback, httpActivity, channel, activityType, + activitySubtype, extraSizeData, + extraStringData}; + this.activities[activitySubtype] = datum; + + if (activitySubtype === + gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE) { + this.totalSize = extraSizeData; + } + + this.maybeEmitEvents(); + }, + + /** + * This is called for a download throttler when the latency timeout + * has ended. + */ + responseStart: function () { + this.responseStarted = true; + this.maybeEmitEvents(); + }, + + /** + * Check our internal state and emit any http activity events as + * needed. Note that we wait until both our internal state has + * changed and we've received the real http activity event from + * platform. This approach ensures we can both pass on the correct + * data from the original event, and update the reported time to be + * consistent with the delay we're introducing. + */ + maybeEmitEvents: function () { + if (this.responseStarted) { + this.maybeEmit(gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_START); + this.maybeEmit(gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_HEADER); + } + + if (this.totalSize !== undefined && this.offset >= this.totalSize) { + this.maybeEmit(gActivityDistributor.ACTIVITY_SUBTYPE_RESPONSE_COMPLETE); + this.maybeEmit(gActivityDistributor.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE); + } + }, + + /** + * Emit an event for |code|, if the appropriate entry in + * |activities| is defined. + */ + maybeEmit: function (code) { + if (this.activities[code] !== undefined) { + let {callback, httpActivity, channel, activityType, + activitySubtype, extraSizeData, + extraStringData} = this.activities[code]; + let now = Date.now() * 1000; + callback(httpActivity, channel, activityType, activitySubtype, + now, extraSizeData, extraStringData); + this.activities[code] = undefined; + } + }, +}; + +/** + * Construct a new queue that can be used to throttle the network for + * a group of related network requests. + * + * meanBPS {Number} Mean bytes per second. + * maxBPS {Number} Maximum bytes per second. + * roundTripTimeMean {Number} Mean round trip time in milliseconds. + * roundTripTimeMax {Number} Maximum round trip time in milliseconds. + */ +function NetworkThrottleQueue(meanBPS, maxBPS, + roundTripTimeMean, roundTripTimeMax) { + this.meanBPS = meanBPS; + this.maxBPS = maxBPS; + this.roundTripTimeMean = roundTripTimeMean; + this.roundTripTimeMax = roundTripTimeMax; + + this.pendingRequests = new Set(); + this.downloadQueue = []; + this.previousReads = []; + + this.pumping = false; +} + +NetworkThrottleQueue.prototype = { + /** + * A helper function that, given a mean and a maximum, returns a + * random integer between (mean - (max - mean)) and max. + */ + random: function (mean, max) { + return mean - (max - mean) + Math.floor(2 * (max - mean) * Math.random()); + }, + + /** + * A helper function that lets the indicating listener start sending + * data. This is called after the initial round trip time for the + * listener has elapsed. + */ + allowDataFrom: function (throttleListener) { + throttleListener.responseStart(); + this.pendingRequests.delete(throttleListener); + const count = throttleListener.pendingCount(); + for (let i = 0; i < count; ++i) { + this.downloadQueue.push(throttleListener); + } + this.pump(); + }, + + /** + * Notice a new listener object. This is called by the + * NetworkThrottleListener when the request has started. Initially + * a new listener object is put into a "pending" state, until the + * round-trip time has elapsed. This is used to simulate latency. + * + * @param {NetworkThrottleListener} throttleListener the new listener + */ + start: function (throttleListener) { + this.pendingRequests.add(throttleListener); + let delay = this.random(this.roundTripTimeMean, this.roundTripTimeMax); + if (delay > 0) { + setTimeout(() => this.allowDataFrom(throttleListener), delay); + } else { + this.allowDataFrom(throttleListener); + } + }, + + /** + * Note that new data is available for a given listener. Each time + * data is available, the listener will be re-queued. + * + * @param {NetworkThrottleListener} throttleListener the listener + * which has data available. + */ + dataAvailable: function (throttleListener) { + if (!this.pendingRequests.has(throttleListener)) { + this.downloadQueue.push(throttleListener); + this.pump(); + } + }, + + /** + * An internal function that permits individual listeners to send + * data. + */ + pump: function () { + // A redirect will cause two NetworkThrottleListeners to be on a + // listener chain. In this case, we might recursively call into + // this method. Avoid infinite recursion here. + if (this.pumping) { + return; + } + this.pumping = true; + + const now = Date.now(); + const oneSecondAgo = now - 1000; + + while (this.previousReads.length && + this.previousReads[0].when < oneSecondAgo) { + this.previousReads.shift(); + } + + const totalBytes = this.previousReads.reduce((sum, elt) => { + return sum + elt.numBytes; + }, 0); + + let thisSliceBytes = this.random(this.meanBPS, this.maxBPS); + if (totalBytes < thisSliceBytes) { + thisSliceBytes -= totalBytes; + let readThisTime = 0; + while (thisSliceBytes > 0 && this.downloadQueue.length) { + let {length, done} = this.downloadQueue[0].sendSomeData(thisSliceBytes); + thisSliceBytes -= length; + readThisTime += length; + if (done) { + this.downloadQueue.shift(); + } + } + this.previousReads.push({when: now, numBytes: readThisTime}); + } + + // If there is more data to download, then schedule ourselves for + // one second after the oldest previous read. + if (this.downloadQueue.length) { + const when = this.previousReads[0].when + 1000; + setTimeout(this.pump.bind(this), when - now); + } + + this.pumping = false; + }, +}; + +/** + * Construct a new object that can be used to throttle the network for + * a group of related network requests. + * + * @param {Object} An object with the following attributes: + * roundTripTimeMean {Number} Mean round trip time in milliseconds. + * roundTripTimeMax {Number} Maximum round trip time in milliseconds. + * downloadBPSMean {Number} Mean bytes per second for downloads. + * downloadBPSMax {Number} Maximum bytes per second for downloads. + * uploadBPSMean {Number} Mean bytes per second for uploads. + * uploadBPSMax {Number} Maximum bytes per second for uploads. + * + * Download throttling will not be done if downloadBPSMean and + * downloadBPSMax are <= 0. Upload throttling will not be done if + * uploadBPSMean and uploadBPSMax are <= 0. + */ +function NetworkThrottleManager({roundTripTimeMean, roundTripTimeMax, + downloadBPSMean, downloadBPSMax, + uploadBPSMean, uploadBPSMax}) { + if (downloadBPSMax <= 0 && downloadBPSMean <= 0) { + this.downloadQueue = null; + } else { + this.downloadQueue = + new NetworkThrottleQueue(downloadBPSMean, downloadBPSMax, + roundTripTimeMean, roundTripTimeMax); + } + if (uploadBPSMax <= 0 && uploadBPSMean <= 0) { + this.uploadQueue = null; + } else { + this.uploadQueue = Cc["@mozilla.org/network/throttlequeue;1"] + .createInstance(Ci.nsIInputChannelThrottleQueue); + this.uploadQueue.init(uploadBPSMean, uploadBPSMax); + } +} +exports.NetworkThrottleManager = NetworkThrottleManager; + +NetworkThrottleManager.prototype = { + /** + * Create a new NetworkThrottleListener for a given channel and + * install it using |setNewListener|. + * + * @param {nsITraceableChannel} channel the channel to manage + * @return {NetworkThrottleListener} the new listener, or null if + * download throttling is not being done. + */ + manage: function (channel) { + if (this.downloadQueue) { + let listener = new NetworkThrottleListener(this.downloadQueue); + let originalListener = channel.setNewListener(listener); + listener.setOriginalListener(originalListener); + return listener; + } + return null; + }, + + /** + * Throttle uploads taking place on the given channel. + * + * @param {nsITraceableChannel} channel the channel to manage + */ + manageUpload: function (channel) { + if (this.uploadQueue) { + channel = channel.QueryInterface(Ci.nsIThrottledInputChannel); + channel.throttleQueue = this.uploadQueue; + } + }, +}; |