summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole/actions/input.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webconsole/actions/input.js')
-rw-r--r--devtools/client/webconsole/actions/input.js466
1 files changed, 466 insertions, 0 deletions
diff --git a/devtools/client/webconsole/actions/input.js b/devtools/client/webconsole/actions/input.js
new file mode 100644
index 0000000000..85b1867fc8
--- /dev/null
+++ b/devtools/client/webconsole/actions/input.js
@@ -0,0 +1,466 @@
+/* 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,
+} = 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
+);
+
+const HELP_URL =
+ "https://firefox-source-docs.mozilla.org/devtools-user/web_console/helpers/";
+
+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, toolbox, 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,
+ })
+ .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 "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;
+ }
+ }
+
+ 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,
+};