diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /devtools/client/webconsole/utils | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/webconsole/utils')
-rw-r--r-- | devtools/client/webconsole/utils/clipboard.js | 57 | ||||
-rw-r--r-- | devtools/client/webconsole/utils/context-menu.js | 368 | ||||
-rw-r--r-- | devtools/client/webconsole/utils/id-generator.js | 15 | ||||
-rw-r--r-- | devtools/client/webconsole/utils/l10n.js | 70 | ||||
-rw-r--r-- | devtools/client/webconsole/utils/messages.js | 1028 | ||||
-rw-r--r-- | devtools/client/webconsole/utils/moz.build | 14 | ||||
-rw-r--r-- | devtools/client/webconsole/utils/object-inspector.js | 158 | ||||
-rw-r--r-- | devtools/client/webconsole/utils/prefs.js | 46 |
8 files changed, 1756 insertions, 0 deletions
diff --git a/devtools/client/webconsole/utils/clipboard.js b/devtools/client/webconsole/utils/clipboard.js new file mode 100644 index 0000000000..72b064f323 --- /dev/null +++ b/devtools/client/webconsole/utils/clipboard.js @@ -0,0 +1,57 @@ +/* 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"; + +/** + * Get the text of the element parameter, as provided by the Selection API. We need to + * rely on the Selection API as it mimics exactly what the user would have if they do a + * selection using the mouse and copy it. `HTMLElement.textContent` and + * `HTMLElement.innerText` follow a different codepath than user selection + copy. + * They both have issues when dealing with whitespaces, and therefore can't be used to + * have a reliable result. + * + * As the Selection API is exposed through the Window object, if we don't have a window + * we fallback to `HTMLElement.textContent`. + * + * @param {HTMLElement} el: The element we want the text of. + * @returns {String|null} The text of the element, or null if el is falsy. + */ +function getElementText(el) { + if (!el) { + return null; + } + // If we can, we use the Selection API to match what the user would get if they + // manually select and copy the message. + const doc = el.ownerDocument; + const win = doc && doc.defaultView; + + if (!win) { + return el.textContent; + } + + // We store the current selected range and unselect everything. + const selection = win.getSelection(); + const currentSelectedRange = + !selection.isCollapsed && selection.getRangeAt(0); + selection.removeAllRanges(); + + // Then creates a range from `el`, and get the text content. + const range = doc.createRange(); + range.selectNode(el); + selection.addRange(range); + const text = selection.toString(); + + // Finally we revert the selection to what it was. + selection.removeRange(range); + if (currentSelectedRange) { + selection.addRange(currentSelectedRange); + } + + return text; +} + +module.exports = { + getElementText, +}; diff --git a/devtools/client/webconsole/utils/context-menu.js b/devtools/client/webconsole/utils/context-menu.js new file mode 100644 index 0000000000..7996194d56 --- /dev/null +++ b/devtools/client/webconsole/utils/context-menu.js @@ -0,0 +1,368 @@ +/* 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 Menu = require("resource://devtools/client/framework/menu.js"); +const MenuItem = require("resource://devtools/client/framework/menu-item.js"); + +const { + MESSAGE_SOURCE, +} = require("resource://devtools/client/webconsole/constants.js"); + +const clipboardHelper = require("resource://devtools/shared/platform/clipboard.js"); +const { + l10n, +} = require("resource://devtools/client/webconsole/utils/messages.js"); +const actions = require("resource://devtools/client/webconsole/actions/index.js"); + +loader.lazyRequireGetter( + this, + "saveAs", + "resource://devtools/shared/DevToolsUtils.js", + true +); +loader.lazyRequireGetter( + this, + "openContentLink", + "resource://devtools/client/shared/link.js", + true +); +loader.lazyRequireGetter( + this, + "getElementText", + "resource://devtools/client/webconsole/utils/clipboard.js", + true +); + +/** + * Create a Menu instance for the webconsole. + * + * @param {Event} context menu event + * {Object} message (optional) message object containing metadata such as: + * - {String} source + * - {String} request + * @param {Object} options + * - {Actions} bound actions + * - {WebConsoleWrapper} wrapper instance used for accessing properties like the store + * and window. + */ +function createContextMenu(event, message, webConsoleWrapper) { + const { target } = event; + const { parentNode, toolbox, hud } = webConsoleWrapper; + const store = webConsoleWrapper.getStore(); + const { dispatch } = store; + + const messageEl = target.closest(".message"); + const clipboardText = getElementText(messageEl); + + const linkEl = target.closest("a[href]"); + const url = linkEl && linkEl.href; + + const messageVariable = target.closest(".objectBox"); + // Ensure that console.group and console.groupCollapsed commands are not captured + const variableText = + messageVariable && + !messageEl.classList.contains("startGroup") && + !messageEl.classList.contains("startGroupCollapsed") + ? messageVariable.textContent + : null; + + // Retrieve closes actor id from the DOM. + const actorEl = + target.closest("[data-link-actor-id]") || + target.querySelector("[data-link-actor-id]"); + const actor = actorEl ? actorEl.dataset.linkActorId : null; + + const rootObjectInspector = target.closest(".object-inspector"); + const rootActor = rootObjectInspector + ? rootObjectInspector.querySelector("[data-link-actor-id]") + : null; + // We can have object which are not displayed inside an ObjectInspector (e.g. Errors), + // so let's default to `actor`. + const rootActorId = rootActor ? rootActor.dataset.linkActorId : actor; + + const elementNode = + target.closest(".objectBox-node") || target.closest(".objectBox-textNode"); + const isConnectedElement = + elementNode && elementNode.querySelector(".open-inspector") !== null; + + const win = parentNode.ownerDocument.defaultView; + const selection = win.getSelection(); + + const { source, request, messageId } = message || {}; + + const menu = new Menu({ id: "webconsole-menu" }); + + // Copy URL for a network request. + menu.append( + new MenuItem({ + id: "console-menu-copy-url", + label: l10n.getStr("webconsole.menu.copyURL.label"), + accesskey: l10n.getStr("webconsole.menu.copyURL.accesskey"), + visible: source === MESSAGE_SOURCE.NETWORK, + click: () => { + if (!request) { + return; + } + clipboardHelper.copyString(request.url); + }, + }) + ); + + if (toolbox && request) { + // Open Network message in the Network panel. + menu.append( + new MenuItem({ + id: "console-menu-open-in-network-panel", + label: l10n.getStr("webconsole.menu.openInNetworkPanel.label"), + accesskey: l10n.getStr("webconsole.menu.openInNetworkPanel.accesskey"), + visible: source === MESSAGE_SOURCE.NETWORK, + click: () => dispatch(actions.openNetworkPanel(message.messageId)), + }) + ); + // Resend Network message. + menu.append( + new MenuItem({ + id: "console-menu-resend-network-request", + label: l10n.getStr("webconsole.menu.resendNetworkRequest.label"), + accesskey: l10n.getStr( + "webconsole.menu.resendNetworkRequest.accesskey" + ), + visible: source === MESSAGE_SOURCE.NETWORK, + click: () => dispatch(actions.resendNetworkRequest(messageId)), + }) + ); + } + + // Open URL in a new tab for a network request. + menu.append( + new MenuItem({ + id: "console-menu-open-url", + label: l10n.getStr("webconsole.menu.openURL.label"), + accesskey: l10n.getStr("webconsole.menu.openURL.accesskey"), + visible: source === MESSAGE_SOURCE.NETWORK, + click: () => { + if (!request) { + return; + } + openContentLink(request.url); + }, + }) + ); + + // Open DOM node in the Inspector panel. + const contentDomReferenceEl = target.closest( + "[data-link-content-dom-reference]" + ); + if (isConnectedElement && contentDomReferenceEl) { + const contentDomReference = contentDomReferenceEl.getAttribute( + "data-link-content-dom-reference" + ); + + menu.append( + new MenuItem({ + id: "console-menu-open-node", + label: l10n.getStr("webconsole.menu.openNodeInInspector.label"), + accesskey: l10n.getStr("webconsole.menu.openNodeInInspector.accesskey"), + disabled: false, + click: () => + dispatch( + actions.openNodeInInspector(JSON.parse(contentDomReference)) + ), + }) + ); + } + + // Store as global variable. + menu.append( + new MenuItem({ + id: "console-menu-store", + label: l10n.getStr("webconsole.menu.storeAsGlobalVar.label"), + accesskey: l10n.getStr("webconsole.menu.storeAsGlobalVar.accesskey"), + disabled: !actor, + click: () => dispatch(actions.storeAsGlobal(actor)), + }) + ); + + // Copy message or grip. + menu.append( + new MenuItem({ + id: "console-menu-copy", + label: l10n.getStr("webconsole.menu.copyMessage.label"), + accesskey: l10n.getStr("webconsole.menu.copyMessage.accesskey"), + // Disabled if there is no selection and no message element available to copy. + disabled: selection.isCollapsed && !clipboardText, + click: () => { + if (selection.isCollapsed) { + // If the selection is empty/collapsed, copy the text content of the + // message for which the context menu was opened. + clipboardHelper.copyString(clipboardText); + } else { + clipboardHelper.copyString(selection.toString()); + } + }, + }) + ); + + // Copy message object. + menu.append( + new MenuItem({ + id: "console-menu-copy-object", + label: l10n.getStr("webconsole.menu.copyObject.label"), + accesskey: l10n.getStr("webconsole.menu.copyObject.accesskey"), + // Disabled if there is no actor and no variable text associated. + disabled: !actor && !variableText, + click: () => dispatch(actions.copyMessageObject(actor, variableText)), + }) + ); + + // Export to clipboard + menu.append( + new MenuItem({ + id: "console-menu-export-clipboard", + label: l10n.getStr("webconsole.menu.copyAllMessages.label"), + accesskey: l10n.getStr("webconsole.menu.copyAllMessages.accesskey"), + disabled: false, + async click() { + const outputText = await getUnvirtualizedConsoleOutputText( + webConsoleWrapper + ); + clipboardHelper.copyString(outputText); + }, + }) + ); + + // Export to file + menu.append( + new MenuItem({ + id: "console-menu-export-file", + label: l10n.getStr("webconsole.menu.saveAllMessagesFile.label"), + accesskey: l10n.getStr("webconsole.menu.saveAllMessagesFile.accesskey"), + disabled: false, + // Note: not async, but returns a promise for the actual save. + click: async () => { + const date = new Date(); + const suggestedName = + `console-export-${date.getFullYear()}-` + + `${date.getMonth() + 1}-${date.getDate()}_${date.getHours()}-` + + `${date.getMinutes()}-${date.getSeconds()}.txt`; + const outputText = await getUnvirtualizedConsoleOutputText( + webConsoleWrapper + ); + const data = new TextEncoder().encode(outputText); + saveAs(window, data, suggestedName); + }, + }) + ); + + // Open object in sidebar. + const shouldOpenSidebar = store.getState().prefs.sidebarToggle; + if (shouldOpenSidebar) { + menu.append( + new MenuItem({ + id: "console-menu-open-sidebar", + label: l10n.getStr("webconsole.menu.openInSidebar.label1"), + accesskey: l10n.getStr("webconsole.menu.openInSidebar.accesskey"), + disabled: !rootActorId, + click: () => dispatch(actions.openSidebar(messageId, rootActorId)), + }) + ); + } + + if (url) { + menu.append( + new MenuItem({ + id: "console-menu-open-url", + label: l10n.getStr("webconsole.menu.openURL.label"), + accesskey: l10n.getStr("webconsole.menu.openURL.accesskey"), + click: () => + openContentLink(url, { + inBackground: true, + relatedToCurrent: true, + }), + }) + ); + menu.append( + new MenuItem({ + id: "console-menu-copy-url", + label: l10n.getStr("webconsole.menu.copyURL.label"), + accesskey: l10n.getStr("webconsole.menu.copyURL.accesskey"), + click: () => clipboardHelper.copyString(url), + }) + ); + } + + // Emit the "menu-open" event for testing. + const { screenX, screenY } = event; + menu.once("open", () => webConsoleWrapper.emitForTests("menu-open")); + menu.popup(screenX, screenY, hud.chromeWindow.document); + + return menu; +} + +exports.createContextMenu = createContextMenu; + +/** + * Returns the whole text content of the console output. + * We're creating a new ConsoleOutput using the current store, turning off virtualization + * so we can have access to all the messages. + * + * @param {WebConsoleWrapper} webConsoleWrapper + * @returns Promise<String> + */ +async function getUnvirtualizedConsoleOutputText(webConsoleWrapper) { + return new Promise(resolve => { + const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.js"); + const { + createElement, + createFactory, + } = require("resource://devtools/client/shared/vendor/react.js"); + const ConsoleOutput = createFactory( + require("resource://devtools/client/webconsole/components/Output/ConsoleOutput.js") + ); + const { + Provider, + createProvider, + } = require("resource://devtools/client/shared/vendor/react-redux.js"); + + const { parentNode, toolbox } = webConsoleWrapper; + const doc = parentNode.ownerDocument; + + // Create an element that won't impact the layout of the console + const singleUseElement = doc.createElement("section"); + singleUseElement.classList.add("clipboard-only"); + doc.body.append(singleUseElement); + + const consoleOutput = ConsoleOutput({ + serviceContainer: { + ...webConsoleWrapper.getServiceContainer(), + preventStacktraceInitialRenderDelay: true, + }, + disableVirtualization: true, + }); + + ReactDOM.render( + createElement( + Provider, + { + store: webConsoleWrapper.getStore(), + }, + toolbox + ? createElement( + createProvider(toolbox.commands.targetCommand.storeId), + { store: toolbox.commands.targetCommand.store }, + consoleOutput + ) + : consoleOutput + ), + singleUseElement, + () => { + resolve(getElementText(singleUseElement)); + singleUseElement.remove(); + ReactDOM.unmountComponentAtNode(singleUseElement); + } + ); + }); +} diff --git a/devtools/client/webconsole/utils/id-generator.js b/devtools/client/webconsole/utils/id-generator.js new file mode 100644 index 0000000000..17144dd73c --- /dev/null +++ b/devtools/client/webconsole/utils/id-generator.js @@ -0,0 +1,15 @@ +/* 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"; + +exports.IdGenerator = class IdGenerator { + constructor() { + this.messageId = 1; + } + + getNextId(packet) { + return packet && packet.actor ? packet.actor : "" + this.messageId++; + } +}; diff --git a/devtools/client/webconsole/utils/l10n.js b/devtools/client/webconsole/utils/l10n.js new file mode 100644 index 0000000000..5c647f84e6 --- /dev/null +++ b/devtools/client/webconsole/utils/l10n.js @@ -0,0 +1,70 @@ +/* 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 { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); +const helper = new LocalizationHelper( + "devtools/client/locales/webconsole.properties" +); + +const l10n = { + /** + * Generates a formatted timestamp string for displaying in console messages. + * + * @param integer [milliseconds] + * Optional, allows you to specify the timestamp in milliseconds since + * the UNIX epoch. + * @return string + * The timestamp formatted for display. + */ + timestampString(milliseconds) { + const d = new Date(milliseconds ? milliseconds : null); + const hours = d.getHours(); + const minutes = d.getMinutes(); + const seconds = d.getSeconds(); + milliseconds = d.getMilliseconds(); + const parameters = [hours, minutes, seconds, milliseconds]; + return l10n.getFormatStr("timestampFormat", parameters); + }, + + /** + * Retrieve a localized string. + * + * @param string name + * The string name you want from the Web Console string bundle. + * @return string + * The localized string. + */ + getStr(name) { + try { + return helper.getStr(name); + } catch (ex) { + console.error("Failed to get string: " + name); + throw ex; + } + }, + + /** + * Retrieve a localized string formatted with values coming from the given + * array. + * + * @param string name + * The string name you want from the Web Console string bundle. + * @param array array + * The array of values you want in the formatted string. + * @return string + * The formatted local string. + */ + getFormatStr(name, array) { + try { + return helper.getFormatStr(name, ...array); + } catch (ex) { + console.error("Failed to format string: " + name); + throw ex; + } + }, +}; + +module.exports = l10n; diff --git a/devtools/client/webconsole/utils/messages.js b/devtools/client/webconsole/utils/messages.js new file mode 100644 index 0000000000..135da20536 --- /dev/null +++ b/devtools/client/webconsole/utils/messages.js @@ -0,0 +1,1028 @@ +/* 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 +); + +// 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 +// found here: http://www.iana.org/domains/root/db +// ) +// [-\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); + } + + 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); + } + + 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_TRACE: { + return transformTraceResource(resource); + } + + case ResourceCommand.TYPES.JSTRACER_STATE: { + return transformTracerStateResource(resource); + } + + case "will-navigate": { + return transformNavigationMessagePacket(resource); + } + + case "evaluationResult": + default: { + return transformEvaluationResultPacket(resource); + } + } +} + +// eslint-disable-next-line complexity +function transformConsoleAPICallResource(consoleMessageResource, persistLogs) { + const { message, targetFront } = consoleMessageResource; + + let parameters = message.arguments; + let type = message.level; + let level = getLevelFromType(type); + let messageText = null; + const { timer } = message; + + // 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 } = message; + + 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 = message.filename + ? { + source: message.filename, + sourceId: message.sourceId, + line: message.lineNumber, + column: message.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: message.stacktrace ? message.stacktrace : null, + frame, + timeStamp: message.timeStamp, + userProvidedStyles: message.styles, + prefix: message.prefix, + private: message.private, + chromeContext: message.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, prefix, timeStamp } = traceResource; + if (traceResource.eventName) { + const { eventName } = traceResource; + + return new ConsoleMessage({ + targetFront, + source: MESSAGE_SOURCE.JSTRACER, + depth: 0, + eventName, + timeStamp, + prefix, + allowRepeating: false, + }); + } + const { + depth, + implementation, + displayName, + filename, + lineNumber, + columnNumber, + args, + sourceId, + + returnedValue, + relatedTraceId, + why, + } = traceResource; + + const frame = { + source: filename, + sourceId, + line: lineNumber, + column: columnNumber, + }; + + return new ConsoleMessage({ + targetFront, + source: MESSAGE_SOURCE.JSTRACER, + frame, + depth, + implementation, + displayName, + parameters: args + ? args.map(p => (p ? getAdHocFrontOrPrimitiveGrip(p, targetFront) : p)) + : null, + returnedValue: why + ? 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, + }); +} + +function transformTracerStateResource(stateResource) { + const { targetFront, enabled, logMethod, timeStamp, reason } = stateResource; + let message; + if (enabled) { + if (logMethod == "stdout") { + message = l10n.getStr("webconsole.message.commands.startTracingToStdout"); + } else if (logMethod == "console") { + message = l10n.getStr( + "webconsole.message.commands.startTracingToWebConsole" + ); + } else if (logMethod == "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}` + ) { + 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; + } + } 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 (isCookieSameSiteMessage(firstMessage)) { + if (Services.prefs.getBoolPref("network.cookie.sameSite.laxByDefault")) { + return l10n.getStr("webconsole.group.cookieSameSiteLaxByDefaultEnabled2"); + } + return l10n.getStr("webconsole.group.cookieSameSiteLaxByDefaultDisabled2"); + } + + 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 && + // CookieSameSite messages are not warnings but 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 (isCookieSameSiteMessage(message)) { + return MESSAGE_TYPE.COOKIE_SAMESITE_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_SAMESITE_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 isCookieSameSiteMessage(message) { + const { category } = message; + return category == "cookieSameSite"; +} + +/** + * 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, +}; diff --git a/devtools/client/webconsole/utils/moz.build b/devtools/client/webconsole/utils/moz.build new file mode 100644 index 0000000000..8367b8c195 --- /dev/null +++ b/devtools/client/webconsole/utils/moz.build @@ -0,0 +1,14 @@ +# vim: set filetype=python: +# 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/. + +DevToolsModules( + "clipboard.js", + "context-menu.js", + "id-generator.js", + "l10n.js", + "messages.js", + "object-inspector.js", + "prefs.js", +) diff --git a/devtools/client/webconsole/utils/object-inspector.js b/devtools/client/webconsole/utils/object-inspector.js new file mode 100644 index 0000000000..8001828b51 --- /dev/null +++ b/devtools/client/webconsole/utils/object-inspector.js @@ -0,0 +1,158 @@ +/* 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 { + createFactory, + createElement, +} = require("resource://devtools/client/shared/vendor/react.js"); + +loader.lazyGetter(this, "REPS", function () { + return require("resource://devtools/client/shared/components/reps/index.js") + .REPS; +}); +loader.lazyGetter(this, "MODE", function () { + return require("resource://devtools/client/shared/components/reps/index.js") + .MODE; +}); +loader.lazyGetter(this, "ObjectInspector", function () { + const reps = require("resource://devtools/client/shared/components/reps/index.js"); + return createFactory(reps.objectInspector.ObjectInspector); +}); + +loader.lazyRequireGetter( + this, + "SmartTrace", + "resource://devtools/client/shared/components/SmartTrace.js" +); + +loader.lazyRequireGetter( + this, + "LongStringFront", + "resource://devtools/client/fronts/string.js", + true +); + +loader.lazyRequireGetter( + this, + "ObjectFront", + "resource://devtools/client/fronts/object.js", + true +); + +/** + * Create and return an ObjectInspector for the given front. + * + * @param {Object} grip + * The object grip to create an ObjectInspector for. + * @param {Object} serviceContainer + * Object containing various utility functions + * @param {Object} override + * Object containing props that should override the default props passed to + * ObjectInspector. + * @returns {ObjectInspector} + * An ObjectInspector for the given grip. + */ +function getObjectInspector( + frontOrPrimitiveGrip, + serviceContainer, + override = {} +) { + let onDOMNodeMouseOver; + let onDOMNodeMouseOut; + let onInspectIconClick; + + if (serviceContainer) { + onDOMNodeMouseOver = serviceContainer.highlightDomElement + ? object => serviceContainer.highlightDomElement(object) + : null; + onDOMNodeMouseOut = serviceContainer.unHighlightDomElement + ? object => serviceContainer.unHighlightDomElement(object) + : null; + onInspectIconClick = serviceContainer.openNodeInInspector + ? (object, e) => { + // Stop the event propagation so we don't trigger ObjectInspector expand/collapse. + e.stopPropagation(); + serviceContainer.openNodeInInspector(object); + } + : null; + } + + const roots = createRoots(frontOrPrimitiveGrip, override.pathPrefix); + + const objectInspectorProps = { + autoExpandDepth: 0, + mode: MODE.LONG, + standalone: true, + roots, + onViewSourceInDebugger: serviceContainer.onViewSourceInDebugger, + recordTelemetryEvent: serviceContainer.recordTelemetryEvent, + openLink: serviceContainer.openLink, + sourceMapURLService: serviceContainer.sourceMapURLService, + customFormat: override.customFormat !== false, + setExpanded: override.setExpanded, + initiallyExpanded: override.initiallyExpanded, + queueActorsForCleanup: override.queueActorsForCleanup, + cachedNodes: override.cachedNodes, + urlCropLimit: 120, + renderStacktrace: stacktrace => { + const attrs = { + key: "stacktrace", + stacktrace, + onViewSourceInDebugger: serviceContainer + ? serviceContainer.onViewSourceInDebugger || + serviceContainer.onViewSource + : null, + onViewSource: serviceContainer.onViewSource, + onReady: override.maybeScrollToBottom, + sourceMapURLService: serviceContainer + ? serviceContainer.sourceMapURLService + : null, + }; + + if (serviceContainer?.preventStacktraceInitialRenderDelay) { + attrs.initialRenderDelay = 0; + } + return createElement(SmartTrace, attrs); + }, + onDOMNodeMouseOver, + onDOMNodeMouseOut, + onInspectIconClick, + defaultRep: REPS.Grip, + createElement: serviceContainer?.createElement, + mayUseCustomFormatter: true, + ...override, + }; + + if (override.autoFocusRoot) { + Object.assign(objectInspectorProps, { + focusedItem: objectInspectorProps.roots[0], + }); + } + + return ObjectInspector(objectInspectorProps); +} + +function createRoots(frontOrPrimitiveGrip, pathPrefix = "") { + const isFront = + frontOrPrimitiveGrip instanceof ObjectFront || + frontOrPrimitiveGrip instanceof LongStringFront; + const grip = isFront ? frontOrPrimitiveGrip.getGrip() : frontOrPrimitiveGrip; + + return [ + { + path: `${pathPrefix}${ + frontOrPrimitiveGrip + ? frontOrPrimitiveGrip.actorID || frontOrPrimitiveGrip.actor + : null + }`, + contents: { value: grip, front: isFront ? frontOrPrimitiveGrip : null }, + }, + ]; +} + +module.exports = { + getObjectInspector, +}; diff --git a/devtools/client/webconsole/utils/prefs.js b/devtools/client/webconsole/utils/prefs.js new file mode 100644 index 0000000000..9235c82069 --- /dev/null +++ b/devtools/client/webconsole/utils/prefs.js @@ -0,0 +1,46 @@ +/* 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"; + +function getPreferenceName(hud, suffix) { + if (!suffix) { + console.error("Suffix shouldn't be falsy", { suffix }); + return null; + } + + if (!hud) { + console.error("hud shouldn't be falsy", { hud }); + return null; + } + + if (suffix.startsWith("devtools.")) { + // We don't have a suffix but a full pref name. Let's return it. + return suffix; + } + + const component = hud.isBrowserConsole ? "browserconsole" : "webconsole"; + return `devtools.${component}.${suffix}`; +} + +function getPrefsService(hud) { + const getPrefName = pref => getPreferenceName(hud, pref); + + return { + getBoolPref: (pref, deflt) => + Services.prefs.getBoolPref(getPrefName(pref), deflt), + getIntPref: (pref, deflt) => + Services.prefs.getIntPref(getPrefName(pref), deflt), + setBoolPref: (pref, value) => + Services.prefs.setBoolPref(getPrefName(pref), value), + setIntPref: (pref, value) => + Services.prefs.setIntPref(getPrefName(pref), value), + clearUserPref: pref => Services.prefs.clearUserPref(getPrefName(pref)), + getPrefName, + }; +} + +module.exports = { + getPrefsService, +}; |