tor-browser

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

messages.js (34153B)


      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 l10n = require("resource://devtools/client/webconsole/utils/l10n.js");
      8 const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
      9 const {
     10  isSupportedByConsoleTable,
     11 } = require("resource://devtools/shared/webconsole/messages.js");
     12 
     13 loader.lazyRequireGetter(
     14  this,
     15  "getAdHocFrontOrPrimitiveGrip",
     16  "resource://devtools/client/fronts/object.js",
     17  true
     18 );
     19 
     20 loader.lazyRequireGetter(
     21  this,
     22  "TRACER_FIELDS_INDEXES",
     23  "resource://devtools/server/actors/tracer.js",
     24  true
     25 );
     26 
     27 loader.lazyRequireGetter(
     28  this,
     29  "TRACER_LOG_METHODS",
     30  "resource://devtools/shared/specs/tracer.js",
     31  true
     32 );
     33 
     34 // URL Regex, common idioms:
     35 //
     36 // Lead-in (URL):
     37 // (                     Capture because we need to know if there was a lead-in
     38 //                       character so we can include it as part of the text
     39 //                       preceding the match. We lack look-behind matching.
     40 //  ^|                   The URL can start at the beginning of the string.
     41 //  [\s(,;'"`“]          Or whitespace or some punctuation that does not imply
     42 //                       a context which would preclude a URL.
     43 // )
     44 //
     45 // We do not need a trailing look-ahead because our regex's will terminate
     46 // because they run out of characters they can eat.
     47 
     48 // What we do not attempt to have the regexp do:
     49 // - Avoid trailing '.' and ')' characters.  We let our greedy match absorb
     50 //   these, but have a separate regex for extra characters to leave off at the
     51 //   end.
     52 //
     53 // The Regex (apart from lead-in/lead-out):
     54 // (                     Begin capture of the URL
     55 //  (?:                  (potential detect beginnings)
     56 //   https?:\/\/|        Start with "http" or "https"
     57 //   www\d{0,3}[.][a-z0-9.\-]{2,249}|
     58 //                      Start with "www", up to 3 numbers, then "." then
     59 //                       something that looks domain-namey.  We differ from the
     60 //                       next case in that we do not constrain the top-level
     61 //                       domain as tightly and do not require a trailing path
     62 //                       indicator of "/".  This is IDN root compatible.
     63 //   [a-z0-9.\-]{2,250}[.][a-z]{2,4}\/
     64 //                       Detect a non-www domain, but requiring a trailing "/"
     65 //                       to indicate a path.  This only detects IDN domains
     66 //                       with a non-IDN root.  This is reasonable in cases where
     67 //                       there is no explicit http/https start us out, but
     68 //                       unreasonable where there is.  Our real fix is the bug
     69 //                       to port the Thunderbird/gecko linkification logic.
     70 //
     71 //                       Domain names can be up to 253 characters long, and are
     72 //                       limited to a-zA-Z0-9 and '-'.  The roots don't have
     73 //                       hyphens unless they are IDN roots.  Root zones can be
     74 //                       found here: http://www.iana.org/domains/root/db
     75 //  )
     76 //  [-\w.!~*'();,/?:@&=+$#%]*
     77 //                       path onwards. We allow the set of characters that
     78 //                       encodeURI does not escape plus the result of escaping
     79 //                       (so also '%')
     80 // )
     81 // eslint-disable-next-line max-len
     82 const urlRegex =
     83  /(^|[\s(,;'"`“])((?:https?:\/\/|www\d{0,3}[.][a-z0-9.\-]{2,249}|[a-z0-9.\-]{2,250}[.][a-z]{2,4}\/)[-\w.!~*'();,/?:@&=+$#%]*)/im;
     84 
     85 // Set of terminators that are likely to have been part of the context rather
     86 // than part of the URL and so should be uneaten. This is '(', ',', ';', plus
     87 // quotes and question end-ing punctuation and the potential permutations with
     88 // parentheses (english-specific).
     89 const uneatLastUrlCharsRegex = /(?:[),;.!?`'"]|[.!?]\)|\)[.!?])$/;
     90 
     91 const {
     92  MESSAGE_SOURCE,
     93  MESSAGE_TYPE,
     94  MESSAGE_LEVEL,
     95 } = require("resource://devtools/client/webconsole/constants.js");
     96 const {
     97  ConsoleMessage,
     98  NetworkEventMessage,
     99 } = require("resource://devtools/client/webconsole/types.js");
    100 
    101 function prepareMessage(resource, idGenerator, persistLogs) {
    102  if (!resource.source) {
    103    resource = transformResource(resource, persistLogs);
    104  }
    105 
    106  // The Tracer resource transformer may process some resource
    107  // which aren't translated into any item in the console (Tracer frames)
    108  if (resource) {
    109    resource.id = idGenerator.getNextId(resource);
    110  }
    111  return resource;
    112 }
    113 
    114 /**
    115 * Transforms a resource given its type.
    116 *
    117 * @param {object} resource: This can be either a simple RDP packet or an object emitted
    118 *                           by the Resource API.
    119 * @param {boolean} persistLogs: Value of the "Persist logs" setting
    120 */
    121 function transformResource(resource, persistLogs) {
    122  switch (resource.resourceType || resource.type) {
    123    case ResourceCommand.TYPES.CONSOLE_MESSAGE: {
    124      return transformConsoleAPICallResource(
    125        resource,
    126        persistLogs,
    127        resource.targetFront
    128      );
    129    }
    130 
    131    case ResourceCommand.TYPES.PLATFORM_MESSAGE: {
    132      return transformPlatformMessageResource(resource);
    133    }
    134 
    135    case ResourceCommand.TYPES.ERROR_MESSAGE: {
    136      return transformPageErrorResource(resource);
    137    }
    138 
    139    case ResourceCommand.TYPES.CSS_MESSAGE: {
    140      return transformCSSMessageResource(resource);
    141    }
    142 
    143    case ResourceCommand.TYPES.NETWORK_EVENT: {
    144      return transformNetworkEventResource(resource);
    145    }
    146 
    147    case ResourceCommand.TYPES.JSTRACER_STATE: {
    148      return transformTracerStateResource(resource);
    149    }
    150 
    151    case ResourceCommand.TYPES.JSTRACER_TRACE: {
    152      return transformTraceResource(resource);
    153    }
    154 
    155    case "will-navigate": {
    156      return transformNavigationMessagePacket(resource);
    157    }
    158 
    159    case "evaluationResult":
    160    default: {
    161      return transformEvaluationResultPacket(resource);
    162    }
    163  }
    164 }
    165 
    166 // eslint-disable-next-line complexity
    167 function transformConsoleAPICallResource(
    168  consoleMessageResource,
    169  persistLogs,
    170  targetFront
    171 ) {
    172  let { arguments: parameters, level: type, timer } = consoleMessageResource;
    173  let level = getLevelFromType(type);
    174  let messageText = null;
    175 
    176  // Special per-type conversion.
    177  switch (type) {
    178    case "clear":
    179      // We show a message to users when calls console.clear() is called.
    180      parameters = [
    181        l10n.getStr(persistLogs ? "preventedConsoleClear" : "consoleCleared"),
    182      ];
    183      break;
    184    case "count":
    185    case "countReset": {
    186      // Chrome RDP doesn't have a special type for count.
    187      type = MESSAGE_TYPE.LOG;
    188      const { counter } = consoleMessageResource;
    189 
    190      if (!counter) {
    191        // We don't show anything if we don't have counter data.
    192        type = MESSAGE_TYPE.NULL_MESSAGE;
    193      } else if (counter.error) {
    194        messageText = l10n.getFormatStr(counter.error, [counter.label]);
    195        level = MESSAGE_LEVEL.WARN;
    196        parameters = null;
    197      } else {
    198        const label = counter.label
    199          ? counter.label
    200          : l10n.getStr("noCounterLabel");
    201        messageText = `${label}: ${counter.count}`;
    202        parameters = null;
    203      }
    204      break;
    205    }
    206    case "timeStamp":
    207      type = MESSAGE_TYPE.NULL_MESSAGE;
    208      break;
    209    case "time":
    210      parameters = null;
    211      if (timer && timer.error) {
    212        messageText = l10n.getFormatStr(timer.error, [timer.name]);
    213        level = MESSAGE_LEVEL.WARN;
    214      } else {
    215        // We don't show anything for console.time calls to match Chrome's behaviour.
    216        type = MESSAGE_TYPE.NULL_MESSAGE;
    217      }
    218      break;
    219    case "timeLog":
    220    case "timeEnd":
    221      if (timer && timer.error) {
    222        parameters = null;
    223        messageText = l10n.getFormatStr(timer.error, [timer.name]);
    224        level = MESSAGE_LEVEL.WARN;
    225      } else if (timer) {
    226        // We show the duration to users when calls console.timeLog/timeEnd is called,
    227        // if corresponding console.time() was called before.
    228        const duration = Math.round(timer.duration * 100) / 100;
    229        if (type === "timeEnd") {
    230          messageText = l10n.getFormatStr("console.timeEnd", [
    231            timer.name,
    232            duration,
    233          ]);
    234          parameters = null;
    235        } else if (type === "timeLog") {
    236          const [, ...rest] = parameters;
    237          parameters = [
    238            l10n.getFormatStr("timeLog", [timer.name, duration]),
    239            ...rest,
    240          ];
    241        }
    242      } else {
    243        // If the `timer` property does not exists, we don't output anything.
    244        type = MESSAGE_TYPE.NULL_MESSAGE;
    245      }
    246      break;
    247    case "table":
    248      if (!isSupportedByConsoleTable(parameters)) {
    249        // If the class of the first parameter is not supported,
    250        // we handle the call as a simple console.log
    251        type = "log";
    252      }
    253      break;
    254    case "group":
    255      type = MESSAGE_TYPE.START_GROUP;
    256      if (parameters.length === 0) {
    257        parameters = [l10n.getStr("noGroupLabel")];
    258      }
    259      break;
    260    case "groupCollapsed":
    261      type = MESSAGE_TYPE.START_GROUP_COLLAPSED;
    262      if (parameters.length === 0) {
    263        parameters = [l10n.getStr("noGroupLabel")];
    264      }
    265      break;
    266    case "groupEnd":
    267      type = MESSAGE_TYPE.END_GROUP;
    268      parameters = null;
    269      break;
    270    case "dirxml":
    271      // Handle console.dirxml calls as simple console.log
    272      type = "log";
    273      break;
    274  }
    275 
    276  const frame = consoleMessageResource.filename
    277    ? {
    278        source: consoleMessageResource.filename,
    279        sourceId: consoleMessageResource.sourceId,
    280        // Both line and column are 1-based
    281        line: consoleMessageResource.lineNumber,
    282        column: consoleMessageResource.columnNumber,
    283      }
    284    : null;
    285 
    286  if (frame && (type === "logPointError" || type === "logPoint")) {
    287    frame.options = { logPoint: true };
    288  }
    289 
    290  return new ConsoleMessage({
    291    targetFront,
    292    source: MESSAGE_SOURCE.CONSOLE_API,
    293    type,
    294    level,
    295    parameters,
    296    messageText,
    297    stacktrace: consoleMessageResource.stacktrace
    298      ? consoleMessageResource.stacktrace
    299      : null,
    300    frame,
    301    timeStamp: consoleMessageResource.timeStamp,
    302    userProvidedStyles: consoleMessageResource.styles,
    303    prefix: consoleMessageResource.prefix,
    304    private: consoleMessageResource.private,
    305    chromeContext: consoleMessageResource.chromeContext,
    306  });
    307 }
    308 
    309 function transformNavigationMessagePacket(packet) {
    310  const { url } = packet;
    311  return new ConsoleMessage({
    312    source: MESSAGE_SOURCE.CONSOLE_FRONTEND,
    313    type: MESSAGE_TYPE.NAVIGATION_MARKER,
    314    level: MESSAGE_LEVEL.LOG,
    315    messageText: url
    316      ? l10n.getFormatStr("webconsole.navigated", [url])
    317      : l10n.getStr("webconsole.reloaded"),
    318    timeStamp: packet.timeStamp,
    319    allowRepeating: false,
    320  });
    321 }
    322 
    323 function transformPlatformMessageResource(platformMessageResource) {
    324  const { message, timeStamp, targetFront } = platformMessageResource;
    325  return new ConsoleMessage({
    326    targetFront,
    327    source: MESSAGE_SOURCE.CONSOLE_API,
    328    type: MESSAGE_TYPE.LOG,
    329    level: MESSAGE_LEVEL.LOG,
    330    messageText: message,
    331    timeStamp,
    332    chromeContext: true,
    333  });
    334 }
    335 
    336 function transformPageErrorResource(pageErrorResource, override = {}) {
    337  const { pageError, targetFront } = pageErrorResource;
    338  let level = MESSAGE_LEVEL.ERROR;
    339  if (pageError.warning) {
    340    level = MESSAGE_LEVEL.WARN;
    341  } else if (pageError.info) {
    342    level = MESSAGE_LEVEL.INFO;
    343  }
    344 
    345  const frame = pageError.sourceName
    346    ? {
    347        source: pageError.sourceName,
    348        sourceId: pageError.sourceId,
    349        // Both line and column are 1-based
    350        line: pageError.lineNumber,
    351        column: pageError.columnNumber,
    352      }
    353    : null;
    354 
    355  return new ConsoleMessage(
    356    Object.assign(
    357      {
    358        targetFront,
    359        innerWindowID: pageError.innerWindowID,
    360        source: MESSAGE_SOURCE.JAVASCRIPT,
    361        type: MESSAGE_TYPE.LOG,
    362        level,
    363        category: pageError.category,
    364        messageText: pageError.errorMessage,
    365        stacktrace: pageError.stacktrace ? pageError.stacktrace : null,
    366        frame,
    367        errorMessageName: pageError.errorMessageName,
    368        exceptionDocURL: pageError.exceptionDocURL,
    369        hasException: pageError.hasException,
    370        parameters: pageError.hasException ? [pageError.exception] : null,
    371        timeStamp: pageError.timeStamp,
    372        notes: pageError.notes,
    373        private: pageError.private,
    374        chromeContext: pageError.chromeContext,
    375        isPromiseRejection: pageError.isPromiseRejection,
    376      },
    377      override
    378    )
    379  );
    380 }
    381 
    382 function transformCSSMessageResource(cssMessageResource) {
    383  return transformPageErrorResource(cssMessageResource, {
    384    cssSelectors: cssMessageResource.cssSelectors,
    385    source: MESSAGE_SOURCE.CSS,
    386  });
    387 }
    388 
    389 function transformNetworkEventResource(networkEventResource) {
    390  return new NetworkEventMessage(networkEventResource);
    391 }
    392 
    393 function transformTraceResource(traceResource) {
    394  const { targetFront } = traceResource;
    395  const type = traceResource[TRACER_FIELDS_INDEXES.TYPE];
    396  const collectedFrames = targetFront.getJsTracerCollectedFramesArray();
    397  switch (type) {
    398    case "frame":
    399      collectedFrames.push(traceResource);
    400      return null;
    401    case "enter": {
    402      const [, prefix, frameIndex, timeStamp, depth, args] = traceResource;
    403      const frame = collectedFrames[frameIndex];
    404      return new ConsoleMessage({
    405        targetFront,
    406        source: MESSAGE_SOURCE.JSTRACER,
    407        frame: {
    408          source: frame[TRACER_FIELDS_INDEXES.FRAME_URL],
    409          sourceId: frame[TRACER_FIELDS_INDEXES.FRAME_SOURCEID],
    410          line: frame[TRACER_FIELDS_INDEXES.FRAME_LINE],
    411          // tracer's column is 0-based while frame uses 1-based numbers
    412          column: frame[TRACER_FIELDS_INDEXES.FRAME_COLUMN] + 1,
    413        },
    414        depth,
    415        implementation: frame[TRACER_FIELDS_INDEXES.FRAME_IMPLEMENTATION],
    416        displayName: frame[TRACER_FIELDS_INDEXES.FRAME_NAME],
    417        parameters: args
    418          ? args.map(p =>
    419              p ? getAdHocFrontOrPrimitiveGrip(p, targetFront) : p
    420            )
    421          : null,
    422        messageText: null,
    423        timeStamp,
    424        prefix,
    425        // Allow the identical frames to be coalesced into a unique message
    426        // with a repeatition counter so that we keep the output short in case of loops.
    427        allowRepeating: true,
    428      });
    429    }
    430    case "exit": {
    431      const [
    432        ,
    433        prefix,
    434        frameIndex,
    435        timeStamp,
    436        depth,
    437        relatedTraceId,
    438        returnedValue,
    439        why,
    440      ] = traceResource;
    441      const frame = collectedFrames[frameIndex];
    442      return new ConsoleMessage({
    443        targetFront,
    444        source: MESSAGE_SOURCE.JSTRACER,
    445        frame: {
    446          source: frame[TRACER_FIELDS_INDEXES.FRAME_URL],
    447          sourceId: frame[TRACER_FIELDS_INDEXES.FRAME_SOURCEID],
    448          line: frame[TRACER_FIELDS_INDEXES.FRAME_LINE],
    449          column: frame[TRACER_FIELDS_INDEXES.FRAME_COLUMN],
    450        },
    451        depth,
    452        implementation: frame[TRACER_FIELDS_INDEXES.FRAME_IMPLEMENTATION],
    453        displayName: frame[TRACER_FIELDS_INDEXES.FRAME_NAME],
    454        parameters: null,
    455        returnedValue:
    456          returnedValue != undefined
    457            ? getAdHocFrontOrPrimitiveGrip(returnedValue, targetFront)
    458            : null,
    459        relatedTraceId,
    460        why,
    461        messageText: null,
    462        timeStamp,
    463        prefix,
    464        // Allow the identical frames to be coallesced into a unique message
    465        // with a repeatition counter so that we keep the output short in case of loops.
    466        allowRepeating: true,
    467      });
    468    }
    469    case "dom-mutation": {
    470      const [
    471        ,
    472        prefix,
    473        frameIndex,
    474        timeStamp,
    475        depth,
    476        mutationType,
    477        mutationElement,
    478      ] = traceResource;
    479      const frame = collectedFrames[frameIndex];
    480      return new ConsoleMessage({
    481        targetFront,
    482        source: MESSAGE_SOURCE.JSTRACER,
    483        frame: {
    484          source: frame[TRACER_FIELDS_INDEXES.FRAME_URL],
    485          sourceId: frame[TRACER_FIELDS_INDEXES.FRAME_SOURCEID],
    486          line: frame[TRACER_FIELDS_INDEXES.FRAME_LINE],
    487          column: frame[TRACER_FIELDS_INDEXES.FRAME_COLUMN],
    488        },
    489        depth,
    490        implementation: frame[TRACER_FIELDS_INDEXES.FRAME_IMPLEMENTATION],
    491        displayName: frame[TRACER_FIELDS_INDEXES.FRAME_NAME],
    492        parameters: null,
    493        messageText: null,
    494        timeStamp,
    495        prefix,
    496        mutationType,
    497        mutationElement: mutationElement
    498          ? getAdHocFrontOrPrimitiveGrip(mutationElement, targetFront)
    499          : null,
    500        // Allow the identical frames to be coallesced into a unique message
    501        // with a repeatition counter so that we keep the output short in case of loops.
    502        allowRepeating: true,
    503      });
    504    }
    505    case "event": {
    506      const [, prefix, , timeStamp, , eventName] = traceResource;
    507      return new ConsoleMessage({
    508        targetFront,
    509        source: MESSAGE_SOURCE.JSTRACER,
    510        depth: 0,
    511        prefix,
    512        timeStamp,
    513        eventName,
    514        allowRepeating: false,
    515      });
    516    }
    517  }
    518  return null;
    519 }
    520 
    521 function transformTracerStateResource(stateResource) {
    522  const { targetFront, enabled, logMethod, timeStamp, reason } = stateResource;
    523  let message;
    524  if (enabled) {
    525    if (logMethod == TRACER_LOG_METHODS.STDOUT) {
    526      message = l10n.getStr("webconsole.message.commands.startTracingToStdout");
    527    } else if (logMethod == "console") {
    528      message = l10n.getStr(
    529        "webconsole.message.commands.startTracingToWebConsole"
    530      );
    531    } else if (logMethod == TRACER_LOG_METHODS.DEBUGGER_SIDEBAR) {
    532      message = l10n.getStr(
    533        "webconsole.message.commands.startTracingToDebuggerSidebar"
    534      );
    535    } else if (logMethod == TRACER_LOG_METHODS.PROFILER) {
    536      message = l10n.getStr(
    537        "webconsole.message.commands.startTracingToProfiler"
    538      );
    539    } else {
    540      throw new Error(`Unsupported tracer log method ${logMethod}`);
    541    }
    542  } else if (reason) {
    543    message = l10n.getFormatStr(
    544      "webconsole.message.commands.stopTracingWithReason",
    545      [reason]
    546    );
    547  } else {
    548    message = l10n.getStr("webconsole.message.commands.stopTracing");
    549  }
    550  return new ConsoleMessage({
    551    targetFront,
    552    source: MESSAGE_SOURCE.CONSOLE_API,
    553    type: MESSAGE_TYPE.JSTRACER,
    554    level: MESSAGE_LEVEL.LOG,
    555    messageText: message,
    556    timeStamp,
    557  });
    558 }
    559 
    560 function transformEvaluationResultPacket(packet) {
    561  let {
    562    exceptionMessage,
    563    errorMessageName,
    564    exceptionDocURL,
    565    exception,
    566    exceptionStack,
    567    hasException,
    568    frame,
    569    result,
    570    helperResult,
    571    timestamp: timeStamp,
    572    notes,
    573  } = packet;
    574 
    575  let parameter;
    576 
    577  if (hasException) {
    578    // If we have an exception, we prefix it, and we reset the exception message, as we're
    579    // not going to use it.
    580    parameter = exception;
    581    exceptionMessage = null;
    582  } else if (helperResult?.object) {
    583    parameter = helperResult.object;
    584  } else if (helperResult?.type === "error") {
    585    try {
    586      exceptionMessage = l10n.getFormatStr(
    587        helperResult.message,
    588        helperResult.messageArgs || []
    589      );
    590    } catch (ex) {
    591      exceptionMessage = helperResult.message;
    592    }
    593  } else {
    594    parameter = result;
    595  }
    596 
    597  const level =
    598    typeof exceptionMessage !== "undefined" && packet.exceptionMessage !== null
    599      ? MESSAGE_LEVEL.ERROR
    600      : MESSAGE_LEVEL.LOG;
    601 
    602  return new ConsoleMessage({
    603    source: MESSAGE_SOURCE.JAVASCRIPT,
    604    type: MESSAGE_TYPE.RESULT,
    605    helperType: helperResult ? helperResult.type : null,
    606    level,
    607    messageText: exceptionMessage,
    608    hasException,
    609    parameters: [parameter],
    610    errorMessageName,
    611    exceptionDocURL,
    612    stacktrace: exceptionStack,
    613    frame,
    614    timeStamp,
    615    notes,
    616    private: packet.private,
    617    allowRepeating: false,
    618  });
    619 }
    620 
    621 /**
    622 * Return if passed messages are similar and can thus be "repeated".
    623 * ⚠ This function is on a hot path, called for (almost) every message being sent by
    624 * the server. This should be kept as fast as possible.
    625 *
    626 * @param {Message} message1
    627 * @param {Message} message2
    628 * @returns {boolean}
    629 */
    630 // eslint-disable-next-line complexity
    631 function areMessagesSimilar(message1, message2) {
    632  if (!message1 || !message2) {
    633    return false;
    634  }
    635 
    636  if (!areMessagesParametersSimilar(message1, message2)) {
    637    return false;
    638  }
    639 
    640  if (!areMessagesStacktracesSimilar(message1, message2)) {
    641    return false;
    642  }
    643 
    644  if (
    645    !message1.allowRepeating ||
    646    !message2.allowRepeating ||
    647    message1.type !== message2.type ||
    648    message1.level !== message2.level ||
    649    message1.source !== message2.source ||
    650    message1.category !== message2.category ||
    651    message1.frame?.source !== message2.frame?.source ||
    652    message1.frame?.line !== message2.frame?.line ||
    653    message1.frame?.column !== message2.frame?.column ||
    654    message1.messageText !== message2.messageText ||
    655    message1.private !== message2.private ||
    656    message1.errorMessageName !== message2.errorMessageName ||
    657    message1.hasException !== message2.hasException ||
    658    message1.isPromiseRejection !== message2.isPromiseRejection ||
    659    message1.userProvidedStyles?.length !==
    660      message2.userProvidedStyles?.length ||
    661    `${message1.userProvidedStyles}` !== `${message2.userProvidedStyles}` ||
    662    message1.mutationType !== message2.mutationType ||
    663    message1.mutationElement != message2.mutationElement
    664  ) {
    665    return false;
    666  }
    667 
    668  return true;
    669 }
    670 
    671 /**
    672 * Return if passed messages parameters are similar
    673 * ⚠ This function is on a hot path, called for (almost) every message being sent by
    674 * the server. This should be kept as fast as possible.
    675 *
    676 * @param {Message} message1
    677 * @param {Message} message2
    678 * @returns {boolean}
    679 */
    680 // eslint-disable-next-line complexity
    681 function areMessagesParametersSimilar(message1, message2) {
    682  const message1ParamsLength = message1.parameters?.length;
    683  if (message1ParamsLength !== message2.parameters?.length) {
    684    return false;
    685  }
    686 
    687  if (!message1ParamsLength) {
    688    return true;
    689  }
    690 
    691  for (let i = 0; i < message1ParamsLength; i++) {
    692    const message1Parameter = message1.parameters[i];
    693    const message2Parameter = message2.parameters[i];
    694    // exceptions have a grip, but we want to consider 2 messages similar as long as
    695    // they refer to the same error.
    696    if (
    697      message1.hasException &&
    698      message2.hasException &&
    699      message1Parameter._grip?.class == message2Parameter._grip?.class &&
    700      message1Parameter._grip?.preview?.message ==
    701        message2Parameter._grip?.preview?.message &&
    702      message1Parameter._grip?.preview?.stack ==
    703        message2Parameter._grip?.preview?.stack
    704    ) {
    705      continue;
    706    }
    707 
    708    // For object references (grips), that are not exceptions, we don't want to consider
    709    // messages to be the same as we only have a preview of what they look like, and not
    710    // some kind of property that would give us the state of a given instance at a given
    711    // time.
    712    if (message1Parameter._grip || message2Parameter._grip) {
    713      return false;
    714    }
    715 
    716    if (message1Parameter.type !== message2Parameter.type) {
    717      return false;
    718    }
    719 
    720    if (message1Parameter.type) {
    721      if (message1Parameter.text !== message2Parameter.text) {
    722        return false;
    723      }
    724      // Some objects don't have a text property but a name one (e.g. Symbol)
    725      if (message1Parameter.name !== message2Parameter.name) {
    726        return false;
    727      }
    728    } else if (message1Parameter !== message2Parameter) {
    729      return false;
    730    }
    731  }
    732  return true;
    733 }
    734 
    735 /**
    736 * Return if passed messages stacktraces are similar
    737 *
    738 * @param {Message} message1
    739 * @param {Message} message2
    740 * @returns {boolean}
    741 */
    742 function areMessagesStacktracesSimilar(message1, message2) {
    743  const message1StackLength = message1.stacktrace?.length;
    744  if (message1StackLength !== message2.stacktrace?.length) {
    745    return false;
    746  }
    747 
    748  if (!message1StackLength) {
    749    return true;
    750  }
    751 
    752  for (let i = 0; i < message1StackLength; i++) {
    753    const message1Frame = message1.stacktrace[i];
    754    const message2Frame = message2.stacktrace[i];
    755 
    756    if (message1Frame.filename !== message2Frame.filename) {
    757      return false;
    758    }
    759 
    760    if (message1Frame.columnNumber !== message2Frame.columnNumber) {
    761      return false;
    762    }
    763 
    764    if (message1Frame.lineNumber !== message2Frame.lineNumber) {
    765      return false;
    766    }
    767  }
    768  return true;
    769 }
    770 
    771 /**
    772 * Maps a Firefox RDP type to its corresponding level.
    773 */
    774 function getLevelFromType(type) {
    775  const levels = {
    776    LEVEL_ERROR: "error",
    777    LEVEL_WARNING: "warn",
    778    LEVEL_INFO: "info",
    779    LEVEL_LOG: "log",
    780    LEVEL_DEBUG: "debug",
    781  };
    782 
    783  // A mapping from the console API log event levels to the Web Console levels.
    784  const levelMap = {
    785    error: levels.LEVEL_ERROR,
    786    exception: levels.LEVEL_ERROR,
    787    assert: levels.LEVEL_ERROR,
    788    logPointError: levels.LEVEL_ERROR,
    789    warn: levels.LEVEL_WARNING,
    790    info: levels.LEVEL_INFO,
    791    log: levels.LEVEL_LOG,
    792    clear: levels.LEVEL_LOG,
    793    trace: levels.LEVEL_LOG,
    794    table: levels.LEVEL_LOG,
    795    debug: levels.LEVEL_DEBUG,
    796    dir: levels.LEVEL_LOG,
    797    dirxml: levels.LEVEL_LOG,
    798    group: levels.LEVEL_LOG,
    799    groupCollapsed: levels.LEVEL_LOG,
    800    groupEnd: levels.LEVEL_LOG,
    801    time: levels.LEVEL_LOG,
    802    timeEnd: levels.LEVEL_LOG,
    803    count: levels.LEVEL_LOG,
    804  };
    805 
    806  return levelMap[type] || MESSAGE_TYPE.LOG;
    807 }
    808 
    809 function isGroupType(type) {
    810  return [
    811    MESSAGE_TYPE.START_GROUP,
    812    MESSAGE_TYPE.START_GROUP_COLLAPSED,
    813  ].includes(type);
    814 }
    815 
    816 function isPacketPrivate(packet) {
    817  return (
    818    packet.private === true ||
    819    (packet.message && packet.message.private === true) ||
    820    (packet.pageError && packet.pageError.private === true) ||
    821    (packet.networkEvent && packet.networkEvent.private === true)
    822  );
    823 }
    824 
    825 function createWarningGroupMessage(id, type, firstMessage) {
    826  return new ConsoleMessage({
    827    id,
    828    allowRepeating: false,
    829    level: MESSAGE_LEVEL.WARN,
    830    source: MESSAGE_SOURCE.CONSOLE_FRONTEND,
    831    type,
    832    messageText: getWarningGroupLabel(firstMessage),
    833    timeStamp: firstMessage.timeStamp,
    834    innerWindowID: firstMessage.innerWindowID,
    835  });
    836 }
    837 
    838 function createSimpleTableMessage(columns, items, timeStamp) {
    839  return new ConsoleMessage({
    840    allowRepeating: false,
    841    level: MESSAGE_LEVEL.LOG,
    842    source: MESSAGE_SOURCE.CONSOLE_FRONTEND,
    843    type: MESSAGE_TYPE.SIMPLE_TABLE,
    844    columns,
    845    items,
    846    timeStamp,
    847  });
    848 }
    849 
    850 /**
    851 * Given the a regular warning message, compute the label of the warning group the message
    852 * could be in.
    853 * For example, if the message text is:
    854 * The resource at “http://evil.com” was blocked because Enhanced Tracking Protection is enabled
    855 *
    856 * it may be turned into
    857 *
    858 * The resource at “<URL>” was blocked because Enhanced Tracking Protection is enabled
    859 *
    860 * @param {ConsoleMessage} firstMessage
    861 * @returns {string} The computed label
    862 */
    863 function getWarningGroupLabel(firstMessage) {
    864  if (
    865    isEnhancedTrackingProtectionMessage(firstMessage) ||
    866    isStorageIsolationMessage(firstMessage) ||
    867    isTrackingProtectionMessage(firstMessage)
    868  ) {
    869    return replaceURL(firstMessage.messageText, "<URL>");
    870  }
    871 
    872  if (isCookieMessage(firstMessage)) {
    873    return l10n.getStr("webconsole.group.cookie");
    874  }
    875 
    876  if (isCSPMessage(firstMessage)) {
    877    return l10n.getStr("webconsole.group.csp");
    878  }
    879 
    880  return "";
    881 }
    882 
    883 /**
    884 * Replace any URL in the provided text by the provided replacement text, or an empty
    885 * string.
    886 *
    887 * @param {string} text
    888 * @param {string} replacementText
    889 * @returns {string}
    890 */
    891 function replaceURL(text, replacementText = "") {
    892  let result = "";
    893  let currentIndex = 0;
    894  let contentStart;
    895  while (true) {
    896    const url = urlRegex.exec(text);
    897    // Pick the regexp with the earlier content; index will always be zero.
    898    if (!url) {
    899      break;
    900    }
    901    contentStart = url.index + url[1].length;
    902    if (contentStart > 0) {
    903      const nonUrlText = text.substring(0, contentStart);
    904      result += nonUrlText;
    905    }
    906 
    907    // There are some final characters for a URL that are much more likely
    908    // to have been part of the enclosing text rather than the end of the
    909    // URL.
    910    let useUrl = url[2];
    911    const uneat = uneatLastUrlCharsRegex.exec(useUrl);
    912    if (uneat) {
    913      useUrl = useUrl.substring(0, uneat.index);
    914    }
    915 
    916    if (useUrl) {
    917      result += replacementText;
    918    }
    919 
    920    currentIndex = currentIndex + contentStart;
    921 
    922    currentIndex = currentIndex + useUrl.length;
    923    text = text.substring(url.index + url[1].length + useUrl.length);
    924  }
    925 
    926  return result + text;
    927 }
    928 
    929 /**
    930 * Get the warningGroup type in which the message could be in.
    931 *
    932 * @param {ConsoleMessage} message
    933 * @returns {string | null} null if the message can't be part of a warningGroup.
    934 */
    935 function getWarningGroupType(message) {
    936  // We got report that this can be called with `undefined` (See Bug 1801462 and Bug 1810109).
    937  // Until we manage to reproduce and find why this happens, guard on message so at least
    938  // we don't crash the console.
    939  if (!message) {
    940    return null;
    941  }
    942 
    943  if (
    944    message.level !== MESSAGE_LEVEL.WARN &&
    945    // Cookie messages are both warnings and infos
    946    message.level !== MESSAGE_LEVEL.INFO
    947  ) {
    948    return null;
    949  }
    950 
    951  if (isEnhancedTrackingProtectionMessage(message)) {
    952    return MESSAGE_TYPE.CONTENT_BLOCKING_GROUP;
    953  }
    954 
    955  if (isStorageIsolationMessage(message)) {
    956    return MESSAGE_TYPE.STORAGE_ISOLATION_GROUP;
    957  }
    958 
    959  if (isTrackingProtectionMessage(message)) {
    960    return MESSAGE_TYPE.TRACKING_PROTECTION_GROUP;
    961  }
    962 
    963  if (isCookieMessage(message)) {
    964    return MESSAGE_TYPE.COOKIE_GROUP;
    965  }
    966 
    967  if (isCSPMessage(message)) {
    968    return MESSAGE_TYPE.CSP_GROUP;
    969  }
    970 
    971  return null;
    972 }
    973 
    974 /**
    975 * Returns a computed id given a message
    976 *
    977 * @param {ConsoleMessage} type: the message type, from MESSAGE_TYPE.
    978 * @param {Integer} innerWindowID: the message innerWindowID.
    979 * @returns {string}
    980 */
    981 function getParentWarningGroupMessageId(message) {
    982  const warningGroupType = getWarningGroupType(message);
    983  if (!warningGroupType) {
    984    return null;
    985  }
    986 
    987  return `${warningGroupType}-${message.innerWindowID}`;
    988 }
    989 
    990 /**
    991 * Returns true if the message is a warningGroup message (i.e. the "Header").
    992 *
    993 * @param {ConsoleMessage} message
    994 * @returns {boolean}
    995 */
    996 function isWarningGroup(message) {
    997  return (
    998    message.type === MESSAGE_TYPE.CONTENT_BLOCKING_GROUP ||
    999    message.type === MESSAGE_TYPE.STORAGE_ISOLATION_GROUP ||
   1000    message.type === MESSAGE_TYPE.TRACKING_PROTECTION_GROUP ||
   1001    message.type === MESSAGE_TYPE.COOKIE_GROUP ||
   1002    message.type === MESSAGE_TYPE.CORS_GROUP ||
   1003    message.type === MESSAGE_TYPE.CSP_GROUP
   1004  );
   1005 }
   1006 
   1007 /**
   1008 * Returns true if the message is an Enhanced Tracking Protection message.
   1009 *
   1010 * @param {ConsoleMessage} message
   1011 * @returns {boolean}
   1012 */
   1013 function isEnhancedTrackingProtectionMessage(message) {
   1014  const { category } = message;
   1015  return (
   1016    category == "cookieBlockedPermission" ||
   1017    category == "cookieBlockedTracker" ||
   1018    category == "cookieBlockedAll" ||
   1019    category == "cookieBlockedForeign"
   1020  );
   1021 }
   1022 
   1023 /**
   1024 * Returns true if the message is a storage isolation message.
   1025 *
   1026 * @param {ConsoleMessage} message
   1027 * @returns {boolean}
   1028 */
   1029 function isStorageIsolationMessage(message) {
   1030  const { category } = message;
   1031  return category == "cookiePartitionedForeign";
   1032 }
   1033 
   1034 /**
   1035 * Returns true if the message is a tracking protection message.
   1036 *
   1037 * @param {ConsoleMessage} message
   1038 * @returns {boolean}
   1039 */
   1040 function isTrackingProtectionMessage(message) {
   1041  const { category } = message;
   1042  return category == "Tracking Protection";
   1043 }
   1044 
   1045 /**
   1046 * Returns true if the message is a cookie message.
   1047 *
   1048 * @param {ConsoleMessage} message
   1049 * @returns {boolean}
   1050 */
   1051 function isCookieMessage(message) {
   1052  const { category } = message;
   1053  return [
   1054    "cookiesCHIPS",
   1055    "cookiesOversize",
   1056    "cookieSameSite",
   1057    "cookieInvalidAttribute",
   1058  ].includes(category);
   1059 }
   1060 
   1061 /**
   1062 * Returns true if the message is a Content Security Policy (CSP) message.
   1063 *
   1064 * @param {ConsoleMessage} message
   1065 * @returns {boolean}
   1066 */
   1067 function isCSPMessage(message) {
   1068  const { category } = message;
   1069  return typeof category == "string" && category.startsWith("CSP_");
   1070 }
   1071 
   1072 function getDescriptorValue(descriptor) {
   1073  if (!descriptor) {
   1074    return descriptor;
   1075  }
   1076 
   1077  if (Object.prototype.hasOwnProperty.call(descriptor, "safeGetterValues")) {
   1078    return descriptor.safeGetterValues;
   1079  }
   1080 
   1081  if (Object.prototype.hasOwnProperty.call(descriptor, "getterValue")) {
   1082    return descriptor.getterValue;
   1083  }
   1084 
   1085  if (Object.prototype.hasOwnProperty.call(descriptor, "value")) {
   1086    return descriptor.value;
   1087  }
   1088  return descriptor;
   1089 }
   1090 
   1091 function getNaturalOrder(messageA, messageB) {
   1092  const aFirst = -1;
   1093  const bFirst = 1;
   1094 
   1095  // It can happen that messages are emitted in the same microsecond, making their
   1096  // timestamp similar. In such case, we rely on which message came first through
   1097  // the console API service, checking their id, except for expression result, which we'll
   1098  // always insert after because console API messages emitted from the expression need to
   1099  // be rendered before.
   1100  if (messageA.timeStamp === messageB.timeStamp) {
   1101    if (messageA.type === "result") {
   1102      return bFirst;
   1103    }
   1104 
   1105    if (messageB.type === "result") {
   1106      return aFirst;
   1107    }
   1108 
   1109    if (
   1110      !Number.isNaN(parseInt(messageA.id, 10)) &&
   1111      !Number.isNaN(parseInt(messageB.id, 10))
   1112    ) {
   1113      return parseInt(messageA.id, 10) < parseInt(messageB.id, 10)
   1114        ? aFirst
   1115        : bFirst;
   1116    }
   1117  }
   1118  return messageA.timeStamp < messageB.timeStamp ? aFirst : bFirst;
   1119 }
   1120 
   1121 function isMessageNetworkError(message) {
   1122  return (
   1123    message.source === MESSAGE_SOURCE.NETWORK &&
   1124    message?.status &&
   1125    message?.status.toString().match(/^[4,5]\d\d$/)
   1126  );
   1127 }
   1128 
   1129 module.exports = {
   1130  areMessagesSimilar,
   1131  createWarningGroupMessage,
   1132  createSimpleTableMessage,
   1133  getDescriptorValue,
   1134  getNaturalOrder,
   1135  getParentWarningGroupMessageId,
   1136  getWarningGroupType,
   1137  isEnhancedTrackingProtectionMessage,
   1138  isGroupType,
   1139  isMessageNetworkError,
   1140  isPacketPrivate,
   1141  isWarningGroup,
   1142  l10n,
   1143  prepareMessage,
   1144 };