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 };