/* -*- 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; } }, };