tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

input.js (15527B)


      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 {
      8  Utils: WebConsoleUtils,
      9 } = require("resource://devtools/client/webconsole/utils.js");
     10 const {
     11  EVALUATE_EXPRESSION,
     12  SET_TERMINAL_INPUT,
     13  SET_TERMINAL_EAGER_RESULT,
     14  EDITOR_PRETTY_PRINT,
     15  HELP_URL,
     16 } = require("resource://devtools/client/webconsole/constants.js");
     17 const {
     18  getAllPrefs,
     19 } = require("resource://devtools/client/webconsole/selectors/prefs.js");
     20 const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");
     21 const l10n = require("resource://devtools/client/webconsole/utils/l10n.js");
     22 
     23 loader.lazyServiceGetter(
     24  this,
     25  "clipboardHelper",
     26  "@mozilla.org/widget/clipboardhelper;1",
     27  "nsIClipboardHelper"
     28 );
     29 loader.lazyRequireGetter(
     30  this,
     31  "messagesActions",
     32  "resource://devtools/client/webconsole/actions/messages.js"
     33 );
     34 loader.lazyRequireGetter(
     35  this,
     36  "historyActions",
     37  "resource://devtools/client/webconsole/actions/history.js"
     38 );
     39 loader.lazyRequireGetter(
     40  this,
     41  "ConsoleCommand",
     42  "resource://devtools/client/webconsole/types.js",
     43  true
     44 );
     45 loader.lazyRequireGetter(
     46  this,
     47  "netmonitorBlockingActions",
     48  "resource://devtools/client/netmonitor/src/actions/request-blocking.js"
     49 );
     50 
     51 loader.lazyRequireGetter(
     52  this,
     53  ["saveScreenshot", "captureAndSaveScreenshot"],
     54  "resource://devtools/client/shared/screenshot.js",
     55  true
     56 );
     57 loader.lazyRequireGetter(
     58  this,
     59  "createSimpleTableMessage",
     60  "resource://devtools/client/webconsole/utils/messages.js",
     61  true
     62 );
     63 loader.lazyRequireGetter(
     64  this,
     65  "getSelectedTarget",
     66  "resource://devtools/shared/commands/target/selectors/targets.js",
     67  true
     68 );
     69 
     70 async function getMappedExpression(hud, expression) {
     71  let mapResult;
     72  try {
     73    mapResult = await hud.getMappedExpression(expression);
     74  } catch (e) {
     75    console.warn("Error when calling getMappedExpression", e);
     76  }
     77 
     78  let mapped = null;
     79  if (mapResult) {
     80    ({ expression, mapped } = mapResult);
     81  }
     82  return { expression, mapped };
     83 }
     84 
     85 function evaluateExpression(expression, from = "input") {
     86  return async ({ dispatch, webConsoleUI, hud, commands }) => {
     87    if (!expression) {
     88      expression = hud.getInputSelection() || hud.getInputValue();
     89    }
     90    if (!expression) {
     91      return null;
     92    }
     93 
     94    // We use the messages action as it's doing additional transformation on the message.
     95    const { messages } = dispatch(
     96      messagesActions.messagesAdd([
     97        new ConsoleCommand({
     98          messageText: expression,
     99          timeStamp: Date.now(),
    100        }),
    101      ])
    102    );
    103    const [consoleCommandMessage] = messages;
    104 
    105    dispatch({
    106      type: EVALUATE_EXPRESSION,
    107      expression,
    108      from,
    109    });
    110 
    111    WebConsoleUtils.usageCount++;
    112 
    113    let mapped;
    114    ({ expression, mapped } = await getMappedExpression(hud, expression));
    115 
    116    // Even if the evaluation fails,
    117    // we still need to pass the error response to onExpressionEvaluated.
    118    const onSettled = res => res;
    119 
    120    const response = await commands.scriptCommand
    121      .execute(expression, {
    122        frameActor: hud.getSelectedFrameActorID(),
    123        selectedNodeActor: webConsoleUI.getSelectedNodeActorID(),
    124        selectedTargetFront: getSelectedTarget(
    125          webConsoleUI.hud.commands.targetCommand.store.getState()
    126        ),
    127        mapped,
    128        // Allow breakpoints to be triggerred and the evaluated source to be shown in debugger UI
    129        disableBreaks: false,
    130      })
    131      .then(onSettled, onSettled);
    132 
    133    const serverConsoleCommandTimestamp = response.startTime;
    134 
    135    // In case of remote debugging, it might happen that the debuggee page does not have
    136    // the exact same clock time as the client. This could cause some ordering issues
    137    // where the result message is displayed *before* the expression that lead to it.
    138    if (
    139      serverConsoleCommandTimestamp &&
    140      consoleCommandMessage.timeStamp > serverConsoleCommandTimestamp
    141    ) {
    142      // If we're in such case, we remove the original command message, and add it again,
    143      // with the timestamp coming from the server.
    144      dispatch(messagesActions.messageRemove(consoleCommandMessage.id));
    145      dispatch(
    146        messagesActions.messagesAdd([
    147          new ConsoleCommand({
    148            messageText: expression,
    149            timeStamp: serverConsoleCommandTimestamp,
    150          }),
    151        ])
    152      );
    153    }
    154 
    155    return dispatch(onExpressionEvaluated(response));
    156  };
    157 }
    158 
    159 /**
    160 * The JavaScript evaluation response handler.
    161 *
    162 * @private
    163 * @param {object} response
    164 *        The message received from the server.
    165 */
    166 function onExpressionEvaluated(response) {
    167  return async ({ dispatch, webConsoleUI }) => {
    168    if (response.error) {
    169      console.error(`Evaluation error`, response.error, ": ", response.message);
    170      return;
    171    }
    172 
    173    // If the evaluation was a top-level await expression that was rejected, there will
    174    // be an uncaught exception reported, so we don't need to do anything.
    175    if (response.topLevelAwaitRejected === true) {
    176      return;
    177    }
    178 
    179    if (!response.helperResult) {
    180      webConsoleUI.wrapper.dispatchMessageAdd(response);
    181      return;
    182    }
    183 
    184    await dispatch(handleHelperResult(response));
    185  };
    186 }
    187 
    188 function handleHelperResult(response) {
    189  // eslint-disable-next-line complexity
    190  return async ({ dispatch, hud, toolbox, webConsoleUI, getState }) => {
    191    const { result, helperResult } = response;
    192    const helperHasRawOutput = !!helperResult?.rawOutput;
    193 
    194    if (helperResult?.type) {
    195      switch (helperResult.type) {
    196        case "exception":
    197          dispatch(
    198            messagesActions.messagesAdd([
    199              {
    200                level: "error",
    201                arguments: [helperResult.message],
    202                chromeContext: true,
    203                resourceType: ResourceCommand.TYPES.CONSOLE_MESSAGE,
    204              },
    205            ])
    206          );
    207          break;
    208        case "clearOutput":
    209          dispatch(messagesActions.messagesClear());
    210          break;
    211        case "clearHistory":
    212          dispatch(historyActions.clearHistory());
    213          break;
    214        case "historyOutput": {
    215          const history = getState().history.entries || [];
    216          const columns = new Map([
    217            ["_index", "(index)"],
    218            ["expression", "Expressions"],
    219          ]);
    220          dispatch(
    221            messagesActions.messagesAdd([
    222              {
    223                ...createSimpleTableMessage(
    224                  columns,
    225                  history.map((expression, index) => {
    226                    return { _index: index, expression };
    227                  })
    228                ),
    229              },
    230            ])
    231          );
    232          break;
    233        }
    234        case "inspectObject": {
    235          const objectActor = helperResult.object;
    236          if (hud.toolbox && !helperResult.forceExpandInConsole) {
    237            hud.toolbox.inspectObjectActor(objectActor);
    238          } else {
    239            webConsoleUI.inspectObjectActor(objectActor);
    240          }
    241          break;
    242        }
    243        case "help":
    244          hud.openLink(HELP_URL);
    245          break;
    246        case "copyValueToClipboard":
    247          clipboardHelper.copyString(helperResult.value);
    248          dispatch(
    249            messagesActions.messagesAdd([
    250              {
    251                resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE,
    252                message: l10n.getStr(
    253                  "webconsole.message.commands.copyValueToClipboard"
    254                ),
    255              },
    256            ])
    257          );
    258          break;
    259        case "screenshotOutput": {
    260          const { args, value } = helperResult;
    261          const targetFront =
    262            getSelectedTarget(hud.commands.targetCommand.store.getState()) ||
    263            hud.currentTarget;
    264          let screenshotMessages;
    265 
    266          // @backward-compat { version 87 } The screenshot-content actor isn't available
    267          // in older server.
    268          // With an old server, the console actor captures the screenshot when handling
    269          // the command, and send it to the client which only needs to save it to a file.
    270          // With a new server, the server simply acknowledges the command,
    271          // and the client will drive the whole screenshot process (capture and save).
    272          if (targetFront.hasActor("screenshotContent")) {
    273            screenshotMessages = await captureAndSaveScreenshot(
    274              targetFront,
    275              webConsoleUI.getPanelWindow(),
    276              args
    277            );
    278          } else {
    279            screenshotMessages = await saveScreenshot(
    280              webConsoleUI.getPanelWindow(),
    281              args,
    282              value
    283            );
    284          }
    285 
    286          if (screenshotMessages && screenshotMessages.length) {
    287            dispatch(
    288              messagesActions.messagesAdd(
    289                screenshotMessages.map(message => ({
    290                  level: message.level || "log",
    291                  arguments: [message.text],
    292                  chromeContext: true,
    293                  resourceType: ResourceCommand.TYPES.CONSOLE_MESSAGE,
    294                }))
    295              )
    296            );
    297          }
    298          break;
    299        }
    300        case "blockURL": {
    301          const blockURL = helperResult.args.url;
    302          // The console actor isn't able to block the request as the console actor runs in the content
    303          // process, while the request has to be blocked from the parent process.
    304          // Then, calling the Netmonitor action will only update the visual state of the Netmonitor,
    305          // but we also have to block the request via the NetworkParentActor.
    306          await hud.commands.networkCommand.blockRequestForUrl(blockURL);
    307          toolbox
    308            .getPanel("netmonitor")
    309            ?.panelWin.store.dispatch(
    310              netmonitorBlockingActions.addBlockedUrl(blockURL)
    311            );
    312 
    313          dispatch(
    314            messagesActions.messagesAdd([
    315              {
    316                resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE,
    317                message: l10n.getFormatStr(
    318                  "webconsole.message.commands.blockedURL",
    319                  [blockURL]
    320                ),
    321              },
    322            ])
    323          );
    324          break;
    325        }
    326        case "unblockURL": {
    327          const unblockURL = helperResult.args.url;
    328          await hud.commands.networkCommand.unblockRequestForUrl(unblockURL);
    329          toolbox
    330            .getPanel("netmonitor")
    331            ?.panelWin.store.dispatch(
    332              netmonitorBlockingActions.removeBlockedUrl(unblockURL)
    333            );
    334 
    335          dispatch(
    336            messagesActions.messagesAdd([
    337              {
    338                resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE,
    339                message: l10n.getFormatStr(
    340                  "webconsole.message.commands.unblockedURL",
    341                  [unblockURL]
    342                ),
    343              },
    344            ])
    345          );
    346          // early return as we already dispatched necessary messages.
    347          return;
    348        }
    349 
    350        // Sent when using ":command --help or :command --usage"
    351        // to help discover command arguments.
    352        //
    353        // The remote runtime will tell us about the usage as it may
    354        // be different from the client one.
    355        case "usage":
    356          dispatch(
    357            messagesActions.messagesAdd([
    358              {
    359                resourceType: ResourceCommand.TYPES.PLATFORM_MESSAGE,
    360                message: helperResult.message,
    361              },
    362            ])
    363          );
    364          break;
    365 
    366        case "traceOutput":
    367          // Nothing in particular to do.
    368          // The JSTRACER_STATE resource will report the start/stop of the profiler.
    369          break;
    370      }
    371    }
    372 
    373    const hasErrorMessage =
    374      response.exceptionMessage ||
    375      (helperResult && helperResult.type === "error");
    376 
    377    // Hide undefined results coming from helper functions.
    378    const hasUndefinedResult =
    379      result && typeof result == "object" && result.type == "undefined";
    380 
    381    if (hasErrorMessage || helperHasRawOutput || !hasUndefinedResult) {
    382      dispatch(messagesActions.messagesAdd([response]));
    383    }
    384  };
    385 }
    386 
    387 function focusInput() {
    388  return ({ hud }) => {
    389    return hud.focusInput();
    390  };
    391 }
    392 
    393 function setInputValue(value) {
    394  return ({ hud }) => {
    395    return hud.setInputValue(value);
    396  };
    397 }
    398 
    399 /**
    400 * Request an eager evaluation from the server.
    401 *
    402 * @param {string} expression: The expression to evaluate.
    403 * @param {boolean} force: When true, will request an eager evaluation again, even if
    404 *                         the expression is the same one than the one that was used in
    405 *                         the previous evaluation.
    406 */
    407 function terminalInputChanged(expression, force = false) {
    408  return async ({ dispatch, webConsoleUI, hud, commands, getState }) => {
    409    const prefs = getAllPrefs(getState());
    410    if (!prefs.eagerEvaluation) {
    411      return null;
    412    }
    413 
    414    const { terminalInput = "" } = getState().history;
    415 
    416    // Only re-evaluate if the expression did change.
    417    if (
    418      (!terminalInput && !expression) ||
    419      (typeof terminalInput === "string" &&
    420        typeof expression === "string" &&
    421        expression.trim() === terminalInput.trim() &&
    422        !force)
    423    ) {
    424      return null;
    425    }
    426 
    427    dispatch({
    428      type: SET_TERMINAL_INPUT,
    429      expression: expression.trim(),
    430    });
    431 
    432    // There's no need to evaluate an empty string.
    433    if (!expression || !expression.trim()) {
    434      return dispatch({
    435        type: SET_TERMINAL_EAGER_RESULT,
    436        expression,
    437        result: null,
    438      });
    439    }
    440 
    441    let mapped;
    442    ({ expression, mapped } = await getMappedExpression(hud, expression));
    443 
    444    // We don't want to evaluate top-level await expressions (see Bug 1786805)
    445    if (mapped?.await) {
    446      return dispatch({
    447        type: SET_TERMINAL_EAGER_RESULT,
    448        expression,
    449        result: null,
    450      });
    451    }
    452 
    453    const response = await commands.scriptCommand.execute(expression, {
    454      frameActor: hud.getSelectedFrameActorID(),
    455      selectedNodeActor: webConsoleUI.getSelectedNodeActorID(),
    456      selectedTargetFront: getSelectedTarget(
    457        hud.commands.targetCommand.store.getState()
    458      ),
    459      mapped,
    460      eager: true,
    461    });
    462 
    463    // If the terminal input changed while the expression was evaluated, don't render
    464    // the results of the eager evaluation, it will be handled by the last call to
    465    // terminalInputChanged
    466    if (expression.trim() !== getState().history?.terminalInput) {
    467      return null;
    468    }
    469 
    470    return dispatch({
    471      type: SET_TERMINAL_EAGER_RESULT,
    472      result: getEagerEvaluationResult(response),
    473    });
    474  };
    475 }
    476 
    477 /**
    478 * Refresh the current eager evaluation by requesting a new eager evaluation.
    479 */
    480 function updateInstantEvaluationResultForCurrentExpression() {
    481  return ({ getState, dispatch }) =>
    482    dispatch(terminalInputChanged(getState().history.terminalInput, true));
    483 }
    484 
    485 function getEagerEvaluationResult(response) {
    486  const result = response.exception || response.result;
    487  // Don't show syntax errors results to the user.
    488  if (result?.isSyntaxError || (result && result.type == "undefined")) {
    489    return null;
    490  }
    491 
    492  return result;
    493 }
    494 
    495 function prettyPrintEditor() {
    496  return {
    497    type: EDITOR_PRETTY_PRINT,
    498  };
    499 }
    500 
    501 module.exports = {
    502  evaluateExpression,
    503  focusInput,
    504  setInputValue,
    505  terminalInputChanged,
    506  updateInstantEvaluationResultForCurrentExpression,
    507  prettyPrintEditor,
    508 };