tor-browser

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

manager.js (30665B)


      1 /* This Smurce 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 loader.lazyRequireGetter(
      8  this,
      9  ["getCommandAndArgs"],
     10  "resource://devtools/server/actors/webconsole/commands/parser.js",
     11  true
     12 );
     13 
     14 loader.lazyGetter(this, "l10n", () => {
     15  return new Localization(
     16    [
     17      "devtools/shared/webconsole-commands.ftl",
     18      "devtools/server/actors/webconsole/commands/experimental-commands.ftl",
     19    ],
     20    true
     21  );
     22 });
     23 
     24 const lazy = {};
     25 ChromeUtils.defineESModuleGetters(
     26  lazy,
     27  {
     28    JSTracer: "resource://devtools/server/tracer/tracer.sys.mjs",
     29  },
     30  { global: "contextual" }
     31 );
     32 
     33 const USAGE_STRING_MAPPING = {
     34  block: "webconsole-commands-usage-block",
     35  trace: "webconsole-commands-usage-trace3",
     36  unblock: "webconsole-commands-usage-unblock",
     37 };
     38 
     39 /**
     40 * WebConsole commands manager.
     41 *
     42 * Defines a set of functions / variables ("commands") that are available from
     43 * the Web Console but not from the web page.
     44 *
     45 */
     46 const WebConsoleCommandsManager = {
     47  // Flag used by eager evaluation in order to allow the execution of commands
     48  // which are side effect free and disallow all the others.
     49  SIDE_EFFECT_FREE: Symbol("SIDE_EFFECT_FREE"),
     50 
     51  // Map of command name to command function or property descriptor (see register method)
     52  _registeredCommands: new Map(),
     53  // Map of command name to optional array of accepted argument names
     54  _validArguments: new Map(),
     55  // Set of command names that are side effect free
     56  _sideEffectFreeCommands: new Set(),
     57 
     58  /**
     59   * Register a new command.
     60   *
     61   * @param {object} options
     62   * @param {string} options.name
     63   *        The command name (exemple: "$", "screenshot",...))
     64   * @param {boolean} isSideEffectFree
     65   *        Tells if the command is free of any side effect to know
     66   *        if it can run in eager console evaluation.
     67   * @param {function|object} options.command
     68   *        The command to register.
     69   *        It can be:
     70   *          - a function for the command like "$()" or ":screenshot"
     71   *            which triggers some code.
     72   *          - a property descriptor for getters like "$0",
     73   *            which only returns a value.
     74   * @param {Array<string>} options.validArguments
     75   *        Optional list of valid arguments.
     76   *        If passed, we will assert that passed arguments are all valid on execution.
     77   *
     78   *  The command function or the command getter are passed a:
     79   *   - "owner" object as their first parameter (see the example below).
     80   *     See _createOwnerObject for definition.
     81   *   - "args" object with all parameters when this is ran as a ":my-command" command.
     82   *     See getCommandAndArgs for definition.
     83   *
     84   * Note that if you want to support `--help` argument, you need to provide a usage string in:
     85   * devtools/shared/locales/en-US/webconsole-commands.properties
     86   *
     87   * @example
     88   *
     89   *   WebConsoleCommandsManager.register("$", function (owner, selector)
     90   *   {
     91   *     return owner.window.document.querySelector(selector);
     92   *   },
     93   *   ["my-argument"]);
     94   *
     95   *   WebConsoleCommandsManager.register("$0", {
     96   *     get: function(owner) {
     97   *       return owner.makeDebuggeeValue(owner.selectedNode);
     98   *     }
     99   *   });
    100   */
    101  register({ name, isSideEffectFree, command, validArguments }) {
    102    if (
    103      typeof command != "function" &&
    104      !(typeof command == "object" && typeof command.get == "function")
    105    ) {
    106      throw new Error(
    107        "Invalid web console command. It can only be a function, or an object with a function as 'get' attribute"
    108      );
    109    }
    110    if (typeof isSideEffectFree !== "boolean") {
    111      throw new Error(
    112        "Invalid web console command. 'isSideEffectFree' attribute should be set and be a boolean"
    113      );
    114    }
    115    this._registeredCommands.set(name, command);
    116    if (validArguments) {
    117      this._validArguments.set(name, validArguments);
    118    }
    119    if (isSideEffectFree) {
    120      this._sideEffectFreeCommands.add(name);
    121    }
    122  },
    123 
    124  /**
    125   * Return the name of all registered commands.
    126   *
    127   * @return {Array} List of all command names.
    128   */
    129  getAllCommandNames() {
    130    return [...this._registeredCommands.keys()];
    131  },
    132 
    133  /**
    134   * There is two types of "commands" here.
    135   *
    136   * - Functions or variables exposed in the scope of the evaluated string from the WebConsole input.
    137   *   Example: $(), $0, copy(), clear(),...
    138   * - "True commands", which can also be ran from the WebConsole input with ":" prefix.
    139   *   Example: this list of commands.
    140   *   Note that some "true commands" are not exposed as function (see getColonOnlyCommandNames).
    141   *
    142   * The following list distinguish these "true commands" from the first category.
    143   * It especially avoid any JavaScript evaluation when the frontend tries to execute
    144   * a string starting with ':' character.
    145   */
    146  getAllColonCommandNames() {
    147    return ["block", "help", "history", "screenshot", "unblock", "trace"];
    148  },
    149 
    150  /**
    151   * Some commands are not exposed in the scope of the evaluated string,
    152   * and can only be used via `:command-name`.
    153   */
    154  getColonOnlyCommandNames() {
    155    return ["screenshot", "trace"];
    156  },
    157 
    158  /**
    159   * Map of all command objects keyed by command name.
    160   * Commands object are the objects passed to register() method.
    161   *
    162   * @return {Map<string -> command>}
    163   */
    164  getAllCommands() {
    165    return this._registeredCommands;
    166  },
    167 
    168  /**
    169   * Is the command name possibly overriding a symbol which
    170   * already exists in the paused frame or the global into which
    171   * we are about to execute into?
    172   */
    173  _isCommandNameAlreadyInScope(name, frame, dbgGlobal) {
    174    if (frame && frame.environment) {
    175      return !!frame.environment.find(name);
    176    }
    177 
    178    // Fallback on global scope when Debugger.Frame doesn't come along an
    179    // Environment, or is not a frame.
    180 
    181    try {
    182      // This can throw in Browser Toolbox tests
    183      const globalEnv = dbgGlobal.asEnvironment();
    184      if (globalEnv) {
    185        return !!dbgGlobal.asEnvironment().find(name);
    186      }
    187    } catch {}
    188 
    189    return !!dbgGlobal.getOwnPropertyDescriptor(name);
    190  },
    191 
    192  _createOwnerObject(
    193    consoleActor,
    194    debuggerGlobal,
    195    evalInput,
    196    selectedNodeActorID
    197  ) {
    198    const owner = {
    199      window: consoleActor.evalGlobal,
    200      makeDebuggeeValue: debuggerGlobal.makeDebuggeeValue.bind(debuggerGlobal),
    201      createValueGrip: consoleActor.createValueGrip.bind(consoleActor),
    202      preprocessDebuggerObject:
    203        consoleActor.preprocessDebuggerObject.bind(consoleActor),
    204      helperResult: null,
    205      consoleActor,
    206      evalInput,
    207    };
    208    if (selectedNodeActorID) {
    209      const actor = consoleActor.conn.getActor(selectedNodeActorID);
    210      if (actor) {
    211        owner.selectedNode = actor.rawNode;
    212      }
    213    }
    214    return owner;
    215  },
    216 
    217  _getCommandsForCurrentEnvironment() {
    218    // Not supporting extra commands in workers yet.  This should be possible to
    219    // add one by one as long as they don't require jsm/mjs, Cu, etc.
    220    return isWorker ? new Map() : this.getAllCommands();
    221  },
    222 
    223  /**
    224   * Create an object with the API we expose to the Web Console during
    225   * JavaScript evaluation.
    226   * This object inherits properties and methods from the Web Console actor.
    227   *
    228   * @param object consoleActor
    229   *        The related web console actor evaluating some code.
    230   * @param object debuggerGlobal
    231   *        A Debugger.Object that wraps a content global. This is used for the
    232   *        Web Console Commands.
    233   * @param object frame (optional)
    234   *        The frame where the string was evaluated.
    235   * @param string evalInput
    236   *        String to evaluate.
    237   * @param string selectedNodeActorID
    238   *        The Node actor ID of the currently selected DOM Element, if any is selected.
    239   * @param bool preferConsoleCommandsOverLocalSymbols
    240   *        If true, define all bindings even if there's conflicting existing
    241   *        symbols.  This is for the case evaluating non-user code in frame
    242   *        environment.
    243   *
    244   * @return object
    245   *         Object with two properties:
    246   *         - 'bindings', the object with all commands set as attribute on this object.
    247   *         - 'getHelperResult', a live getter returning the additional data the last command
    248   *           which executed want to convey to the frontend.
    249   *           (The return value of commands isn't returned to the client but it only
    250   *            returned to the code ran from console evaluation)
    251   */
    252  getWebConsoleCommands(
    253    consoleActor,
    254    debuggerGlobal,
    255    frame,
    256    evalInput,
    257    selectedNodeActorID,
    258    preferConsoleCommandsOverLocalSymbols
    259  ) {
    260    const bindings = Object.create(null);
    261 
    262    const owner = this._createOwnerObject(
    263      consoleActor,
    264      debuggerGlobal,
    265      evalInput,
    266      selectedNodeActorID
    267    );
    268 
    269    const evalGlobal = consoleActor.evalGlobal;
    270    function maybeExport(obj, name) {
    271      if (typeof obj[name] != "function") {
    272        return;
    273      }
    274 
    275      // By default, chrome-implemented functions that are exposed to content
    276      // refuse to accept arguments that are cross-origin for the caller. This
    277      // is generally the safe thing, but causes problems for certain console
    278      // helpers like cd(), where we users sometimes want to pass a cross-origin
    279      // window. To circumvent this restriction, we use exportFunction along
    280      // with a special option designed for this purpose. See bug 1051224.
    281      obj[name] = Cu.exportFunction(obj[name], evalGlobal, {
    282        allowCrossOriginArguments: true,
    283      });
    284    }
    285 
    286    const commands = this._getCommandsForCurrentEnvironment();
    287 
    288    const colonOnlyCommandNames = this.getColonOnlyCommandNames();
    289    for (const [name, command] of commands) {
    290      // When we run user code in frame, we want to avoid overriding existing
    291      // symbols with commands.
    292      //
    293      // When we run user code in global scope, all bindings are automatically
    294      // shadowed, except for "help" function which is checked by getEvalInput.
    295      //
    296      // When preferConsoleCommandsOverLocalSymbols is true, ignore symbols in
    297      // the current scope and always use commands ones.
    298      if (
    299        !preferConsoleCommandsOverLocalSymbols &&
    300        (frame || name === "help") &&
    301        this._isCommandNameAlreadyInScope(name, frame, debuggerGlobal)
    302      ) {
    303        continue;
    304      }
    305      // Also ignore commands which can only be run with the `:` prefix.
    306      if (colonOnlyCommandNames.includes(name)) {
    307        continue;
    308      }
    309 
    310      const descriptor = {
    311        // We force the enumerability and the configurability (so the
    312        // WebConsoleActor can reconfigure the property).
    313        enumerable: true,
    314        configurable: true,
    315      };
    316 
    317      if (typeof command === "function") {
    318        // Function commands
    319        descriptor.value = command.bind(undefined, owner);
    320        maybeExport(descriptor, "value");
    321 
    322        // Unfortunately evalWithBindings will access all bindings values,
    323        // which would trigger a debuggee native call because bindings's property
    324        // is using Cu.exportFunction.
    325        // Put a magic symbol attribute on them in order to carefully accept
    326        // all bindings as being side effect safe by default.
    327        if (this._sideEffectFreeCommands.has(name)) {
    328          descriptor.value.isSideEffectFree = this.SIDE_EFFECT_FREE;
    329        }
    330 
    331        // Make sure the helpers can be used during eval.
    332        descriptor.value = debuggerGlobal.makeDebuggeeValue(descriptor.value);
    333      } else if (typeof command?.get === "function") {
    334        // Getter commands
    335        descriptor.get = command.get.bind(undefined, owner);
    336        maybeExport(descriptor, "get");
    337 
    338        // See comment in previous block.
    339        if (this._sideEffectFreeCommands.has(name)) {
    340          descriptor.get.isSideEffectFree = this.SIDE_EFFECT_FREE;
    341        }
    342      }
    343      Object.defineProperty(bindings, name, descriptor);
    344    }
    345 
    346    return {
    347      // Use a method as commands will update owner.helperResult later
    348      getHelperResult() {
    349        return owner.helperResult;
    350      },
    351      bindings,
    352    };
    353  },
    354 
    355  /**
    356   * Create a function for given ':command'-style command.
    357   *
    358   * @param object consoleActor
    359   *        The related web console actor evaluating some code.
    360   * @param object debuggerGlobal
    361   *        A Debugger.Object that wraps a content global. This is used for the
    362   *        Web Console Commands.
    363   * @param string selectedNodeActorID
    364   *        The Node actor ID of the currently selected DOM Element, if any is selected.
    365   * @param string evalInput
    366   *        String to evaluate.
    367   *
    368   * @return object
    369   *         Object with two properties:
    370   *         - 'commandFunc', a function corresponds to the 'commandName'
    371   *         - 'getHelperResult', a live getter returning the data the command
    372   *           which executed want to convey to the frontend.
    373   */
    374  executeCommand(consoleActor, debuggerGlobal, selectedNodeActorID, evalInput) {
    375    const { command, args } = getCommandAndArgs(evalInput);
    376    const commands = this._getCommandsForCurrentEnvironment();
    377    if (!commands.has(command)) {
    378      throw new Error(`Unsupported command '${command}'`);
    379    }
    380 
    381    if (args.help || args.usage) {
    382      const l10nKey = USAGE_STRING_MAPPING[command];
    383      if (l10nKey) {
    384        const message = l10n.formatValueSync(l10nKey);
    385        if (message && message !== l10nKey) {
    386          return {
    387            result: null,
    388            helperResult: {
    389              type: "usage",
    390              message,
    391            },
    392          };
    393        }
    394      }
    395    }
    396 
    397    const validArguments = this._validArguments.get(command);
    398    if (validArguments) {
    399      for (const key of Object.keys(args)) {
    400        if (!validArguments.includes(key)) {
    401          throw new Error(
    402            `:${command} command doesn't support '${key}' argument.`
    403          );
    404        }
    405      }
    406    }
    407 
    408    const owner = this._createOwnerObject(
    409      consoleActor,
    410      debuggerGlobal,
    411      evalInput,
    412      selectedNodeActorID
    413    );
    414 
    415    const commandFunction = commands.get(command);
    416 
    417    // This is where we run the command passed to register method
    418    const result = commandFunction(owner, args);
    419 
    420    return {
    421      result,
    422 
    423      // commandFunction may mutate owner.helperResult which is used
    424      // to convey additional data to the frontend.
    425      helperResult: owner.helperResult,
    426    };
    427  },
    428 };
    429 
    430 exports.WebConsoleCommandsManager = WebConsoleCommandsManager;
    431 
    432 /*
    433 * Built-in commands.
    434 *
    435 * A list of helper functions used by Firebug can be found here:
    436 *   http://getfirebug.com/wiki/index.php/Command_Line_API
    437 */
    438 
    439 /**
    440 * Find the first node matching a CSS selector.
    441 *
    442 * @param string selector
    443 *        A string that is passed to window.document.querySelector
    444 * @param [optional] Node element
    445 *        An optional Node to replace window.document
    446 * @return Node or null
    447 *         The result of calling document.querySelectorAll(selector).
    448 */
    449 WebConsoleCommandsManager.register({
    450  name: "$",
    451  isSideEffectFree: true,
    452  command(owner, selector, element) {
    453    try {
    454      if (
    455        element &&
    456        element.querySelector &&
    457        (element.nodeType == Node.ELEMENT_NODE ||
    458          element.nodeType == Node.DOCUMENT_NODE ||
    459          element.nodeType == Node.DOCUMENT_FRAGMENT_NODE)
    460      ) {
    461        return element.querySelector(selector);
    462      }
    463      return owner.window.document.querySelector(selector);
    464    } catch (err) {
    465      // Throw an error like `err` but that belongs to `owner.window`.
    466      throw new owner.window.DOMException(err.message, err.name);
    467    }
    468  },
    469 });
    470 
    471 /**
    472 * Find the nodes matching a CSS selector.
    473 *
    474 * @param string selector
    475 *        A string that is passed to window.document.querySelectorAll.
    476 * @param [optional] Node element
    477 *        An optional root Node, defaults to window.document
    478 * @return array of Node
    479 *         The result of calling document.querySelector(selector) in an array.
    480 */
    481 WebConsoleCommandsManager.register({
    482  name: "$$",
    483  isSideEffectFree: true,
    484  command(owner, selector, element) {
    485    let scope = owner.window.document;
    486    try {
    487      if (
    488        element &&
    489        element.querySelectorAll &&
    490        (element.nodeType == Node.ELEMENT_NODE ||
    491          element.nodeType == Node.DOCUMENT_NODE ||
    492          element.nodeType == Node.DOCUMENT_FRAGMENT_NODE)
    493      ) {
    494        scope = element;
    495      }
    496      const nodes = scope.querySelectorAll(selector);
    497      const result = new owner.window.Array();
    498      // Calling owner.window.Array.from() doesn't work without accessing the
    499      // wrappedJSObject, so just loop through the results instead.
    500      for (let i = 0; i < nodes.length; i++) {
    501        result.push(nodes[i]);
    502      }
    503      return result;
    504    } catch (err) {
    505      // Throw an error like `err` but that belongs to `owner.window`.
    506      throw new owner.window.DOMException(err.message, err.name);
    507    }
    508  },
    509 });
    510 
    511 /**
    512 * Find the nodes matching a CSS selector, including those inside shadow DOM
    513 *
    514 * @param string selector
    515 *        A string that is passed to all `querySelectorAll` calls performed by this command.
    516 * @param [optional] Node element
    517 *        An optional root Node, defaults to window.document
    518 * @return array of Node
    519 *         An array containing the nodes returned by calling `querySelectorAll(selector)`
    520 *         on `element` and on all shadow hosts under element (recursively).
    521 */
    522 WebConsoleCommandsManager.register({
    523  name: "$$$",
    524  isSideEffectFree: true,
    525  command(owner, selector, element) {
    526    let scope = owner.window.document;
    527    try {
    528      if (
    529        element?.querySelectorAll &&
    530        (element.nodeType == Node.ELEMENT_NODE ||
    531          element.nodeType == Node.DOCUMENT_NODE ||
    532          element.nodeType == Node.DOCUMENT_FRAGMENT_NODE)
    533      ) {
    534        scope = element;
    535      }
    536 
    537      const result = new owner.window.Array();
    538 
    539      const collectElements = root => {
    540        const nodes = root.querySelectorAll(selector);
    541        // Calling owner.window.Array.from() doesn't work without accessing the
    542        // wrappedJSObject, so just loop through the results instead.
    543        for (let i = 0, len = nodes.length; i < len; i++) {
    544          // If we have a native anonymous element, it's seen as a cross-origin object
    545          // and can't be added to result. We could waive `result` to avoid this exception,
    546          // but those nodes would show up as `Restricted` (See Bug 2006913), so it's not
    547          // really useful. If we'd have a proper rendering for those, ideally we'd use
    548          // the Inspector Walker filter to see if a node should be skipped or not
    549          // and we could add the node here.
    550          if (nodes[i].isNativeAnonymous) {
    551            continue;
    552          }
    553 
    554          result.push(nodes[i]);
    555        }
    556 
    557        // If the scope is a host, run the query inside its shadow DOM
    558        if (root.openOrClosedShadowRoot) {
    559          collectElements(root.openOrClosedShadowRoot);
    560        }
    561 
    562        // Finally, run the query for all hosts in scope
    563        const all = root.querySelectorAll("*");
    564        for (let i = 0, len = all.length; i < len; i++) {
    565          const el = all[i];
    566          if (el.openOrClosedShadowRoot) {
    567            collectElements(el.openOrClosedShadowRoot);
    568          }
    569        }
    570      };
    571 
    572      collectElements(scope);
    573 
    574      return result;
    575    } catch (err) {
    576      // Throw an error like `err` but that belongs to `owner.window`.
    577      throw new owner.window.DOMException(err.message, err.name);
    578    }
    579  },
    580 });
    581 
    582 /**
    583 * Returns the result of the last console input evaluation
    584 *
    585 * @return object|undefined
    586 * Returns last console evaluation or undefined
    587 */
    588 WebConsoleCommandsManager.register({
    589  name: "$_",
    590  isSideEffectFree: true,
    591  command: {
    592    get(owner) {
    593      return owner.consoleActor.getLastConsoleInputEvaluation();
    594    },
    595  },
    596 });
    597 
    598 /**
    599 * Runs an xPath query and returns all matched nodes.
    600 *
    601 * @param string xPath
    602 *        xPath search query to execute.
    603 * @param [optional] Node context
    604 *        Context to run the xPath query on. Uses window.document if not set.
    605 * @param [optional] string|number resultType
    606          Specify the result type. Default value XPathResult.ANY_TYPE
    607 * @return array of Node
    608 */
    609 WebConsoleCommandsManager.register({
    610  name: "$x",
    611  isSideEffectFree: true,
    612  command(
    613    owner,
    614    xPath,
    615    context,
    616    resultType = owner.window.XPathResult.ANY_TYPE
    617  ) {
    618    const nodes = new owner.window.Array();
    619    // Not waiving Xrays, since we want the original Document.evaluate function,
    620    // instead of anything that's been redefined.
    621    const doc = owner.window.document;
    622    context = context || doc;
    623    switch (resultType) {
    624      case "number":
    625        resultType = owner.window.XPathResult.NUMBER_TYPE;
    626        break;
    627 
    628      case "string":
    629        resultType = owner.window.XPathResult.STRING_TYPE;
    630        break;
    631 
    632      case "bool":
    633        resultType = owner.window.XPathResult.BOOLEAN_TYPE;
    634        break;
    635 
    636      case "node":
    637        resultType = owner.window.XPathResult.FIRST_ORDERED_NODE_TYPE;
    638        break;
    639 
    640      case "nodes":
    641        resultType = owner.window.XPathResult.UNORDERED_NODE_ITERATOR_TYPE;
    642        break;
    643    }
    644    const results = doc.evaluate(xPath, context, null, resultType, null);
    645    if (results.resultType === owner.window.XPathResult.NUMBER_TYPE) {
    646      return results.numberValue;
    647    }
    648    if (results.resultType === owner.window.XPathResult.STRING_TYPE) {
    649      return results.stringValue;
    650    }
    651    if (results.resultType === owner.window.XPathResult.BOOLEAN_TYPE) {
    652      return results.booleanValue;
    653    }
    654    if (
    655      results.resultType === owner.window.XPathResult.ANY_UNORDERED_NODE_TYPE ||
    656      results.resultType === owner.window.XPathResult.FIRST_ORDERED_NODE_TYPE
    657    ) {
    658      return results.singleNodeValue;
    659    }
    660    if (
    661      results.resultType ===
    662        owner.window.XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE ||
    663      results.resultType === owner.window.XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
    664    ) {
    665      for (let i = 0; i < results.snapshotLength; i++) {
    666        nodes.push(results.snapshotItem(i));
    667      }
    668      return nodes;
    669    }
    670 
    671    let node;
    672    while ((node = results.iterateNext())) {
    673      nodes.push(node);
    674    }
    675 
    676    return nodes;
    677  },
    678 });
    679 
    680 /**
    681 * Returns the currently selected object in the highlighter.
    682 *
    683 * @return Object representing the current selection in the
    684 *         Inspector, or null if no selection exists.
    685 */
    686 WebConsoleCommandsManager.register({
    687  name: "$0",
    688  isSideEffectFree: true,
    689  command: {
    690    get(owner) {
    691      return owner.makeDebuggeeValue(owner.selectedNode);
    692    },
    693  },
    694 });
    695 
    696 /**
    697 * Clears the output of the WebConsole.
    698 */
    699 WebConsoleCommandsManager.register({
    700  name: "clear",
    701  isSideEffectFree: false,
    702  command(owner) {
    703    owner.helperResult = {
    704      type: "clearOutput",
    705    };
    706  },
    707 });
    708 
    709 /**
    710 * Clears the input history of the WebConsole.
    711 */
    712 WebConsoleCommandsManager.register({
    713  name: "clearHistory",
    714  isSideEffectFree: false,
    715  command(owner) {
    716    owner.helperResult = {
    717      type: "clearHistory",
    718    };
    719  },
    720 });
    721 
    722 /**
    723 * Returns the result of Object.keys(object).
    724 *
    725 * @param object object
    726 *        Object to return the property names from.
    727 * @return array of strings
    728 */
    729 WebConsoleCommandsManager.register({
    730  name: "keys",
    731  isSideEffectFree: true,
    732  command(owner, object) {
    733    // Need to waive Xrays so we can iterate functions and accessor properties
    734    return Cu.cloneInto(Object.keys(Cu.waiveXrays(object)), owner.window);
    735  },
    736 });
    737 
    738 /**
    739 * Returns the values of all properties on object.
    740 *
    741 * @param object object
    742 *        Object to display the values from.
    743 * @return array of string
    744 */
    745 WebConsoleCommandsManager.register({
    746  name: "values",
    747  isSideEffectFree: true,
    748  command(owner, object) {
    749    const values = [];
    750    // Need to waive Xrays so we can iterate functions and accessor properties
    751    const waived = Cu.waiveXrays(object);
    752    const names = Object.getOwnPropertyNames(waived);
    753 
    754    for (const name of names) {
    755      values.push(waived[name]);
    756    }
    757 
    758    return Cu.cloneInto(values, owner.window);
    759  },
    760 });
    761 
    762 /**
    763 * Opens a help window in MDN.
    764 */
    765 WebConsoleCommandsManager.register({
    766  name: "help",
    767  isSideEffectFree: false,
    768  command(owner) {
    769    owner.helperResult = { type: "help" };
    770  },
    771 });
    772 
    773 /**
    774 * Inspects the passed object. This is done by opening the PropertyPanel.
    775 *
    776 * @param object object
    777 *        Object to inspect.
    778 */
    779 WebConsoleCommandsManager.register({
    780  name: "inspect",
    781  isSideEffectFree: false,
    782  command(owner, object, forceExpandInConsole = false) {
    783    const dbgObj = owner.preprocessDebuggerObject(
    784      owner.makeDebuggeeValue(object)
    785    );
    786 
    787    const grip = owner.createValueGrip(dbgObj);
    788    owner.helperResult = {
    789      type: "inspectObject",
    790      input: owner.evalInput,
    791      object: grip,
    792      forceExpandInConsole,
    793    };
    794  },
    795 });
    796 
    797 /**
    798 * Copy the String representation of a value to the clipboard.
    799 *
    800 * @param any value
    801 *        A value you want to copy as a string.
    802 * @return void
    803 */
    804 WebConsoleCommandsManager.register({
    805  name: "copy",
    806  isSideEffectFree: false,
    807  command(owner, value) {
    808    let payload;
    809    try {
    810      if (Element.isInstance(value)) {
    811        payload = value.outerHTML;
    812      } else if (typeof value == "string") {
    813        payload = value;
    814      } else {
    815        // Need to waive Xrays so we can iterate accessor properties.
    816        // If Cu is not defined, we are running on a worker thread, where xrays don't exist.
    817        if (value && Cu) {
    818          value = Cu.waiveXrays(value);
    819        }
    820        payload = JSON.stringify(value, null, "  ");
    821      }
    822    } catch (ex) {
    823      owner.helperResult = {
    824        type: "error",
    825        message: "webconsole.error.commands.copyError",
    826        messageArgs: [ex.toString()],
    827      };
    828      return;
    829    }
    830    owner.helperResult = {
    831      type: "copyValueToClipboard",
    832      value: payload,
    833    };
    834  },
    835 });
    836 
    837 /**
    838 * Take a screenshot of a page.
    839 *
    840 * @param object args
    841 *               The arguments to be passed to the screenshot
    842 * @return void
    843 */
    844 WebConsoleCommandsManager.register({
    845  name: "screenshot",
    846  isSideEffectFree: false,
    847  command(owner, args = {}) {
    848    owner.helperResult = {
    849      type: "screenshotOutput",
    850      args,
    851    };
    852  },
    853 });
    854 
    855 /**
    856 * Shows a history of commands and expressions previously executed within the command line.
    857 *
    858 * @param object args
    859 *               The arguments to be passed to the history
    860 * @return void
    861 */
    862 WebConsoleCommandsManager.register({
    863  name: "history",
    864  isSideEffectFree: false,
    865  command(owner, args = {}) {
    866    owner.helperResult = {
    867      type: "historyOutput",
    868      args,
    869    };
    870  },
    871 });
    872 
    873 /**
    874 * Block specific resource from loading
    875 *
    876 * @param object args
    877 *               an object with key "url", i.e. a filter
    878 *
    879 * @return void
    880 */
    881 WebConsoleCommandsManager.register({
    882  name: "block",
    883  isSideEffectFree: false,
    884  command(owner, args = {}) {
    885    // Note that this command is implemented in the frontend, from actions's input.js
    886    // We only forward the command arguments back to the client.
    887    if (!args.url) {
    888      owner.helperResult = {
    889        type: "error",
    890        message: "webconsole.messages.commands.blockArgMissing",
    891      };
    892      return;
    893    }
    894 
    895    owner.helperResult = {
    896      type: "blockURL",
    897      args,
    898    };
    899  },
    900  validArguments: ["url"],
    901 });
    902 
    903 /**
    904 * Unblock a blocked a resource
    905 *
    906 * @param object filter
    907 *               an object with key "url", i.e. a filter
    908 *
    909 * @return void
    910 */
    911 WebConsoleCommandsManager.register({
    912  name: "unblock",
    913  isSideEffectFree: false,
    914  command(owner, args = {}) {
    915    // Note that this command is implemented in the frontend, from actions's input.js
    916    // We only forward the command arguments back to the client.
    917    if (!args.url) {
    918      owner.helperResult = {
    919        type: "error",
    920        message: "webconsole.messages.commands.blockArgMissing",
    921      };
    922      return;
    923    }
    924 
    925    owner.helperResult = {
    926      type: "unblockURL",
    927      args,
    928    };
    929  },
    930  validArguments: ["url"],
    931 });
    932 
    933 /**
    934 * Toggle JavaScript tracing
    935 *
    936 * @param object args
    937 *        An object with various configuration only valid when starting the tracing.
    938 *
    939 * @return void
    940 */
    941 WebConsoleCommandsManager.register({
    942  name: "trace",
    943  isSideEffectFree: false,
    944  command(owner, args) {
    945    // Disable :trace command on worker until this feature is enabled by default
    946    if (isWorker) {
    947      throw new Error(":trace command isn't supported in workers");
    948    }
    949 
    950    if (!owner.consoleActor.targetActor.isTracerFeatureEnabled) {
    951      throw new Error(
    952        ":trace requires 'devtools.debugger.features.javascript-tracing' preference to be true"
    953      );
    954    }
    955    const tracerActor =
    956      owner.consoleActor.targetActor.getTargetScopedActor("tracer");
    957    const logMethod = args.logMethod || "console";
    958    let traceDOMMutations = null;
    959    if ("dom-mutations" in args) {
    960      // When no value is passed, track all types of mutations
    961      if (args["dom-mutations"] === true) {
    962        traceDOMMutations = ["add", "attributes", "remove"];
    963      } else if (typeof args["dom-mutations"] == "string") {
    964        // Otherwise consider the value as coma seperated list and remove any white space.
    965        traceDOMMutations = args["dom-mutations"].split(",").map(e => e.trim());
    966        const acceptedValues = Object.values(lazy.JSTracer.DOM_MUTATIONS);
    967        if (!traceDOMMutations.every(e => acceptedValues.includes(e))) {
    968          throw new Error(
    969            `:trace --dom-mutations only accept a list of strings whose values can be: ${acceptedValues}`
    970          );
    971        }
    972      } else {
    973        throw new Error(
    974          ":trace --dom-mutations accept only no arguments, or a list mutation type strings (add,attributes,remove)"
    975        );
    976      }
    977    }
    978    // Note that toggleTracing does some sanity checks and will throw meaningful error
    979    // when the arguments are wrong.
    980    const enabled = tracerActor.toggleTracing({
    981      logMethod,
    982      prefix: args.prefix || null,
    983      traceFunctionReturn: !!args.returns,
    984      traceValues: !!args.values,
    985      traceOnNextInteraction: args["on-next-interaction"] || null,
    986      traceDOMMutations,
    987      maxDepth: args["max-depth"] || null,
    988      maxRecords: args["max-records"] || null,
    989    });
    990 
    991    owner.helperResult = {
    992      type: "traceOutput",
    993      enabled,
    994      logMethod,
    995    };
    996  },
    997  validArguments: [
    998    "logMethod",
    999    "max-depth",
   1000    "max-records",
   1001    "on-next-interaction",
   1002    "dom-mutations",
   1003    "prefix",
   1004    "returns",
   1005    "values",
   1006  ],
   1007 });