tor-browser

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

log.sys.mjs (8068B)


      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 import { WindowGlobalBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/WindowGlobalBiDiModule.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  ConsoleAPIListener:
     11    "chrome://remote/content/shared/listeners/ConsoleAPIListener.sys.mjs",
     12  ConsoleListener:
     13    "chrome://remote/content/shared/listeners/ConsoleListener.sys.mjs",
     14  isChromeFrame: "chrome://remote/content/shared/Stack.sys.mjs",
     15  OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
     16  setDefaultSerializationOptions:
     17    "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
     18 });
     19 
     20 class LogModule extends WindowGlobalBiDiModule {
     21  #consoleAPIListener;
     22  #consoleMessageListener;
     23  #subscribedEvents;
     24 
     25  constructor(messageHandler) {
     26    super(messageHandler);
     27 
     28    // Create the console-api listener and listen on "message" events.
     29    this.#consoleAPIListener = new lazy.ConsoleAPIListener(
     30      this.messageHandler.innerWindowId
     31    );
     32    this.#consoleAPIListener.on("message", this.#onConsoleAPIMessage);
     33 
     34    // Create the console listener and listen on error messages.
     35    this.#consoleMessageListener = new lazy.ConsoleListener(
     36      this.messageHandler.innerWindowId
     37    );
     38    this.#consoleMessageListener.on("error", this.#onJavaScriptError);
     39 
     40    // Set of event names which have active subscriptions.
     41    this.#subscribedEvents = new Set();
     42  }
     43 
     44  destroy() {
     45    this.#consoleAPIListener.off("message", this.#onConsoleAPIMessage);
     46    this.#consoleAPIListener.destroy();
     47    this.#consoleMessageListener.off("error", this.#onJavaScriptError);
     48    this.#consoleMessageListener.destroy();
     49 
     50    this.#subscribedEvents = null;
     51  }
     52 
     53  #buildSource(realm) {
     54    return {
     55      realm: realm.id,
     56      context: this.messageHandler.context,
     57    };
     58  }
     59 
     60  /**
     61   * Map the internal stacktrace representation to a WebDriver BiDi
     62   * compatible one.
     63   *
     64   * Currently chrome frames will be filtered out until chrome scope
     65   * is supported (bug 1722679).
     66   *
     67   * @param {Array<StackFrame>=} stackTrace
     68   *     Stack frames to process.
     69   *
     70   * @returns {object=} Object, containing the list of frames as `callFrames`.
     71   */
     72  #buildStackTrace(stackTrace) {
     73    if (stackTrace == undefined) {
     74      return undefined;
     75    }
     76 
     77    const callFrames = stackTrace
     78      .filter(frame => !lazy.isChromeFrame(frame))
     79      .map(frame => {
     80        return {
     81          columnNumber: frame.columnNumber - 1,
     82          functionName: frame.functionName,
     83          lineNumber: frame.lineNumber - 1,
     84          url: frame.filename,
     85        };
     86      });
     87 
     88    return { callFrames };
     89  }
     90 
     91  #getLogEntryLevelFromConsoleMethod(method) {
     92    switch (method) {
     93      case "assert":
     94      case "error":
     95        return "error";
     96      case "debug":
     97      case "trace":
     98        return "debug";
     99      case "warn":
    100        return "warn";
    101      default:
    102        return "info";
    103    }
    104  }
    105 
    106  #onConsoleAPIMessage = (eventName, data = {}) => {
    107    const {
    108      // `arguments` cannot be used as variable name in functions
    109      arguments: messageArguments,
    110      // `level` corresponds to the console method used
    111      level: method,
    112      stacktrace,
    113      timeStamp,
    114    } = data;
    115 
    116    // Step numbers below refer to the specifications at
    117    //   https://w3c.github.io/webdriver-bidi/#event-log-entryAdded
    118 
    119    // Translate the console message method to a log.LogEntry level
    120    const logEntrylevel = this.#getLogEntryLevelFromConsoleMethod(method);
    121 
    122    // Use the message's timeStamp or fallback on the current time value.
    123    const timestamp = timeStamp || Date.now();
    124 
    125    // Start assembling the text representation of the message.
    126    let text = "";
    127 
    128    // Formatters have already been applied at this points.
    129    // message.arguments corresponds to the "formatted args" from the
    130    // specifications.
    131 
    132    // Concatenate all formatted arguments in text
    133    // TODO: For m1 we only support string arguments, so we rely on the builtin
    134    // toString for each argument which will be available in message.arguments.
    135    const args = messageArguments || [];
    136    text += args.map(String).join(" ");
    137 
    138    const defaultRealm = this.messageHandler.getRealm();
    139    const serializedArgs = [];
    140    const seenNodeIds = new Map();
    141 
    142    // Serialize each arg as remote value.
    143    for (const arg of args) {
    144      // Note that we can pass a default realm for now since realms are only
    145      // involved when creating object references, which will not happen with
    146      // OwnershipModel.None. This will be revisited in Bug 1742589.
    147      serializedArgs.push(
    148        this.serialize(
    149          Cu.waiveXrays(arg),
    150          lazy.setDefaultSerializationOptions(),
    151          lazy.OwnershipModel.None,
    152          defaultRealm,
    153          { seenNodeIds }
    154        )
    155      );
    156    }
    157 
    158    // Set source to an object which contains realm and browsing context.
    159    // TODO: Bug 1742589. Use an actual realm from which the event came from.
    160    const source = this.#buildSource(defaultRealm);
    161 
    162    // Set stack trace only for certain methods.
    163    let stackTrace;
    164    if (["assert", "error", "trace", "warn"].includes(method)) {
    165      stackTrace = this.#buildStackTrace(stacktrace);
    166    }
    167 
    168    // Build the ConsoleLogEntry
    169    const entry = {
    170      type: "console",
    171      method,
    172      source,
    173      args: serializedArgs,
    174      level: logEntrylevel,
    175      text,
    176      timestamp,
    177      stackTrace,
    178      _extraData: { seenNodeIds },
    179    };
    180 
    181    // TODO: Those steps relate to:
    182    // - emitting associated BrowsingContext. See log.entryAdded full support
    183    //   in https://bugzilla.mozilla.org/show_bug.cgi?id=1724669#c0
    184    // - handling cases where session doesn't exist or the event is not
    185    //   monitored. The implementation differs from the spec here because we
    186    //   only react to events if there is a session & if the session subscribed
    187    //   to those events.
    188 
    189    this.emitEvent("log.entryAdded", entry);
    190  };
    191 
    192  #onJavaScriptError = (eventName, data = {}) => {
    193    const { level, message, stacktrace, timeStamp } = data;
    194    const defaultRealm = this.messageHandler.getRealm();
    195 
    196    // Build the JavascriptLogEntry
    197    const entry = {
    198      type: "javascript",
    199      level,
    200      // TODO: Bug 1742589. Use an actual realm from which the event came from.
    201      source: this.#buildSource(defaultRealm),
    202      text: message,
    203      timestamp: timeStamp || Date.now(),
    204      stackTrace: this.#buildStackTrace(stacktrace),
    205    };
    206 
    207    this.emitEvent("log.entryAdded", entry);
    208  };
    209 
    210  #subscribeEvent(event) {
    211    if (event === "log.entryAdded") {
    212      this.#consoleAPIListener.startListening();
    213      this.#consoleMessageListener.startListening();
    214      this.#subscribedEvents.add(event);
    215    }
    216  }
    217 
    218  #unsubscribeEvent(event) {
    219    if (event === "log.entryAdded") {
    220      this.#consoleAPIListener.stopListening();
    221      this.#consoleMessageListener.stopListening();
    222      this.#subscribedEvents.delete(event);
    223    }
    224  }
    225 
    226  /**
    227   * Internal commands
    228   */
    229 
    230  _applySessionData(params) {
    231    // TODO: Bug 1775231. Move this logic to a shared module or an abstract
    232    // class.
    233    const { category } = params;
    234    if (category === "event") {
    235      const filteredSessionData = params.sessionData.filter(item =>
    236        this.messageHandler.matchesContext(item.contextDescriptor)
    237      );
    238      for (const event of this.#subscribedEvents.values()) {
    239        const hasSessionItem = filteredSessionData.some(
    240          item => item.value === event
    241        );
    242        // If there are no session items for this context, we should unsubscribe from the event.
    243        if (!hasSessionItem) {
    244          this.#unsubscribeEvent(event);
    245        }
    246      }
    247 
    248      // Subscribe to all events, which have an item in SessionData.
    249      for (const { value } of filteredSessionData) {
    250        this.#subscribeEvent(value);
    251      }
    252    }
    253  }
    254 }
    255 
    256 export const log = LogModule;