diff options
Diffstat (limited to 'devtools/client/webconsole/utils.js')
-rw-r--r-- | devtools/client/webconsole/utils.js | 395 |
1 files changed, 395 insertions, 0 deletions
diff --git a/devtools/client/webconsole/utils.js b/devtools/client/webconsole/utils.js new file mode 100644 index 000000000..ae2f1809f --- /dev/null +++ b/devtools/client/webconsole/utils.js @@ -0,0 +1,395 @@ +/* -*- 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, components} = require("chrome"); +const Services = require("Services"); +const {LocalizationHelper} = require("devtools/shared/l10n"); + +// Match the function name from the result of toString() or toSource(). +// +// Examples: +// (function foobar(a, b) { ... +// function foobar2(a) { ... +// function() { ... +const REGEX_MATCH_FUNCTION_NAME = /^\(?function\s+([^(\s]+)\s*\(/; + +// Number of terminal entries for the self-xss prevention to go away +const CONSOLE_ENTRY_THRESHOLD = 5; + +const CONSOLE_WORKER_IDS = exports.CONSOLE_WORKER_IDS = [ + "SharedWorker", + "ServiceWorker", + "Worker" +]; + +var WebConsoleUtils = { + + /** + * Wrap a string in an nsISupportsString object. + * + * @param string string + * @return nsISupportsString + */ + supportsString: function (string) { + let str = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + str.data = string; + return str; + }, + + /** + * Clone an object. + * + * @param object object + * The object you want cloned. + * @param boolean recursive + * Tells if you want to dig deeper into the object, to clone + * recursively. + * @param function [filter] + * Optional, filter function, called for every property. Three + * arguments are passed: key, value and object. Return true if the + * property should be added to the cloned object. Return false to skip + * the property. + * @return object + * The cloned object. + */ + cloneObject: function (object, recursive, filter) { + if (typeof object != "object") { + return object; + } + + let temp; + + if (Array.isArray(object)) { + temp = []; + Array.forEach(object, function (value, index) { + if (!filter || filter(index, value, object)) { + temp.push(recursive ? WebConsoleUtils.cloneObject(value) : value); + } + }); + } else { + temp = {}; + for (let key in object) { + let value = object[key]; + if (object.hasOwnProperty(key) && + (!filter || filter(key, value, object))) { + temp[key] = recursive ? WebConsoleUtils.cloneObject(value) : value; + } + } + } + + return temp; + }, + + /** + * Copies certain style attributes from one element to another. + * + * @param nsIDOMNode from + * The target node. + * @param nsIDOMNode to + * The destination node. + */ + copyTextStyles: function (from, to) { + let win = from.ownerDocument.defaultView; + let style = win.getComputedStyle(from); + to.style.fontFamily = style.getPropertyCSSValue("font-family").cssText; + to.style.fontSize = style.getPropertyCSSValue("font-size").cssText; + to.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText; + to.style.fontStyle = style.getPropertyCSSValue("font-style").cssText; + }, + + /** + * Create a grip for the given value. If the value is an object, + * an object wrapper will be created. + * + * @param mixed value + * The value you want to create a grip for, before sending it to the + * client. + * @param function objectWrapper + * If the value is an object then the objectWrapper function is + * invoked to give us an object grip. See this.getObjectGrip(). + * @return mixed + * The value grip. + */ + createValueGrip: function (value, objectWrapper) { + switch (typeof value) { + case "boolean": + return value; + case "string": + return objectWrapper(value); + case "number": + if (value === Infinity) { + return { type: "Infinity" }; + } else if (value === -Infinity) { + return { type: "-Infinity" }; + } else if (Number.isNaN(value)) { + return { type: "NaN" }; + } else if (!value && 1 / value === -Infinity) { + return { type: "-0" }; + } + return value; + case "undefined": + return { type: "undefined" }; + case "object": + if (value === null) { + return { type: "null" }; + } + // Fall through. + case "function": + return objectWrapper(value); + default: + console.error("Failed to provide a grip for value of " + typeof value + + ": " + value); + return null; + } + }, + + /** + * Determine if the given request mixes HTTP with HTTPS content. + * + * @param string request + * Location of the requested content. + * @param string location + * Location of the current page. + * @return boolean + * True if the content is mixed, false if not. + */ + isMixedHTTPSRequest: function (request, location) { + try { + let requestURI = Services.io.newURI(request, null, null); + let contentURI = Services.io.newURI(location, null, null); + return (contentURI.scheme == "https" && requestURI.scheme != "https"); + } catch (ex) { + return false; + } + }, + + /** + * Helper function to deduce the name of the provided function. + * + * @param funtion function + * The function whose name will be returned. + * @return string + * Function name. + */ + getFunctionName: function (func) { + let name = null; + if (func.name) { + name = func.name; + } else { + let desc; + try { + desc = func.getOwnPropertyDescriptor("displayName"); + } catch (ex) { + // Ignore. + } + if (desc && typeof desc.value == "string") { + name = desc.value; + } + } + if (!name) { + try { + let str = (func.toString() || func.toSource()) + ""; + name = (str.match(REGEX_MATCH_FUNCTION_NAME) || [])[1]; + } catch (ex) { + // Ignore. + } + } + return name; + }, + + /** + * Get the object class name. For example, the |window| object has the Window + * class name (based on [object Window]). + * + * @param object object + * The object you want to get the class name for. + * @return string + * The object class name. + */ + getObjectClassName: function (object) { + if (object === null) { + return "null"; + } + if (object === undefined) { + return "undefined"; + } + + let type = typeof object; + if (type != "object") { + // Grip class names should start with an uppercase letter. + return type.charAt(0).toUpperCase() + type.substr(1); + } + + let className; + + try { + className = ((object + "").match(/^\[object (\S+)\]$/) || [])[1]; + if (!className) { + className = ((object.constructor + "") + .match(/^\[object (\S+)\]$/) || [])[1]; + } + if (!className && typeof object.constructor == "function") { + className = this.getFunctionName(object.constructor); + } + } catch (ex) { + // Ignore. + } + + return className; + }, + + /** + * Check if the given value is a grip with an actor. + * + * @param mixed grip + * Value you want to check if it is a grip with an actor. + * @return boolean + * True if the given value is a grip with an actor. + */ + isActorGrip: function (grip) { + return grip && typeof (grip) == "object" && grip.actor; + }, + + /** + * Value of devtools.selfxss.count preference + * + * @type number + * @private + */ + _usageCount: 0, + get usageCount() { + if (WebConsoleUtils._usageCount < CONSOLE_ENTRY_THRESHOLD) { + WebConsoleUtils._usageCount = + Services.prefs.getIntPref("devtools.selfxss.count"); + if (Services.prefs.getBoolPref("devtools.chrome.enabled")) { + WebConsoleUtils.usageCount = CONSOLE_ENTRY_THRESHOLD; + } + } + return WebConsoleUtils._usageCount; + }, + set usageCount(newUC) { + if (newUC <= CONSOLE_ENTRY_THRESHOLD) { + WebConsoleUtils._usageCount = newUC; + Services.prefs.setIntPref("devtools.selfxss.count", newUC); + } + }, + /** + * The inputNode "paste" event handler generator. Helps prevent + * self-xss attacks + * + * @param nsIDOMElement inputField + * @param nsIDOMElement notificationBox + * @returns A function to be added as a handler to 'paste' and + *'drop' events on the input field + */ + pasteHandlerGen: function (inputField, notificationBox, msg, okstring) { + let handler = function (event) { + if (WebConsoleUtils.usageCount >= CONSOLE_ENTRY_THRESHOLD) { + inputField.removeEventListener("paste", handler); + inputField.removeEventListener("drop", handler); + return true; + } + if (notificationBox.getNotificationWithValue("selfxss-notification")) { + event.preventDefault(); + event.stopPropagation(); + return false; + } + + let notification = notificationBox.appendNotification(msg, + "selfxss-notification", null, + notificationBox.PRIORITY_WARNING_HIGH, null, + function (eventType) { + // Cleanup function if notification is dismissed + if (eventType == "removed") { + inputField.removeEventListener("keyup", pasteKeyUpHandler); + } + }); + + function pasteKeyUpHandler(event2) { + let value = inputField.value || inputField.textContent; + if (value.includes(okstring)) { + notificationBox.removeNotification(notification); + inputField.removeEventListener("keyup", pasteKeyUpHandler); + WebConsoleUtils.usageCount = CONSOLE_ENTRY_THRESHOLD; + } + } + inputField.addEventListener("keyup", pasteKeyUpHandler); + + event.preventDefault(); + event.stopPropagation(); + return false; + }; + return handler; + }, +}; + +exports.Utils = WebConsoleUtils; + +// Localization + +WebConsoleUtils.L10n = function (bundleURI) { + this._helper = new LocalizationHelper(bundleURI); +}; + +WebConsoleUtils.L10n.prototype = { + /** + * Generates a formatted timestamp string for displaying in console messages. + * + * @param integer [milliseconds] + * Optional, allows you to specify the timestamp in milliseconds since + * the UNIX epoch. + * @return string + * The timestamp formatted for display. + */ + timestampString: function (milliseconds) { + let d = new Date(milliseconds ? milliseconds : null); + let hours = d.getHours(), minutes = d.getMinutes(); + let seconds = d.getSeconds(); + milliseconds = d.getMilliseconds(); + let parameters = [hours, minutes, seconds, milliseconds]; + return this.getFormatStr("timestampFormat", parameters); + }, + + /** + * Retrieve a localized string. + * + * @param string name + * The string name you want from the Web Console string bundle. + * @return string + * The localized string. + */ + getStr: function (name) { + try { + return this._helper.getStr(name); + } catch (ex) { + console.error("Failed to get string: " + name); + throw ex; + } + }, + + /** + * Retrieve a localized string formatted with values coming from the given + * array. + * + * @param string name + * The string name you want from the Web Console string bundle. + * @param array array + * The array of values you want in the formatted string. + * @return string + * The formatted local string. + */ + getFormatStr: function (name, array) { + try { + return this._helper.getFormatStr(name, ...array); + } catch (ex) { + console.error("Failed to format string: " + name); + throw ex; + } + }, +}; |