/* 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 { Utils: WebConsoleUtils, } = require("resource://devtools/client/webconsole/utils.js"); const { EVALUATE_EXPRESSION, SET_TERMINAL_INPUT, SET_TERMINAL_EAGER_RESULT, EDITOR_PRETTY_PRINT, HELP_URL, } = require("resource://devtools/client/webconsole/constants.js"); const { getAllPrefs, } = require("resource://devtools/client/webconsole/selectors/prefs.js"); const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js"); const l10n = require("resource://devtools/client/webconsole/utils/l10n.js"); loader.lazyServiceGetter( this, "clipboardHelper", "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper" ); loader.lazyRequireGetter( this, "messagesActions", "resource://devtools/client/webconsole/actions/messages.js" ); loader.lazyRequireGetter( this, "historyActions", "resource://devtools/client/webconsole/actions/history.js" ); loader.lazyRequireGetter( this, "ConsoleCommand", "resource://devtools/client/webconsole/types.js", true ); loader.lazyRequireGetter( this, "netmonitorBlockingActions", "resource://devtools/client/netmonitor/src/actions/request-blocking.js" ); loader.lazyRequireGetter( this, ["saveScreenshot", "captureAndSaveScreenshot"], "resource://devtools/client/shared/screenshot.js", true ); loader.lazyRequireGetter( this, "createSimpleTableMessage", "resource://devtools/client/webconsole/utils/messages.js", true ); loader.lazyRequireGetter( this, "getSelectedTarget", "resource://devtools/shared/commands/target/selectors/targets.js", true ); async function getMappedExpression(hud, expression) { let mapResult; try { mapResult = await hud.getMappedExpression(expression); } catch (e) { console.warn("Error when calling getMappedExpression", e); } let mapped = null; if (mapResult) { ({ expression, mapped } = mapResult); } return { expression, mapped }; } function evaluateExpression(expression, from = "input") { return async ({ dispatch, webConsoleUI, hud, commands }) => { if (!expression) { expression = hud.getInputSelection() || hud.getInputValue(); } if (!expression) { return null; } // We use the messages action as it's doing additional transformation on the message. const { messages } = dispatch( messagesActions.messagesAdd([ new ConsoleCommand({ messageText: expression, timeStamp: Date.now(), }), ]) ); const [consoleCommandMessage] = messages; dispatch({ type: EVALUATE_EXPRESSION, expression, from, }); WebConsoleUtils.usageCount++; let mapped; ({ expression, mapped } = await getMappedExpression(hud, expression)); // Even if the evaluation fails, // we still need to pass the error response to onExpressionEvaluated. const onSettled = res => res; const response = await commands.scriptCommand .execute(expression, { frameActor: hud.getSelectedFrameActorID(), selectedNodeActor: webConsoleUI.getSelectedNodeActorID(), selectedTargetFront: getSelectedTarget( webConsoleUI.hud.commands.targetCommand.store.getState() ), mapped, // Allow breakpoints to be triggerred and the evaluated source to be shown in debugger UI disableBreaks: false, }) .then(onSettled, onSettled); const serverConsoleCommandTimestamp = response.startTime; // In case of remote debugging, it might happen that the debuggee page does not have // the exact same clock time as the client. This could cause some ordering issues // where the result message is displayed *before* the expression that lead to it. if ( serverConsoleCommandTimestamp && consoleCommandMessage.timeStamp > serverConsoleCommandTimestamp ) { // If we're in such case, we remove the original command message, and add it again, // with the timestamp coming from the server. dispatch(messagesActions.messageRemove(consoleCommandMessage.id)); dispatch( messagesActions.messagesAdd([ new ConsoleCommand({ messageText: expression, timeStamp: serverConsoleCommandTimestamp, }), ]) ); } return dispatch(onExpressionEvaluated(response)); }; } /** * The JavaScript evaluation response handler. * * @private * @param {Object} response * The message received from the server. */ function onExpressionEvaluated(response) { return async ({ dispatch, webConsoleUI }) => { if (response.error) { console.error(`Evaluation error`, response.error, ": ", response.message); return; } // If the evaluation was a top-level await expression that was rejected, there will // be an uncaught exception reported, so we don't need to do anything. if (response.topLevelAwaitRejected === true) { return; } if (!response.helperResult) { webConsoleUI.wrapper.dispatchMessageAdd(response); return; } await dispatch(handleHelperResult(response)); }; } function handleHelperResult(response) { // eslint-disable-next-line complexity return async ({ dispatch, hud, toolbox, webConsoleUI, getState }) => { const { result, helperResult } = response; const helperHasRawOutput = !!helperResult?.rawOutput; if (helperResult?.type) { switch (helperResult.type) { case "exception": dispatch( messagesActions.messagesAdd([ { message: { level: "error", arguments: [helperResult.message], chromeContext: true, }, resourceType: ResourceCommand.TYPES.CONSOLE_MESSAGE, }, ]) ); break; case "clearOutput": dispatch(messagesActions.messagesClear()); break; case "clearHistory": dispatch(historyActions.clearHistory()); break; case "historyOutput": const history = getState().history.entries || []; const columns = new Map([ ["_index", "(index)"], ["expression", "Expressions"], ]); dispatch( messagesActions.messagesAdd([ { ...createSimpleTableMessage( columns, history.map((expression, index) => { return { _index: index, expression }; }) ), }, ]) ); break; case "inspectObject": { const objectActor = helperResult.object; if (hud.toolbox && !helperResult.forceExpandInConsole) { hud.toolbox.inspectObjectActor(objectActor); } else { webConsoleUI.inspectObjectActor(objectActor); } break; } case "help": hud.openLink(HELP_URL); break; case "copyValueToClipboard": clipboardHelper.copyString(helperResult.value); dispatch( messagesActions.messagesAdd([ { resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE, message: l10n.getStr( "webconsole.message.commands.copyValueToClipboard" ), }, ]) ); break; case "screenshotOutput": const { args, value } = helperResult; const targetFront = getSelectedTarget(hud.commands.targetCommand.store.getState()) || hud.currentTarget; let screenshotMessages; // @backward-compat { version 87 } The screenshot-content actor isn't available // in older server. // With an old server, the console actor captures the screenshot when handling // the command, and send it to the client which only needs to save it to a file. // With a new server, the server simply acknowledges the command, // and the client will drive the whole screenshot process (capture and save). if (targetFront.hasActor("screenshotContent")) { screenshotMessages = await captureAndSaveScreenshot( targetFront, webConsoleUI.getPanelWindow(), args ); } else { screenshotMessages = await saveScreenshot( webConsoleUI.getPanelWindow(), args, value ); } if (screenshotMessages && screenshotMessages.length) { dispatch( messagesActions.messagesAdd( screenshotMessages.map(message => ({ message: { level: message.level || "log", arguments: [message.text], chromeContext: true, }, resourceType: ResourceCommand.TYPES.CONSOLE_MESSAGE, })) ) ); } break; case "blockURL": const blockURL = helperResult.args.url; // The console actor isn't able to block the request as the console actor runs in the content // process, while the request has to be blocked from the parent process. // Then, calling the Netmonitor action will only update the visual state of the Netmonitor, // but we also have to block the request via the NetworkParentActor. await hud.commands.networkCommand.blockRequestForUrl(blockURL); toolbox .getPanel("netmonitor") ?.panelWin.store.dispatch( netmonitorBlockingActions.addBlockedUrl(blockURL) ); dispatch( messagesActions.messagesAdd([ { resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE, message: l10n.getFormatStr( "webconsole.message.commands.blockedURL", [blockURL] ), }, ]) ); break; case "unblockURL": const unblockURL = helperResult.args.url; await hud.commands.networkCommand.unblockRequestForUrl(unblockURL); toolbox .getPanel("netmonitor") ?.panelWin.store.dispatch( netmonitorBlockingActions.removeBlockedUrl(unblockURL) ); dispatch( messagesActions.messagesAdd([ { resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE, message: l10n.getFormatStr( "webconsole.message.commands.unblockedURL", [unblockURL] ), }, ]) ); // early return as we already dispatched necessary messages. return; // Sent when using ":command --help or :command --usage" // to help discover command arguments. // // The remote runtime will tell us about the usage as it may // be different from the client one. case "usage": dispatch( messagesActions.messagesAdd([ { resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE, message: helperResult.message, }, ]) ); break; case "traceOutput": // Nothing in particular to do. // The JSTRACER_STATE resource will report the start/stop of the profiler. break; } } const hasErrorMessage = response.exceptionMessage || (helperResult && helperResult.type === "error"); // Hide undefined results coming from helper functions. const hasUndefinedResult = result && typeof result == "object" && result.type == "undefined"; if (hasErrorMessage || helperHasRawOutput || !hasUndefinedResult) { dispatch(messagesActions.messagesAdd([response])); } }; } function focusInput() { return ({ hud }) => { return hud.focusInput(); }; } function setInputValue(value) { return ({ hud }) => { return hud.setInputValue(value); }; } /** * Request an eager evaluation from the server. * * @param {String} expression: The expression to evaluate. * @param {Boolean} force: When true, will request an eager evaluation again, even if * the expression is the same one than the one that was used in * the previous evaluation. */ function terminalInputChanged(expression, force = false) { return async ({ dispatch, webConsoleUI, hud, commands, getState }) => { const prefs = getAllPrefs(getState()); if (!prefs.eagerEvaluation) { return null; } const { terminalInput = "" } = getState().history; // Only re-evaluate if the expression did change. if ( (!terminalInput && !expression) || (typeof terminalInput === "string" && typeof expression === "string" && expression.trim() === terminalInput.trim() && !force) ) { return null; } dispatch({ type: SET_TERMINAL_INPUT, expression: expression.trim(), }); // There's no need to evaluate an empty string. if (!expression || !expression.trim()) { return dispatch({ type: SET_TERMINAL_EAGER_RESULT, expression, result: null, }); } let mapped; ({ expression, mapped } = await getMappedExpression(hud, expression)); // We don't want to evaluate top-level await expressions (see Bug 1786805) if (mapped?.await) { return dispatch({ type: SET_TERMINAL_EAGER_RESULT, expression, result: null, }); } const response = await commands.scriptCommand.execute(expression, { frameActor: hud.getSelectedFrameActorID(), selectedNodeActor: webConsoleUI.getSelectedNodeActorID(), selectedTargetFront: getSelectedTarget( hud.commands.targetCommand.store.getState() ), mapped, eager: true, }); return dispatch({ type: SET_TERMINAL_EAGER_RESULT, result: getEagerEvaluationResult(response), }); }; } /** * Refresh the current eager evaluation by requesting a new eager evaluation. */ function updateInstantEvaluationResultForCurrentExpression() { return ({ getState, dispatch }) => dispatch(terminalInputChanged(getState().history.terminalInput, true)); } function getEagerEvaluationResult(response) { const result = response.exception || response.result; // Don't show syntax errors results to the user. if (result?.isSyntaxError || (result && result.type == "undefined")) { return null; } return result; } function prettyPrintEditor() { return { type: EDITOR_PRETTY_PRINT, }; } module.exports = { evaluateExpression, focusInput, setInputValue, terminalInputChanged, updateInstantEvaluationResultForCurrentExpression, prettyPrintEditor, };