/* 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 { CONSOLE_WORKER_IDS, WebConsoleUtils, } = require("resource://devtools/server/actors/webconsole/utils.js"); // The window.console API observer /** * The window.console API observer. This allows the window.console API messages * to be sent to the remote Web Console instance. * * @constructor * @param nsIDOMWindow window * Optional - the window object for which we are created. This is used * for filtering out messages that belong to other windows. * @param Function handler * This function is invoked with one argument, the Console API message that comes * from the observer service, whenever a relevant console API call is received. * @param object filteringOptions * Optional - The filteringOptions that this listener should listen to: * - addonId: filter console messages based on the addonId. * - excludeMessagesBoundToWindow: Set to true to filter out messages that * are bound to a specific window. * - matchExactWindow: Set to true to match the messages on a specific window (when * `window` is defined) and not on the whole window tree. */ class ConsoleAPIListener { constructor( window, handler, { addonId, excludeMessagesBoundToWindow, matchExactWindow } = {} ) { this.window = window; this.handler = handler; this.addonId = addonId; this.excludeMessagesBoundToWindow = excludeMessagesBoundToWindow; this.matchExactWindow = matchExactWindow; if (this.window) { this.innerWindowId = WebConsoleUtils.getInnerWindowId(this.window); } } QueryInterface = ChromeUtils.generateQI([Ci.nsIObserver]); /** * The content window for which we listen to window.console API calls. * @type nsIDOMWindow */ window = null; /** * The function which is notified of window.console API calls. It is invoked with one * argument: the console API call object that comes from the ConsoleAPIStorage service. * * @type function */ handler = null; /** * The addonId that we listen for. If not null then only messages from this * console will be returned. */ addonId = null; /** * Initialize the window.console API listener. */ init() { const ConsoleAPIStorage = Cc[ "@mozilla.org/consoleAPI-storage;1" ].getService(Ci.nsIConsoleAPIStorage); // Note that the listener is process-wide. We will filter the messages as // needed, see onConsoleAPILogEvent(). this.onConsoleAPILogEvent = this.onConsoleAPILogEvent.bind(this); ConsoleAPIStorage.addLogEventListener( this.onConsoleAPILogEvent, // We create a principal here to get the privileged principal of this // script. Note that this is importantly *NOT* the principal of the // content we are observing, as that would not have access to the // message object created in ConsoleAPIStorage.jsm's scope. Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal) ); } /** * The console API message listener. When messages are received from the * ConsoleAPIStorage service we forward them to the remote Web Console instance. * * @param object message * The message object receives from the ConsoleAPIStorage service. */ onConsoleAPILogEvent(message) { if (!this.handler) { return; } // Here, wrappedJSObject is not a security wrapper but a property defined // by the XPCOM component which allows us to unwrap the XPCOM interface and // access the underlying JSObject. const apiMessage = message.wrappedJSObject; if (!this.isMessageRelevant(apiMessage)) { return; } this.handler(apiMessage); } /** * Given a message, return true if this window should show it and false * if it should be ignored. * * @param message * The message from the Storage Service * @return bool * Do we care about this message? */ isMessageRelevant(message) { const workerType = WebConsoleUtils.getWorkerType(message); if (this.window && workerType === "ServiceWorker") { // For messages from Service Workers, message.ID is the // scope, which can be used to determine whether it's controlling // a window. const scope = message.ID; if (!this.window.shouldReportForServiceWorkerScope(scope)) { return false; } } // innerID can be of different type: // - a number if the message is bound to a specific window // - a worker type ([Shared|Service]Worker) if the message comes from a worker // - a JSM filename // if we want to filter on a specific window, ignore all non-worker messages that // don't have a proper window id (for now, we receive the worker messages from the // main process so we still want to get them, although their innerID isn't a number). if (!workerType && typeof message.innerID !== "number" && this.window) { return false; } // Don't show ChromeWorker messages on WindowGlobal targets if (workerType && this.window && message.chromeContext) { return false; } if (typeof message.innerID == "number") { if ( this.excludeMessagesBoundToWindow && // If innerID is 0, the message isn't actually bound to a window. message.innerID ) { return false; } if (this.window) { const matchesWindow = this.matchExactWindow ? this.innerWindowId === message.innerID : WebConsoleUtils.getInnerWindowIDsForFrames(this.window).includes( message.innerID ); if (!matchesWindow) { // Not the same window! return false; } } } if (this.addonId) { // ConsoleAPI.jsm messages contains a consoleID, (and it is currently // used in Addon SDK add-ons), the standard 'console' object // (which is used in regular webpages and in WebExtensions pages) // contains the originAttributes of the source document principal. // Filtering based on the originAttributes used by // the Console API object. if (message.addonId == this.addonId) { return true; } // Filtering based on the old-style consoleID property used by // the legacy Console JSM module. if (message.consoleID && message.consoleID == `addon/${this.addonId}`) { return true; } return false; } return true; } /** * Get the cached messages for the current inner window and its (i)frames. * * @param boolean [includePrivate=false] * Tells if you want to also retrieve messages coming from private * windows. Defaults to false. * @return array * The array of cached messages. */ getCachedMessages(includePrivate = false) { let messages = []; const ConsoleAPIStorage = Cc[ "@mozilla.org/consoleAPI-storage;1" ].getService(Ci.nsIConsoleAPIStorage); // if !this.window, we're in a browser console. Retrieve all events // for filtering based on privacy. if (!this.window) { messages = ConsoleAPIStorage.getEvents(); } else if (this.matchExactWindow) { messages = ConsoleAPIStorage.getEvents(this.innerWindowId); } else { WebConsoleUtils.getInnerWindowIDsForFrames(this.window).forEach(id => { messages = messages.concat(ConsoleAPIStorage.getEvents(id)); }); } CONSOLE_WORKER_IDS.forEach(id => { messages = messages.concat(ConsoleAPIStorage.getEvents(id)); }); messages = messages.filter(msg => { return this.isMessageRelevant(msg); }); if (includePrivate) { return messages; } return messages.filter(m => !m.private); } /** * Destroy the console API listener. */ destroy() { const ConsoleAPIStorage = Cc[ "@mozilla.org/consoleAPI-storage;1" ].getService(Ci.nsIConsoleAPIStorage); ConsoleAPIStorage.removeLogEventListener(this.onConsoleAPILogEvent); this.window = this.handler = null; } } exports.ConsoleAPIListener = ConsoleAPIListener;