Source code

Revision control

Copy as Markdown

Other Tools

/* 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 l10n = require("resource://devtools/client/webconsole/utils/l10n.js");
const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
const {
isSupportedByConsoleTable,
} = require("resource://devtools/shared/webconsole/messages.js");
loader.lazyRequireGetter(
this,
"getAdHocFrontOrPrimitiveGrip",
"resource://devtools/client/fronts/object.js",
true
);
loader.lazyRequireGetter(
this,
"TRACER_FIELDS_INDEXES",
"resource://devtools/server/actors/tracer.js",
true
);
loader.lazyRequireGetter(
this,
"TRACER_LOG_METHODS",
"resource://devtools/shared/specs/tracer.js",
true
);
// URL Regex, common idioms:
//
// Lead-in (URL):
// ( Capture because we need to know if there was a lead-in
// character so we can include it as part of the text
// preceding the match. We lack look-behind matching.
// ^| The URL can start at the beginning of the string.
// [\s(,;'"`“] Or whitespace or some punctuation that does not imply
// a context which would preclude a URL.
// )
//
// We do not need a trailing look-ahead because our regex's will terminate
// because they run out of characters they can eat.
// What we do not attempt to have the regexp do:
// - Avoid trailing '.' and ')' characters. We let our greedy match absorb
// these, but have a separate regex for extra characters to leave off at the
// end.
//
// The Regex (apart from lead-in/lead-out):
// ( Begin capture of the URL
// (?: (potential detect beginnings)
// https?:\/\/| Start with "http" or "https"
// www\d{0,3}[.][a-z0-9.\-]{2,249}|
// Start with "www", up to 3 numbers, then "." then
// something that looks domain-namey. We differ from the
// next case in that we do not constrain the top-level
// domain as tightly and do not require a trailing path
// indicator of "/". This is IDN root compatible.
// [a-z0-9.\-]{2,250}[.][a-z]{2,4}\/
// Detect a non-www domain, but requiring a trailing "/"
// to indicate a path. This only detects IDN domains
// with a non-IDN root. This is reasonable in cases where
// there is no explicit http/https start us out, but
// unreasonable where there is. Our real fix is the bug
// to port the Thunderbird/gecko linkification logic.
//
// Domain names can be up to 253 characters long, and are
// limited to a-zA-Z0-9 and '-'. The roots don't have
// hyphens unless they are IDN roots. Root zones can be
// )
// [-\w.!~*'();,/?:@&=+$#%]*
// path onwards. We allow the set of characters that
// encodeURI does not escape plus the result of escaping
// (so also '%')
// )
// eslint-disable-next-line max-len
const urlRegex =
/(^|[\s(,;'"`“])((?:https?:\/\/|www\d{0,3}[.][a-z0-9.\-]{2,249}|[a-z0-9.\-]{2,250}[.][a-z]{2,4}\/)[-\w.!~*'();,/?:@&=+$#%]*)/im;
// Set of terminators that are likely to have been part of the context rather
// than part of the URL and so should be uneaten. This is '(', ',', ';', plus
// quotes and question end-ing punctuation and the potential permutations with
// parentheses (english-specific).
const uneatLastUrlCharsRegex = /(?:[),;.!?`'"]|[.!?]\)|\)[.!?])$/;
const {
MESSAGE_SOURCE,
MESSAGE_TYPE,
MESSAGE_LEVEL,
} = require("resource://devtools/client/webconsole/constants.js");
const {
ConsoleMessage,
NetworkEventMessage,
} = require("resource://devtools/client/webconsole/types.js");
function prepareMessage(resource, idGenerator, persistLogs) {
if (!resource.source) {
resource = transformResource(resource, persistLogs);
}
// The Tracer resource transformer may process some resource
// which aren't translated into any item in the console (Tracer frames)
if (resource) {
resource.id = idGenerator.getNextId(resource);
}
return resource;
}
/**
* Transforms a resource given its type.
*
* @param {Object} resource: This can be either a simple RDP packet or an object emitted
* by the Resource API.
* @param {Boolean} persistLogs: Value of the "Persist logs" setting
*/
function transformResource(resource, persistLogs) {
switch (resource.resourceType || resource.type) {
case ResourceCommand.TYPES.CONSOLE_MESSAGE: {
return transformConsoleAPICallResource(
resource,
persistLogs,
resource.targetFront
);
}
case ResourceCommand.TYPES.PLATFORM_MESSAGE: {
return transformPlatformMessageResource(resource);
}
case ResourceCommand.TYPES.ERROR_MESSAGE: {
return transformPageErrorResource(resource);
}
case ResourceCommand.TYPES.CSS_MESSAGE: {
return transformCSSMessageResource(resource);
}
case ResourceCommand.TYPES.NETWORK_EVENT: {
return transformNetworkEventResource(resource);
}
case ResourceCommand.TYPES.JSTRACER_STATE: {
return transformTracerStateResource(resource);
}
case ResourceCommand.TYPES.JSTRACER_TRACE: {
return transformTraceResource(resource);
}
case "will-navigate": {
return transformNavigationMessagePacket(resource);
}
case "evaluationResult":
default: {
return transformEvaluationResultPacket(resource);
}
}
}
// eslint-disable-next-line complexity
function transformConsoleAPICallResource(
consoleMessageResource,
persistLogs,
targetFront
) {
let { arguments: parameters, level: type, timer } = consoleMessageResource;
let level = getLevelFromType(type);
let messageText = null;
// Special per-type conversion.
switch (type) {
case "clear":
// We show a message to users when calls console.clear() is called.
parameters = [
l10n.getStr(persistLogs ? "preventedConsoleClear" : "consoleCleared"),
];
break;
case "count":
case "countReset":
// Chrome RDP doesn't have a special type for count.
type = MESSAGE_TYPE.LOG;
const { counter } = consoleMessageResource;
if (!counter) {
// We don't show anything if we don't have counter data.
type = MESSAGE_TYPE.NULL_MESSAGE;
} else if (counter.error) {
messageText = l10n.getFormatStr(counter.error, [counter.label]);
level = MESSAGE_LEVEL.WARN;
parameters = null;
} else {
const label = counter.label
? counter.label
: l10n.getStr("noCounterLabel");
messageText = `${label}: ${counter.count}`;
parameters = null;
}
break;
case "timeStamp":
type = MESSAGE_TYPE.NULL_MESSAGE;
break;
case "time":
parameters = null;
if (timer && timer.error) {
messageText = l10n.getFormatStr(timer.error, [timer.name]);
level = MESSAGE_LEVEL.WARN;
} else {
// We don't show anything for console.time calls to match Chrome's behaviour.
type = MESSAGE_TYPE.NULL_MESSAGE;
}
break;
case "timeLog":
case "timeEnd":
if (timer && timer.error) {
parameters = null;
messageText = l10n.getFormatStr(timer.error, [timer.name]);
level = MESSAGE_LEVEL.WARN;
} else if (timer) {
// We show the duration to users when calls console.timeLog/timeEnd is called,
// if corresponding console.time() was called before.
const duration = Math.round(timer.duration * 100) / 100;
if (type === "timeEnd") {
messageText = l10n.getFormatStr("console.timeEnd", [
timer.name,
duration,
]);
parameters = null;
} else if (type === "timeLog") {
const [, ...rest] = parameters;
parameters = [
l10n.getFormatStr("timeLog", [timer.name, duration]),
...rest,
];
}
} else {
// If the `timer` property does not exists, we don't output anything.
type = MESSAGE_TYPE.NULL_MESSAGE;
}
break;
case "table":
if (!isSupportedByConsoleTable(parameters)) {
// If the class of the first parameter is not supported,
// we handle the call as a simple console.log
type = "log";
}
break;
case "group":
type = MESSAGE_TYPE.START_GROUP;
if (parameters.length === 0) {
parameters = [l10n.getStr("noGroupLabel")];
}
break;
case "groupCollapsed":
type = MESSAGE_TYPE.START_GROUP_COLLAPSED;
if (parameters.length === 0) {
parameters = [l10n.getStr("noGroupLabel")];
}
break;
case "groupEnd":
type = MESSAGE_TYPE.END_GROUP;
parameters = null;
break;
case "dirxml":
// Handle console.dirxml calls as simple console.log
type = "log";
break;
}
const frame = consoleMessageResource.filename
? {
source: consoleMessageResource.filename,
sourceId: consoleMessageResource.sourceId,
line: consoleMessageResource.lineNumber,
column: consoleMessageResource.columnNumber,
}
: null;
if (frame && (type === "logPointError" || type === "logPoint")) {
frame.options = { logPoint: true };
}
return new ConsoleMessage({
targetFront,
source: MESSAGE_SOURCE.CONSOLE_API,
type,
level,
parameters,
messageText,
stacktrace: consoleMessageResource.stacktrace
? consoleMessageResource.stacktrace
: null,
frame,
timeStamp: consoleMessageResource.timeStamp,
userProvidedStyles: consoleMessageResource.styles,
prefix: consoleMessageResource.prefix,
private: consoleMessageResource.private,
chromeContext: consoleMessageResource.chromeContext,
});
}
function transformNavigationMessagePacket(packet) {
const { url } = packet;
return new ConsoleMessage({
source: MESSAGE_SOURCE.CONSOLE_FRONTEND,
type: MESSAGE_TYPE.NAVIGATION_MARKER,
level: MESSAGE_LEVEL.LOG,
messageText: l10n.getFormatStr("webconsole.navigated", [url]),
timeStamp: packet.timeStamp,
allowRepeating: false,
});
}
function transformPlatformMessageResource(platformMessageResource) {
const { message, timeStamp, targetFront } = platformMessageResource;
return new ConsoleMessage({
targetFront,
source: MESSAGE_SOURCE.CONSOLE_API,
type: MESSAGE_TYPE.LOG,
level: MESSAGE_LEVEL.LOG,
messageText: message,
timeStamp,
chromeContext: true,
});
}
function transformPageErrorResource(pageErrorResource, override = {}) {
const { pageError, targetFront } = pageErrorResource;
let level = MESSAGE_LEVEL.ERROR;
if (pageError.warning) {
level = MESSAGE_LEVEL.WARN;
} else if (pageError.info) {
level = MESSAGE_LEVEL.INFO;
}
const frame = pageError.sourceName
? {
source: pageError.sourceName,
sourceId: pageError.sourceId,
line: pageError.lineNumber,
column: pageError.columnNumber,
}
: null;
return new ConsoleMessage(
Object.assign(
{
targetFront,
innerWindowID: pageError.innerWindowID,
source: MESSAGE_SOURCE.JAVASCRIPT,
type: MESSAGE_TYPE.LOG,
level,
category: pageError.category,
messageText: pageError.errorMessage,
stacktrace: pageError.stacktrace ? pageError.stacktrace : null,
frame,
errorMessageName: pageError.errorMessageName,
exceptionDocURL: pageError.exceptionDocURL,
hasException: pageError.hasException,
parameters: pageError.hasException ? [pageError.exception] : null,
timeStamp: pageError.timeStamp,
notes: pageError.notes,
private: pageError.private,
chromeContext: pageError.chromeContext,
isPromiseRejection: pageError.isPromiseRejection,
},
override
)
);
}
function transformCSSMessageResource(cssMessageResource) {
return transformPageErrorResource(cssMessageResource, {
cssSelectors: cssMessageResource.cssSelectors,
source: MESSAGE_SOURCE.CSS,
});
}
function transformNetworkEventResource(networkEventResource) {
return new NetworkEventMessage(networkEventResource);
}
function transformTraceResource(traceResource) {
const { targetFront } = traceResource;
const type = traceResource[TRACER_FIELDS_INDEXES.TYPE];
const collectedFrames = targetFront.getJsTracerCollectedFramesArray();
switch (type) {
case "frame":
collectedFrames.push(traceResource);
return null;
case "enter": {
const [, prefix, frameIndex, timeStamp, depth, args] = traceResource;
const frame = collectedFrames[frameIndex];
return new ConsoleMessage({
targetFront,
source: MESSAGE_SOURCE.JSTRACER,
frame: {
source: frame[TRACER_FIELDS_INDEXES.FRAME_URL],
sourceId: frame[TRACER_FIELDS_INDEXES.FRAME_SOURCEID],
line: frame[TRACER_FIELDS_INDEXES.FRAME_LINE],
column: frame[TRACER_FIELDS_INDEXES.FRAME_COLUMN],
},
depth,
implementation: frame[TRACER_FIELDS_INDEXES.FRAME_IMPLEMENTATION],
displayName: frame[TRACER_FIELDS_INDEXES.FRAME_NAME],
parameters: args
? args.map(p =>
p ? getAdHocFrontOrPrimitiveGrip(p, targetFront) : p
)
: null,
messageText: null,
timeStamp,
prefix,
// Allow the identical frames to be coalesced into a unique message
// with a repeatition counter so that we keep the output short in case of loops.
allowRepeating: true,
});
}
case "exit": {
const [
,
prefix,
frameIndex,
timeStamp,
depth,
relatedTraceId,
returnedValue,
why,
] = traceResource;
const frame = collectedFrames[frameIndex];
return new ConsoleMessage({
targetFront,
source: MESSAGE_SOURCE.JSTRACER,
frame: {
source: frame[TRACER_FIELDS_INDEXES.FRAME_URL],
sourceId: frame[TRACER_FIELDS_INDEXES.FRAME_SOURCEID],
line: frame[TRACER_FIELDS_INDEXES.FRAME_LINE],
column: frame[TRACER_FIELDS_INDEXES.FRAME_COLUMN],
},
depth,
implementation: frame[TRACER_FIELDS_INDEXES.FRAME_IMPLEMENTATION],
displayName: frame[TRACER_FIELDS_INDEXES.FRAME_NAME],
parameters: null,
returnedValue:
returnedValue != undefined
? getAdHocFrontOrPrimitiveGrip(returnedValue, targetFront)
: null,
relatedTraceId,
why,
messageText: null,
timeStamp,
prefix,
// Allow the identical frames to be coallesced into a unique message
// with a repeatition counter so that we keep the output short in case of loops.
allowRepeating: true,
});
}
case "dom-mutation": {
const [
,
prefix,
frameIndex,
timeStamp,
depth,
mutationType,
mutationElement,
] = traceResource;
const frame = collectedFrames[frameIndex];
return new ConsoleMessage({
targetFront,
source: MESSAGE_SOURCE.JSTRACER,
frame: {
source: frame[TRACER_FIELDS_INDEXES.FRAME_URL],
sourceId: frame[TRACER_FIELDS_INDEXES.FRAME_SOURCEID],
line: frame[TRACER_FIELDS_INDEXES.FRAME_LINE],
column: frame[TRACER_FIELDS_INDEXES.FRAME_COLUMN],
},
depth,
implementation: frame[TRACER_FIELDS_INDEXES.FRAME_IMPLEMENTATION],
displayName: frame[TRACER_FIELDS_INDEXES.FRAME_NAME],
parameters: null,
messageText: null,
timeStamp,
prefix,
mutationType,
mutationElement: mutationElement
? getAdHocFrontOrPrimitiveGrip(mutationElement, targetFront)
: null,
// Allow the identical frames to be coallesced into a unique message
// with a repeatition counter so that we keep the output short in case of loops.
allowRepeating: true,
});
}
case "event": {
const [, prefix, , timeStamp, , eventName] = traceResource;
return new ConsoleMessage({
targetFront,
source: MESSAGE_SOURCE.JSTRACER,
depth: 0,
prefix,
timeStamp,
eventName,
allowRepeating: false,
});
}
}
return null;
}
function transformTracerStateResource(stateResource) {
const { targetFront, enabled, logMethod, timeStamp, reason } = stateResource;
let message;
if (enabled) {
if (logMethod == TRACER_LOG_METHODS.STDOUT) {
message = l10n.getStr("webconsole.message.commands.startTracingToStdout");
} else if (logMethod == "console") {
message = l10n.getStr(
"webconsole.message.commands.startTracingToWebConsole"
);
} else if (logMethod == TRACER_LOG_METHODS.DEBUGGER_SIDEBAR) {
message = l10n.getStr(
"webconsole.message.commands.startTracingToDebuggerSidebar"
);
} else if (logMethod == TRACER_LOG_METHODS.PROFILER) {
message = l10n.getStr(
"webconsole.message.commands.startTracingToProfiler"
);
} else {
throw new Error(`Unsupported tracer log method ${logMethod}`);
}
} else if (reason) {
message = l10n.getFormatStr(
"webconsole.message.commands.stopTracingWithReason",
[reason]
);
} else {
message = l10n.getStr("webconsole.message.commands.stopTracing");
}
return new ConsoleMessage({
targetFront,
source: MESSAGE_SOURCE.CONSOLE_API,
type: MESSAGE_TYPE.JSTRACER,
level: MESSAGE_LEVEL.LOG,
messageText: message,
timeStamp,
});
}
function transformEvaluationResultPacket(packet) {
let {
exceptionMessage,
errorMessageName,
exceptionDocURL,
exception,
exceptionStack,
hasException,
frame,
result,
helperResult,
timestamp: timeStamp,
notes,
} = packet;
let parameter;
if (hasException) {
// If we have an exception, we prefix it, and we reset the exception message, as we're
// not going to use it.
parameter = exception;
exceptionMessage = null;
} else if (helperResult?.object) {
parameter = helperResult.object;
} else if (helperResult?.type === "error") {
try {
exceptionMessage = l10n.getFormatStr(
helperResult.message,
helperResult.messageArgs || []
);
} catch (ex) {
exceptionMessage = helperResult.message;
}
} else {
parameter = result;
}
const level =
typeof exceptionMessage !== "undefined" && packet.exceptionMessage !== null
? MESSAGE_LEVEL.ERROR
: MESSAGE_LEVEL.LOG;
return new ConsoleMessage({
source: MESSAGE_SOURCE.JAVASCRIPT,
type: MESSAGE_TYPE.RESULT,
helperType: helperResult ? helperResult.type : null,
level,
messageText: exceptionMessage,
hasException,
parameters: [parameter],
errorMessageName,
exceptionDocURL,
stacktrace: exceptionStack,
frame,
timeStamp,
notes,
private: packet.private,
allowRepeating: false,
});
}
/**
* Return if passed messages are similar and can thus be "repeated".
* ⚠ This function is on a hot path, called for (almost) every message being sent by
* the server. This should be kept as fast as possible.
*
* @param {Message} message1
* @param {Message} message2
* @returns {Boolean}
*/
// eslint-disable-next-line complexity
function areMessagesSimilar(message1, message2) {
if (!message1 || !message2) {
return false;
}
if (!areMessagesParametersSimilar(message1, message2)) {
return false;
}
if (!areMessagesStacktracesSimilar(message1, message2)) {
return false;
}
if (
!message1.allowRepeating ||
!message2.allowRepeating ||
message1.type !== message2.type ||
message1.level !== message2.level ||
message1.source !== message2.source ||
message1.category !== message2.category ||
message1.frame?.source !== message2.frame?.source ||
message1.frame?.line !== message2.frame?.line ||
message1.frame?.column !== message2.frame?.column ||
message1.messageText !== message2.messageText ||
message1.private !== message2.private ||
message1.errorMessageName !== message2.errorMessageName ||
message1.hasException !== message2.hasException ||
message1.isPromiseRejection !== message2.isPromiseRejection ||
message1.userProvidedStyles?.length !==
message2.userProvidedStyles?.length ||
`${message1.userProvidedStyles}` !== `${message2.userProvidedStyles}` ||
message1.mutationType !== message2.mutationType ||
message1.mutationElement != message2.mutationElement
) {
return false;
}
return true;
}
/**
* Return if passed messages parameters are similar
* ⚠ This function is on a hot path, called for (almost) every message being sent by
* the server. This should be kept as fast as possible.
*
* @param {Message} message1
* @param {Message} message2
* @returns {Boolean}
*/
function areMessagesParametersSimilar(message1, message2) {
const message1ParamsLength = message1.parameters?.length;
if (message1ParamsLength !== message2.parameters?.length) {
return false;
}
if (!message1ParamsLength) {
return true;
}
for (let i = 0; i < message1ParamsLength; i++) {
const message1Parameter = message1.parameters[i];
const message2Parameter = message2.parameters[i];
// exceptions have a grip, but we want to consider 2 messages similar as long as
// they refer to the same error.
if (
message1.hasException &&
message2.hasException &&
message1Parameter._grip?.class == message2Parameter._grip?.class &&
message1Parameter._grip?.preview?.message ==
message2Parameter._grip?.preview?.message &&
message1Parameter._grip?.preview?.stack ==
message2Parameter._grip?.preview?.stack
) {
continue;
}
// For object references (grips), that are not exceptions, we don't want to consider
// messages to be the same as we only have a preview of what they look like, and not
// some kind of property that would give us the state of a given instance at a given
// time.
if (message1Parameter._grip || message2Parameter._grip) {
return false;
}
if (message1Parameter.type !== message2Parameter.type) {
return false;
}
if (message1Parameter.type) {
if (message1Parameter.text !== message2Parameter.text) {
return false;
}
// Some objects don't have a text property but a name one (e.g. Symbol)
if (message1Parameter.name !== message2Parameter.name) {
return false;
}
} else if (message1Parameter !== message2Parameter) {
return false;
}
}
return true;
}
/**
* Return if passed messages stacktraces are similar
*
* @param {Message} message1
* @param {Message} message2
* @returns {Boolean}
*/
function areMessagesStacktracesSimilar(message1, message2) {
const message1StackLength = message1.stacktrace?.length;
if (message1StackLength !== message2.stacktrace?.length) {
return false;
}
if (!message1StackLength) {
return true;
}
for (let i = 0; i < message1StackLength; i++) {
const message1Frame = message1.stacktrace[i];
const message2Frame = message2.stacktrace[i];
if (message1Frame.filename !== message2Frame.filename) {
return false;
}
if (message1Frame.columnNumber !== message2Frame.columnNumber) {
return false;
}
if (message1Frame.lineNumber !== message2Frame.lineNumber) {
return false;
}
}
return true;
}
/**
* Maps a Firefox RDP type to its corresponding level.
*/
function getLevelFromType(type) {
const levels = {
LEVEL_ERROR: "error",
LEVEL_WARNING: "warn",
LEVEL_INFO: "info",
LEVEL_LOG: "log",
LEVEL_DEBUG: "debug",
};
// A mapping from the console API log event levels to the Web Console levels.
const levelMap = {
error: levels.LEVEL_ERROR,
exception: levels.LEVEL_ERROR,
assert: levels.LEVEL_ERROR,
logPointError: levels.LEVEL_ERROR,
warn: levels.LEVEL_WARNING,
info: levels.LEVEL_INFO,
log: levels.LEVEL_LOG,
clear: levels.LEVEL_LOG,
trace: levels.LEVEL_LOG,
table: levels.LEVEL_LOG,
debug: levels.LEVEL_DEBUG,
dir: levels.LEVEL_LOG,
dirxml: levels.LEVEL_LOG,
group: levels.LEVEL_LOG,
groupCollapsed: levels.LEVEL_LOG,
groupEnd: levels.LEVEL_LOG,
time: levels.LEVEL_LOG,
timeEnd: levels.LEVEL_LOG,
count: levels.LEVEL_LOG,
};
return levelMap[type] || MESSAGE_TYPE.LOG;
}
function isGroupType(type) {
return [
MESSAGE_TYPE.START_GROUP,
MESSAGE_TYPE.START_GROUP_COLLAPSED,
].includes(type);
}
function isPacketPrivate(packet) {
return (
packet.private === true ||
(packet.message && packet.message.private === true) ||
(packet.pageError && packet.pageError.private === true) ||
(packet.networkEvent && packet.networkEvent.private === true)
);
}
function createWarningGroupMessage(id, type, firstMessage) {
return new ConsoleMessage({
id,
allowRepeating: false,
level: MESSAGE_LEVEL.WARN,
source: MESSAGE_SOURCE.CONSOLE_FRONTEND,
type,
messageText: getWarningGroupLabel(firstMessage),
timeStamp: firstMessage.timeStamp,
innerWindowID: firstMessage.innerWindowID,
});
}
function createSimpleTableMessage(columns, items, timeStamp) {
return new ConsoleMessage({
allowRepeating: false,
level: MESSAGE_LEVEL.LOG,
source: MESSAGE_SOURCE.CONSOLE_FRONTEND,
type: MESSAGE_TYPE.SIMPLE_TABLE,
columns,
items,
timeStamp,
});
}
/**
* Given the a regular warning message, compute the label of the warning group the message
* could be in.
* For example, if the message text is:
* The resource at “http://evil.com” was blocked because content blocking is enabled
*
* it may be turned into
*
* The resource at “<URL>” was blocked because content blocking is enabled
*
* @param {ConsoleMessage} firstMessage
* @returns {String} The computed label
*/
function getWarningGroupLabel(firstMessage) {
if (
isContentBlockingMessage(firstMessage) ||
isStorageIsolationMessage(firstMessage) ||
isTrackingProtectionMessage(firstMessage)
) {
return replaceURL(firstMessage.messageText, "<URL>");
}
if (isCookieMessage(firstMessage)) {
return l10n.getStr("webconsole.group.cookie");
}
if (isCSPMessage(firstMessage)) {
return l10n.getStr("webconsole.group.csp");
}
return "";
}
/**
* Replace any URL in the provided text by the provided replacement text, or an empty
* string.
*
* @param {String} text
* @param {String} replacementText
* @returns {String}
*/
function replaceURL(text, replacementText = "") {
let result = "";
let currentIndex = 0;
let contentStart;
while (true) {
const url = urlRegex.exec(text);
// Pick the regexp with the earlier content; index will always be zero.
if (!url) {
break;
}
contentStart = url.index + url[1].length;
if (contentStart > 0) {
const nonUrlText = text.substring(0, contentStart);
result += nonUrlText;
}
// There are some final characters for a URL that are much more likely
// to have been part of the enclosing text rather than the end of the
// URL.
let useUrl = url[2];
const uneat = uneatLastUrlCharsRegex.exec(useUrl);
if (uneat) {
useUrl = useUrl.substring(0, uneat.index);
}
if (useUrl) {
result += replacementText;
}
currentIndex = currentIndex + contentStart;
currentIndex = currentIndex + useUrl.length;
text = text.substring(url.index + url[1].length + useUrl.length);
}
return result + text;
}
/**
* Get the warningGroup type in which the message could be in.
* @param {ConsoleMessage} message
* @returns {String|null} null if the message can't be part of a warningGroup.
*/
function getWarningGroupType(message) {
// We got report that this can be called with `undefined` (See Bug 1801462 and Bug 1810109).
// Until we manage to reproduce and find why this happens, guard on message so at least
// we don't crash the console.
if (!message) {
return null;
}
if (
message.level !== MESSAGE_LEVEL.WARN &&
// Cookie messages are both warnings and infos
message.level !== MESSAGE_LEVEL.INFO
) {
return null;
}
if (isContentBlockingMessage(message)) {
return MESSAGE_TYPE.CONTENT_BLOCKING_GROUP;
}
if (isStorageIsolationMessage(message)) {
return MESSAGE_TYPE.STORAGE_ISOLATION_GROUP;
}
if (isTrackingProtectionMessage(message)) {
return MESSAGE_TYPE.TRACKING_PROTECTION_GROUP;
}
if (isCookieMessage(message)) {
return MESSAGE_TYPE.COOKIE_GROUP;
}
if (isCSPMessage(message)) {
return MESSAGE_TYPE.CSP_GROUP;
}
return null;
}
/**
* Returns a computed id given a message
*
* @param {ConsoleMessage} type: the message type, from MESSAGE_TYPE.
* @param {Integer} innerWindowID: the message innerWindowID.
* @returns {String}
*/
function getParentWarningGroupMessageId(message) {
const warningGroupType = getWarningGroupType(message);
if (!warningGroupType) {
return null;
}
return `${warningGroupType}-${message.innerWindowID}`;
}
/**
* Returns true if the message is a warningGroup message (i.e. the "Header").
* @param {ConsoleMessage} message
* @returns {Boolean}
*/
function isWarningGroup(message) {
return (
message.type === MESSAGE_TYPE.CONTENT_BLOCKING_GROUP ||
message.type === MESSAGE_TYPE.STORAGE_ISOLATION_GROUP ||
message.type === MESSAGE_TYPE.TRACKING_PROTECTION_GROUP ||
message.type === MESSAGE_TYPE.COOKIE_GROUP ||
message.type === MESSAGE_TYPE.CORS_GROUP ||
message.type === MESSAGE_TYPE.CSP_GROUP
);
}
/**
* Returns true if the message is a content blocking message.
* @param {ConsoleMessage} message
* @returns {Boolean}
*/
function isContentBlockingMessage(message) {
const { category } = message;
return (
category == "cookieBlockedPermission" ||
category == "cookieBlockedTracker" ||
category == "cookieBlockedAll" ||
category == "cookieBlockedForeign"
);
}
/**
* Returns true if the message is a storage isolation message.
* @param {ConsoleMessage} message
* @returns {Boolean}
*/
function isStorageIsolationMessage(message) {
const { category } = message;
return category == "cookiePartitionedForeign";
}
/**
* Returns true if the message is a tracking protection message.
* @param {ConsoleMessage} message
* @returns {Boolean}
*/
function isTrackingProtectionMessage(message) {
const { category } = message;
return category == "Tracking Protection";
}
/**
* Returns true if the message is a cookie message.
* @param {ConsoleMessage} message
* @returns {Boolean}
*/
function isCookieMessage(message) {
const { category } = message;
return [
"cookiesCHIPS",
"cookiesOversize",
"cookieSameSite",
"cookieInvalidAttribute",
].includes(category);
}
/**
* Returns true if the message is a Content Security Policy (CSP) message.
* @param {ConsoleMessage} message
* @returns {Boolean}
*/
function isCSPMessage(message) {
const { category } = message;
return typeof category == "string" && category.startsWith("CSP_");
}
function getDescriptorValue(descriptor) {
if (!descriptor) {
return descriptor;
}
if (Object.prototype.hasOwnProperty.call(descriptor, "safeGetterValues")) {
return descriptor.safeGetterValues;
}
if (Object.prototype.hasOwnProperty.call(descriptor, "getterValue")) {
return descriptor.getterValue;
}
if (Object.prototype.hasOwnProperty.call(descriptor, "value")) {
return descriptor.value;
}
return descriptor;
}
function getNaturalOrder(messageA, messageB) {
const aFirst = -1;
const bFirst = 1;
// It can happen that messages are emitted in the same microsecond, making their
// timestamp similar. In such case, we rely on which message came first through
// the console API service, checking their id, except for expression result, which we'll
// always insert after because console API messages emitted from the expression need to
// be rendered before.
if (messageA.timeStamp === messageB.timeStamp) {
if (messageA.type === "result") {
return bFirst;
}
if (messageB.type === "result") {
return aFirst;
}
if (
!Number.isNaN(parseInt(messageA.id, 10)) &&
!Number.isNaN(parseInt(messageB.id, 10))
) {
return parseInt(messageA.id, 10) < parseInt(messageB.id, 10)
? aFirst
: bFirst;
}
}
return messageA.timeStamp < messageB.timeStamp ? aFirst : bFirst;
}
function isMessageNetworkError(message) {
return (
message.source === MESSAGE_SOURCE.NETWORK &&
message?.status &&
message?.status.toString().match(/^[4,5]\d\d$/)
);
}
module.exports = {
areMessagesSimilar,
createWarningGroupMessage,
createSimpleTableMessage,
getDescriptorValue,
getNaturalOrder,
getParentWarningGroupMessageId,
getWarningGroupType,
isContentBlockingMessage,
isGroupType,
isMessageNetworkError,
isPacketPrivate,
isWarningGroup,
l10n,
prepareMessage,
};