tor-browser

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

EvaluationContextSelector.js (11627B)


      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 // React & Redux
      8 const {
      9  Component,
     10  createFactory,
     11 } = require("resource://devtools/client/shared/vendor/react.mjs");
     12 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     13 
     14 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     15 const {
     16  connect,
     17 } = require("resource://devtools/client/shared/vendor/react-redux.js");
     18 
     19 const targetActions = require("resource://devtools/shared/commands/target/actions/targets.js");
     20 const webconsoleActions = require("resource://devtools/client/webconsole/actions/index.js");
     21 
     22 const {
     23  l10n,
     24 } = require("resource://devtools/client/webconsole/utils/messages.js");
     25 const targetSelectors = require("resource://devtools/shared/commands/target/selectors/targets.js");
     26 
     27 loader.lazyGetter(this, "TARGET_TYPES", function () {
     28  return require("resource://devtools/shared/commands/target/target-command.js")
     29    .TYPES;
     30 });
     31 
     32 // Additional Components
     33 const MenuButton = createFactory(
     34  require("resource://devtools/client/shared/components/menu/MenuButton.js")
     35 );
     36 
     37 loader.lazyGetter(this, "MenuItem", function () {
     38  return createFactory(
     39    require("resource://devtools/client/shared/components/menu/MenuItem.js")
     40  );
     41 });
     42 
     43 loader.lazyGetter(this, "MenuList", function () {
     44  return createFactory(
     45    require("resource://devtools/client/shared/components/menu/MenuList.js")
     46  );
     47 });
     48 
     49 class EvaluationContextSelector extends Component {
     50  static get propTypes() {
     51    return {
     52      selectTarget: PropTypes.func.isRequired,
     53      onContextChange: PropTypes.func.isRequired,
     54      selectedTarget: PropTypes.object,
     55      lastTargetRefresh: PropTypes.number,
     56      targets: PropTypes.array,
     57      webConsoleUI: PropTypes.object.isRequired,
     58    };
     59  }
     60 
     61  shouldComponentUpdate(nextProps) {
     62    if (this.props.selectedTarget !== nextProps.selectedTarget) {
     63      return true;
     64    }
     65 
     66    if (this.props.lastTargetRefresh !== nextProps.lastTargetRefresh) {
     67      return true;
     68    }
     69 
     70    if (this.props.targets.length !== nextProps.targets.length) {
     71      return true;
     72    }
     73 
     74    for (let i = 0; i < nextProps.targets.length; i++) {
     75      const target = this.props.targets[i];
     76      const nextTarget = nextProps.targets[i];
     77      if (target.url != nextTarget.url || target.name != nextTarget.name) {
     78        return true;
     79      }
     80    }
     81    return false;
     82  }
     83 
     84  componentDidUpdate(prevProps) {
     85    if (this.props.selectedTarget !== prevProps.selectedTarget) {
     86      this.props.onContextChange();
     87    }
     88  }
     89 
     90  getIcon(target) {
     91    if (target.targetType === TARGET_TYPES.FRAME) {
     92      return "chrome://devtools/content/debugger/images/globe-small.svg";
     93    }
     94 
     95    if (
     96      target.targetType === TARGET_TYPES.WORKER ||
     97      target.targetType === TARGET_TYPES.SHARED_WORKER ||
     98      target.targetType === TARGET_TYPES.SERVICE_WORKER
     99    ) {
    100      return "chrome://devtools/content/debugger/images/worker.svg";
    101    }
    102 
    103    if (target.targetType === TARGET_TYPES.PROCESS) {
    104      return "chrome://devtools/content/debugger/images/window.svg";
    105    }
    106 
    107    if (target.targetType === TARGET_TYPES.CONTENT_SCRIPT) {
    108      return "chrome://devtools/content/debugger/images/sources/extension.svg";
    109    }
    110 
    111    return null;
    112  }
    113 
    114  renderMenuItem(target, indented = false) {
    115    const { selectTarget, selectedTarget } = this.props;
    116 
    117    // When debugging a Web Extension, the top level target is always the fallback document.
    118    // It isn't really a top level document as it won't be the parent of any other.
    119    // So only print its name.
    120    const label =
    121      target.isTopLevel && !target.commands.descriptorFront.isWebExtension
    122        ? l10n.getStr("webconsole.input.selector.top")
    123        : target.name;
    124 
    125    return MenuItem({
    126      key: `webconsole-evaluation-selector-item-${target.actorID}`,
    127      className: `menu-item webconsole-evaluation-selector-item ${
    128        indented ? "indented" : ""
    129      }`,
    130      type: "checkbox",
    131      checked: selectedTarget ? selectedTarget == target : target.isTopLevel,
    132      label,
    133      tooltip: target.url || target.name,
    134      icon: this.getIcon(target),
    135      onClick: () => selectTarget(target.actorID),
    136    });
    137  }
    138 
    139  renderMenuItems() {
    140    const { targets } = this.props;
    141 
    142    // Let's sort the targets (using "numeric" so Content processes are ordered by PID).
    143    const collator = new Intl.Collator("en", { numeric: true });
    144    targets.sort((a, b) => collator.compare(a.name, b.name));
    145 
    146    // When in Browser Toolbox, we want to display the process targets with the frames
    147    // in the same process as a group
    148    // e.g.
    149    //     |------------------------------|
    150    //     | Top                          |
    151    //     | -----------------------------|
    152    //     | (pid 1234) priviledgedabout  |
    153    //     | New Tab                      |
    154    //     | -----------------------------|
    155    //     | (pid 5678) web               |
    156    //     | cnn.com                      |
    157    //     | -----------------------------|
    158    //     | RemoteSettingWorker.js       |
    159    //     |------------------------------|
    160    //
    161 
    162    const { webConsoleUI } = this.props;
    163    const handleProcessTargets =
    164      webConsoleUI.isBrowserConsole || webConsoleUI.isBrowserToolboxConsole;
    165 
    166    const processTargets = [];
    167    const frameTargets = new Set();
    168    const contentScriptTargets = new Set();
    169    const workerTargets = new Set();
    170    let topTarget = null;
    171 
    172    for (const target of targets) {
    173      if (target.isTopLevel) {
    174        topTarget = target;
    175        continue;
    176      }
    177      switch (target.targetType) {
    178        case TARGET_TYPES.PROCESS:
    179          processTargets.push(target);
    180          break;
    181        case TARGET_TYPES.FRAME:
    182          frameTargets.add(target);
    183          break;
    184        case TARGET_TYPES.CONTENT_SCRIPT:
    185          contentScriptTargets.add(target);
    186          break;
    187        case TARGET_TYPES.WORKER:
    188        case TARGET_TYPES.SHARED_WORKER:
    189        case TARGET_TYPES.SERVICE_WORKER:
    190          workerTargets.add(target);
    191          break;
    192        default:
    193          console.warn(
    194            "Unsupported target type in the evalutiong context selector",
    195            target.targetType
    196          );
    197      }
    198    }
    199 
    200    const items = [];
    201 
    202    const renderFrameWithContentScripts = frameTarget => {
    203      items.push(this.renderMenuItem(frameTarget));
    204 
    205      // Render under each frame, its related web extension content scripts,...
    206      for (const contentScriptTarget of contentScriptTargets) {
    207        if (contentScriptTarget.innerWindowId != frameTarget.innerWindowId) {
    208          continue;
    209        }
    210        items.push(this.renderMenuItem(contentScriptTarget, true));
    211        contentScriptTargets.delete(contentScriptTarget);
    212      }
    213 
    214      // ...as well as all its related workers
    215      for (const workerTarget of workerTargets) {
    216        if (
    217          workerTarget.relatedDocumentInnerWindowId != frameTarget.innerWindowId
    218        ) {
    219          continue;
    220        }
    221        items.push(this.renderMenuItem(workerTarget, true));
    222        workerTargets.delete(workerTarget);
    223      }
    224    };
    225 
    226    // Note that while debugging popups, we might have a small period
    227    // of time where we don't have any top level target when we reload
    228    // the original tab
    229    if (topTarget) {
    230      renderFrameWithContentScripts(topTarget);
    231    }
    232 
    233    if (handleProcessTargets) {
    234      const sortedProcessTargets = processTargets.sort(
    235        (a, b) => a.processID < b.processID
    236      );
    237      for (const target of sortedProcessTargets) {
    238        items.push(
    239          dom.hr({
    240            role: "menuseparator",
    241            key: `process-separator-${target.actorID}`,
    242          }),
    243          this.renderMenuItem(target)
    244        );
    245 
    246        for (const frameTarget of frameTargets) {
    247          if (frameTarget.processID != target.processID) {
    248            continue;
    249          }
    250          renderFrameWithContentScripts(frameTarget);
    251          frameTargets.delete(frameTarget);
    252        }
    253      }
    254    }
    255 
    256    // Render all targets when running in regular non-browser-console/toolbox,
    257    // but also possibly render any leftover frame which can't be matched to any Process ID.
    258    const sortedFrames = [...frameTargets].sort(
    259      (a, b) => a.innerWindowID < b.innerWindowID
    260    );
    261    if (sortedFrames.length) {
    262      items.push(dom.hr({ role: "menuseparator", key: `frame-separator` }));
    263    }
    264    for (const frameTarget of sortedFrames) {
    265      renderFrameWithContentScripts(frameTarget);
    266    }
    267 
    268    // All content scripts and workers should have matched their related frame target in `renderFrameWithContentScripts`,
    269    // but just in case, display any leftover.
    270    for (const contentScriptTarget of contentScriptTargets) {
    271      items.push(this.renderMenuItem(contentScriptTarget));
    272    }
    273    const sortedWorkers = [...workerTargets].sort((a, b) => a.url < b.url);
    274    if (sortedWorkers.length) {
    275      items.push(dom.hr({ role: "menuseparator", key: `worker-separator` }));
    276    }
    277    for (const workerTarget of sortedWorkers) {
    278      items.push(this.renderMenuItem(workerTarget));
    279    }
    280 
    281    return MenuList(
    282      { id: "webconsole-console-evaluation-context-selector-menu-list" },
    283      items
    284    );
    285  }
    286 
    287  getLabel() {
    288    const { selectedTarget } = this.props;
    289 
    290    // When debugging a Web Extension, the top level target is always the fallback document.
    291    // It isn't really a top level document as it won't be the parent of any other.
    292    // So only print its name.
    293    if (
    294      !selectedTarget ||
    295      (selectedTarget.isTopLevel &&
    296        !selectedTarget.commands.descriptorFront.isWebExtension)
    297    ) {
    298      return l10n.getStr("webconsole.input.selector.top");
    299    }
    300 
    301    return selectedTarget.name;
    302  }
    303 
    304  render() {
    305    const { webConsoleUI, targets, selectedTarget } = this.props;
    306 
    307    // Don't render if there's only one target.
    308    // Also bail out if the console is being destroyed (where WebConsoleUI.wrapper gets
    309    // nullified).
    310    if (targets.length <= 1 || !webConsoleUI.wrapper) {
    311      return null;
    312    }
    313 
    314    const doc = webConsoleUI.document;
    315    const { toolbox } = webConsoleUI.wrapper;
    316 
    317    return MenuButton(
    318      {
    319        menuId: "webconsole-input-evaluationsButton",
    320        toolboxDoc: toolbox ? toolbox.doc : doc,
    321        label: this.getLabel(),
    322        className:
    323          "webconsole-evaluation-selector-button devtools-button devtools-dropdown-button" +
    324          (selectedTarget && !selectedTarget.isTopLevel ? " checked" : ""),
    325        title: l10n.getStr("webconsole.input.selector.tooltip"),
    326      },
    327      // We pass the children in a function so we don't require the MenuItem and MenuList
    328      // components until we need to display them (i.e. when the button is clicked).
    329      () => this.renderMenuItems()
    330    );
    331  }
    332 }
    333 
    334 const toolboxConnected = connect(
    335  state => ({
    336    targets: targetSelectors.getToolboxTargets(state),
    337    selectedTarget: targetSelectors.getSelectedTarget(state),
    338    lastTargetRefresh: targetSelectors.getLastTargetRefresh(state),
    339  }),
    340  dispatch => ({
    341    selectTarget: actorID => dispatch(targetActions.selectTarget(actorID)),
    342  }),
    343  undefined,
    344  { storeKey: "target-store" }
    345 )(EvaluationContextSelector);
    346 
    347 module.exports = connect(
    348  state => state,
    349  dispatch => ({
    350    onContextChange: () => {
    351      dispatch(
    352        webconsoleActions.updateInstantEvaluationResultForCurrentExpression()
    353      );
    354      dispatch(webconsoleActions.autocompleteClear());
    355    },
    356  })
    357 )(toolboxConnected);