tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

console-messages.js (10728B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const Targets = require("devtools/server/actors/targets/index");
      8 
      9 const consoleAPIListenerModule = isWorker
     10  ? "devtools/server/actors/webconsole/worker-listeners"
     11  : "devtools/server/actors/webconsole/listeners/console-api";
     12 const { ConsoleAPIListener } = require(consoleAPIListenerModule);
     13 
     14 const {
     15  makeDebuggeeValue,
     16  createValueGripForTarget,
     17 } = require("devtools/server/actors/object/utils");
     18 
     19 const {
     20  getActorIdForInternalSourceId,
     21 } = require("devtools/server/actors/utils/dbg-source");
     22 
     23 loader.lazyRequireGetter(
     24  this,
     25  "isArray",
     26  "resource://devtools/server/actors/object/utils.js",
     27  true
     28 );
     29 
     30 loader.lazyRequireGetter(
     31  this,
     32  "isSupportedByConsoleTable",
     33  "resource://devtools/shared/webconsole/messages.js",
     34  true
     35 );
     36 
     37 /**
     38 * Start watching for all console messages related to a given Target Actor.
     39 * This will notify about existing console messages, but also the one created in future.
     40 *
     41 * @param TargetActor targetActor
     42 *        The target actor from which we should observe console messages
     43 * @param Object options
     44 *        Dictionary object with following attributes:
     45 *        - onAvailable: mandatory function
     46 *          This will be called for each resource.
     47 */
     48 class ConsoleMessageWatcher {
     49  async watch(targetActor, { onAvailable }) {
     50    this.targetActor = targetActor;
     51    this.onAvailable = onAvailable;
     52 
     53    // Bug 1642297: Maybe we could merge ConsoleAPI Listener into this module?
     54    const onConsoleAPICall = message => {
     55      onAvailable([prepareConsoleMessageForRemote(targetActor, message)]);
     56    };
     57 
     58    const isTargetActorContentProcess =
     59      targetActor.targetType === Targets.TYPES.PROCESS;
     60 
     61    // Only consider messages from a given window for all FRAME targets (this includes
     62    // WebExt and ParentProcess which inherits from WindowGlobalTargetActor)
     63    // But ParentProcess should be ignored as we want all messages emitted directly from
     64    // that process (window and window-less).
     65    // To do that we pass a null window and ConsoleAPIListener will catch everything.
     66    const messagesShouldMatchWindow =
     67      targetActor.targetType === Targets.TYPES.FRAME &&
     68      targetActor.typeName != "parentProcessTarget";
     69    const window = messagesShouldMatchWindow ? targetActor.window : null;
     70 
     71    // If we should match messages for a given window but for some reason, targetActor.window
     72    // did not return a window, bail out. Otherwise we wouldn't have anything to match against
     73    // and would consume all the messages, which could lead to issue (e.g. infinite loop,
     74    // see Bug 1828026).
     75    if (messagesShouldMatchWindow && !window) {
     76      return;
     77    }
     78 
     79    const listener = new ConsoleAPIListener(window, onConsoleAPICall, {
     80      excludeMessagesBoundToWindow: isTargetActorContentProcess,
     81      matchExactWindow: targetActor.ignoreSubFrames,
     82      addonId:
     83        targetActor.targetType === Targets.TYPES.CONTENT_SCRIPT
     84          ? targetActor.addonId
     85          : null,
     86    });
     87    this.listener = listener;
     88    listener.init();
     89 
     90    // It can happen that the targetActor does not have a window reference (e.g. in worker
     91    // thread, targetActor exposes a targetGlobal property which isn't a Window object)
     92    const winStartTime =
     93      targetActor.window?.performance?.timing?.navigationStart || 0;
     94 
     95    const cachedMessages = listener.getCachedMessages(!targetActor.isRootActor);
     96    const messages = [];
     97    // Filter out messages that came from a ServiceWorker but happened
     98    // before the page was requested.
     99    for (const message of cachedMessages) {
    100      if (
    101        message.innerID === "ServiceWorker" &&
    102        winStartTime > message.timeStamp
    103      ) {
    104        continue;
    105      }
    106      messages.push(prepareConsoleMessageForRemote(targetActor, message));
    107    }
    108    onAvailable(messages);
    109  }
    110 
    111  /**
    112   * Stop watching for console messages.
    113   */
    114  destroy() {
    115    if (this.listener) {
    116      this.listener.destroy();
    117      this.listener = null;
    118    }
    119    this.targetActor = null;
    120    this.onAvailable = null;
    121  }
    122 
    123  /**
    124   * Spawn some custom console messages.
    125   * This is used for example for log points and JS tracing.
    126   *
    127   * @param Array<Object> messages
    128   *        A list of fake nsIConsoleMessage, which looks like the one being generated by
    129   *        the platform API.
    130   * @param boolean argumentsAreRawObjects
    131   *        Tells whether the messages' arguments properties contain raw or debugger objects.
    132   */
    133  emitMessages(messages, argumentsAreRawObjects = true) {
    134    if (!this.listener) {
    135      throw new Error("This target actor isn't listening to console messages");
    136    }
    137    this.onAvailable(
    138      messages.map(message => {
    139        if (!message.timeStamp) {
    140          throw new Error("timeStamp property is mandatory");
    141        }
    142 
    143        return prepareConsoleMessageForRemote(
    144          this.targetActor,
    145          message,
    146          argumentsAreRawObjects
    147        );
    148      })
    149    );
    150  }
    151 }
    152 
    153 module.exports = ConsoleMessageWatcher;
    154 
    155 /**
    156 * Return the properties needed to display the appropriate table for a given
    157 * console.table call.
    158 * This function does a little more than creating an ObjectActor for the first
    159 * parameter of the message. When layout out the console table in the output, we want
    160 * to be able to look into sub-properties so the table can have a different layout (
    161 * for arrays of arrays, objects with objects properties, arrays of objects, …).
    162 * So here we need to retrieve the properties of the first parameter, and also all the
    163 * sub-properties we might need.
    164 *
    165 * @param {TargetActor} targetActor: The Target Actor from which this object originates.
    166 * @param {object} result: The console.table message.
    167 * @returns {object} An object containing the properties of the first argument of the
    168 *                   console.table call.
    169 */
    170 function getConsoleTableMessageItems(targetActor, result) {
    171  const [tableItemGrip] = result.arguments;
    172  const dataType = tableItemGrip.class;
    173  const needEntries = ["Map", "WeakMap", "Set", "WeakSet"].includes(dataType);
    174  const ignoreNonIndexedProperties = isArray(tableItemGrip);
    175 
    176  const tableItemActor = targetActor.objectsPool.getActorByID(
    177    tableItemGrip.actor
    178  );
    179  if (!tableItemActor) {
    180    return null;
    181  }
    182 
    183  // Retrieve the properties (or entries for Set/Map) of the console table first arg.
    184  const iterator = needEntries
    185    ? tableItemActor.enumEntries()
    186    : tableItemActor.enumProperties({
    187        ignoreNonIndexedProperties,
    188      });
    189  const { ownProperties } = iterator.all();
    190 
    191  // The iterator returns a descriptor for each property, wherein the value could be
    192  // in one of those sub-property.
    193  const descriptorKeys = ["safeGetterValues", "getterValue", "value"];
    194 
    195  Object.values(ownProperties).forEach(desc => {
    196    if (typeof desc !== "undefined") {
    197      descriptorKeys.forEach(key => {
    198        if (desc && desc.hasOwnProperty(key)) {
    199          const grip = desc[key];
    200 
    201          // We need to load sub-properties as well to render the table in a nice way.
    202          const actor =
    203            grip && targetActor.objectsPool.getActorByID(grip.actor);
    204          if (actor && typeof actor.enumProperties === "function") {
    205            const res = actor
    206              .enumProperties({
    207                ignoreNonIndexedProperties: isArray(grip),
    208              })
    209              .all();
    210            if (res?.ownProperties) {
    211              desc[key].ownProperties = res.ownProperties;
    212            }
    213          }
    214        }
    215      });
    216    }
    217  });
    218 
    219  return ownProperties;
    220 }
    221 
    222 /**
    223 * Prepare a message from the console API to be sent to the remote Web Console
    224 * instance.
    225 *
    226 * @param TargetActor targetActor
    227 *        The related target actor
    228 * @param object message
    229 *        The original message received from the console storage listener or emitMessages().
    230 * @param boolean argumentsAreRawObjects
    231 *        Tells whether the message's arguments property contains raw or debugger objects.
    232 * @return object
    233 *         The object that can be sent to the remote client.
    234 */
    235 function prepareConsoleMessageForRemote(
    236  targetActor,
    237  message,
    238  argumentsAreRawObjects = true
    239 ) {
    240  const result = {
    241    arguments: message.arguments
    242      ? message.arguments.map(obj => {
    243          const dbgObj = argumentsAreRawObjects
    244            ? makeDebuggeeValue(targetActor, obj)
    245            : obj;
    246          return createValueGripForTarget(targetActor, dbgObj);
    247        })
    248      : [],
    249 
    250    // The line is 1-based.
    251    lineNumber: message.lineNumber,
    252    // The column is also 1-based as it ultimately derivates from SavedFrame's 1-based column
    253    columnNumber: message.columnNumber,
    254 
    255    filename: message.filename,
    256    level: message.level,
    257 
    258    // messages emitted from Console.sys.mjs don't have a microSecondTimeStamp property
    259    timeStamp: message.microSecondTimeStamp
    260      ? message.microSecondTimeStamp / 1000
    261      : message.timeStamp || ChromeUtils.dateNow(),
    262    sourceId: getActorIdForInternalSourceId(targetActor, message.sourceId),
    263    innerWindowID: message.innerID,
    264  };
    265 
    266  // This can be a hot path when loading lots of messages, and it only make sense to
    267  // include the following properties in the message when they have a meaningful value.
    268  // Otherwise we simply don't include them so we save cycles in JSActor communication.
    269  if (message.chromeContext) {
    270    result.chromeContext = message.chromeContext;
    271  }
    272 
    273  if (message.counter) {
    274    result.counter = message.counter;
    275  }
    276  if (message.private) {
    277    result.private = message.private;
    278  }
    279  if (message.prefix) {
    280    result.prefix = message.prefix;
    281  }
    282 
    283  if (message.stacktrace) {
    284    result.stacktrace = message.stacktrace.map(frame => {
    285      return {
    286        ...frame,
    287        sourceId: getActorIdForInternalSourceId(targetActor, frame.sourceId),
    288      };
    289    });
    290  }
    291 
    292  if (message.styles && message.styles.length) {
    293    result.styles = message.styles.map(string => {
    294      return createValueGripForTarget(targetActor, string);
    295    });
    296  }
    297 
    298  if (message.timer) {
    299    result.timer = message.timer;
    300  }
    301 
    302  if (message.level === "table") {
    303    if (result && isSupportedByConsoleTable(result.arguments)) {
    304      const tableItems = getConsoleTableMessageItems(targetActor, result);
    305      if (tableItems) {
    306        result.arguments[0].ownProperties = tableItems;
    307        result.arguments[0].preview = null;
    308 
    309        // Only return the 2 first params.
    310        result.arguments = result.arguments.slice(0, 2);
    311      }
    312    }
    313    // NOTE: See transformConsoleAPICallResource for not-supported case.
    314  }
    315 
    316  return result;
    317 }