tor-browser

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

App.js (14354B)


      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 "use strict";
      5 
      6 const {
      7  Component,
      8  createFactory,
      9 } = require("resource://devtools/client/shared/vendor/react.mjs");
     10 loader.lazyRequireGetter(
     11  this,
     12  "PropTypes",
     13  "resource://devtools/client/shared/vendor/react-prop-types.js"
     14 );
     15 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     16 const {
     17  connect,
     18 } = require("resource://devtools/client/shared/vendor/react-redux.js");
     19 
     20 const actions = require("resource://devtools/client/webconsole/actions/index.js");
     21 const {
     22  FILTERBAR_DISPLAY_MODES,
     23 } = require("resource://devtools/client/webconsole/constants.js");
     24 
     25 // We directly require Components that we know are going to be used right away
     26 const ConsoleOutput = createFactory(
     27  require("resource://devtools/client/webconsole/components/Output/ConsoleOutput.js")
     28 );
     29 const FilterBar = createFactory(
     30  require("resource://devtools/client/webconsole/components/FilterBar/FilterBar.js")
     31 );
     32 const ReverseSearchInput = createFactory(
     33  require("resource://devtools/client/webconsole/components/Input/ReverseSearchInput.js")
     34 );
     35 const JSTerm = createFactory(
     36  require("resource://devtools/client/webconsole/components/Input/JSTerm.js")
     37 );
     38 const ConfirmDialog = createFactory(
     39  require("resource://devtools/client/webconsole/components/Input/ConfirmDialog.js")
     40 );
     41 const EagerEvaluation = createFactory(
     42  require("resource://devtools/client/webconsole/components/Input/EagerEvaluation.js")
     43 );
     44 const EvaluationNotification = createFactory(
     45  require("resource://devtools/client/webconsole/components/Input/EvaluationNotification.js")
     46 );
     47 
     48 // And lazy load the ones that may not be used.
     49 loader.lazyGetter(this, "SideBar", () =>
     50  createFactory(
     51    require("resource://devtools/client/webconsole/components/SideBar.js")
     52  )
     53 );
     54 
     55 loader.lazyGetter(this, "EditorToolbar", () =>
     56  createFactory(
     57    require("resource://devtools/client/webconsole/components/Input/EditorToolbar.js")
     58  )
     59 );
     60 
     61 loader.lazyGetter(this, "NotificationBox", () =>
     62  createFactory(
     63    require("resource://devtools/client/shared/components/NotificationBox.js")
     64      .NotificationBox
     65  )
     66 );
     67 loader.lazyRequireGetter(
     68  this,
     69  ["getNotificationWithValue", "PriorityLevels"],
     70  "resource://devtools/client/shared/components/NotificationBox.js",
     71  true
     72 );
     73 
     74 loader.lazyGetter(this, "GridElementWidthResizer", () =>
     75  createFactory(
     76    require("resource://devtools/client/shared/components/splitter/GridElementWidthResizer.js")
     77  )
     78 );
     79 
     80 loader.lazyGetter(this, "ChromeDebugToolbar", () =>
     81  createFactory(
     82    require("resource://devtools/client/framework/components/ChromeDebugToolbar.js")
     83  )
     84 );
     85 
     86 const l10n = require("resource://devtools/client/webconsole/utils/l10n.js");
     87 const {
     88  Utils: WebConsoleUtils,
     89 } = require("resource://devtools/client/webconsole/utils.js");
     90 
     91 const SELF_XSS_OK = l10n.getStr("selfxss.okstring");
     92 const SELF_XSS_MSG = l10n.getFormatStr("selfxss.msg", [SELF_XSS_OK]);
     93 
     94 const {
     95  getAllNotifications,
     96 } = require("resource://devtools/client/webconsole/selectors/notifications.js");
     97 const { div } = dom;
     98 const isMacOS = Services.appinfo.OS === "Darwin";
     99 
    100 /**
    101 * Console root Application component.
    102 */
    103 class App extends Component {
    104  static get propTypes() {
    105    return {
    106      dispatch: PropTypes.func.isRequired,
    107      webConsoleUI: PropTypes.object.isRequired,
    108      notifications: PropTypes.object,
    109      onFirstMeaningfulPaint: PropTypes.func.isRequired,
    110      serviceContainer: PropTypes.object.isRequired,
    111      closeSplitConsole: PropTypes.func.isRequired,
    112      autocomplete: PropTypes.bool,
    113      currentReverseSearchEntry: PropTypes.string,
    114      reverseSearchInputVisible: PropTypes.bool,
    115      reverseSearchInitialValue: PropTypes.string,
    116      editorMode: PropTypes.bool,
    117      editorWidth: PropTypes.number,
    118      inputEnabled: PropTypes.bool,
    119      sidebarVisible: PropTypes.bool.isRequired,
    120      eagerEvaluationEnabled: PropTypes.bool.isRequired,
    121      filterBarDisplayMode: PropTypes.oneOf([
    122        ...Object.values(FILTERBAR_DISPLAY_MODES),
    123      ]).isRequired,
    124    };
    125  }
    126 
    127  constructor(props) {
    128    super(props);
    129 
    130    this.onClick = this.onClick.bind(this);
    131    this.onPaste = this.onPaste.bind(this);
    132    this.onKeyDown = this.onKeyDown.bind(this);
    133    this.onBlur = this.onBlur.bind(this);
    134  }
    135 
    136  componentDidMount() {
    137    window.addEventListener("blur", this.onBlur, {
    138      signal: this.#abortController.signal,
    139    });
    140  }
    141 
    142  componentWillUnmount() {
    143    this.#abortController.abort();
    144  }
    145 
    146  #abortController = new AbortController();
    147 
    148  onBlur() {
    149    this.props.dispatch(actions.autocompleteClear());
    150  }
    151 
    152  onKeyDown(event) {
    153    const { dispatch, webConsoleUI } = this.props;
    154 
    155    if (
    156      (!isMacOS && event.key === "F9") ||
    157      (isMacOS && event.key === "r" && event.ctrlKey === true)
    158    ) {
    159      const initialValue =
    160        webConsoleUI.jsterm && webConsoleUI.jsterm.getSelectedText();
    161 
    162      dispatch(
    163        actions.reverseSearchInputToggle({ initialValue, access: "keyboard" })
    164      );
    165      event.stopPropagation();
    166      // Prevent Reader Mode to be enabled (See Bug 1682340)
    167      event.preventDefault();
    168    }
    169 
    170    if (
    171      event.key.toLowerCase() === "b" &&
    172      ((isMacOS && event.metaKey) || (!isMacOS && event.ctrlKey))
    173    ) {
    174      event.stopPropagation();
    175      event.preventDefault();
    176      dispatch(actions.editorToggle());
    177    }
    178  }
    179 
    180  onClick(event) {
    181    const target = event.originalTarget || event.target;
    182    const { reverseSearchInputVisible, dispatch, webConsoleUI } = this.props;
    183 
    184    if (
    185      reverseSearchInputVisible === true &&
    186      !target.closest(".reverse-search")
    187    ) {
    188      event.preventDefault();
    189      event.stopPropagation();
    190      dispatch(actions.reverseSearchInputToggle());
    191      return;
    192    }
    193 
    194    // Do not focus on middle/right-click or 2+ clicks.
    195    if (event.detail !== 1 || event.button !== 0) {
    196      return;
    197    }
    198 
    199    // Do not focus if a link was clicked
    200    if (target.closest("a")) {
    201      return;
    202    }
    203 
    204    // Do not focus if an input field was clicked
    205    if (target.closest("input")) {
    206      return;
    207    }
    208 
    209    // Do not focus if the click happened in the reverse search toolbar.
    210    if (target.closest(".reverse-search")) {
    211      return;
    212    }
    213 
    214    // Do not focus if something other than the output region was clicked
    215    // (including e.g. the clear messages button in toolbar)
    216    if (!target.closest(".webconsole-app")) {
    217      return;
    218    }
    219 
    220    // Do not focus if something is selected
    221    const selection = webConsoleUI.document.defaultView.getSelection();
    222    if (selection && !selection.isCollapsed) {
    223      return;
    224    }
    225 
    226    if (webConsoleUI?.jsterm) {
    227      webConsoleUI.jsterm.focus();
    228    }
    229  }
    230 
    231  onPaste(event) {
    232    const { dispatch, webConsoleUI, notifications } = this.props;
    233 
    234    const { usageCount, CONSOLE_ENTRY_THRESHOLD } = WebConsoleUtils;
    235 
    236    // Bail out if self-xss notification is suppressed.
    237    if (
    238      webConsoleUI.isBrowserConsole ||
    239      usageCount >= CONSOLE_ENTRY_THRESHOLD
    240    ) {
    241      return;
    242    }
    243 
    244    // Stop event propagation, so the clipboard content is *not* inserted.
    245    event.preventDefault();
    246    event.stopPropagation();
    247 
    248    // Bail out if self-xss notification is already there.
    249    if (getNotificationWithValue(notifications, "selfxss-notification")) {
    250      return;
    251    }
    252 
    253    const input = event.target;
    254 
    255    // Cleanup function if notification is closed by the user.
    256    const removeCallback = eventType => {
    257      if (eventType == "removed") {
    258        input.removeEventListener("keyup", pasteKeyUpHandler);
    259        dispatch(actions.removeNotification("selfxss-notification"));
    260      }
    261    };
    262 
    263    // Create self-xss notification
    264    dispatch(
    265      actions.appendNotification(
    266        SELF_XSS_MSG,
    267        "selfxss-notification",
    268        null,
    269        PriorityLevels.PRIORITY_WARNING_HIGH,
    270        null,
    271        removeCallback
    272      )
    273    );
    274 
    275    // Remove notification automatically when the user types "allow pasting".
    276    const pasteKeyUpHandler = e => {
    277      const { value } = e.target;
    278      if (value.includes(SELF_XSS_OK)) {
    279        dispatch(actions.removeNotification("selfxss-notification"));
    280        input.removeEventListener("keyup", pasteKeyUpHandler);
    281        WebConsoleUtils.usageCount = WebConsoleUtils.CONSOLE_ENTRY_THRESHOLD;
    282      }
    283    };
    284 
    285    input.addEventListener("keyup", pasteKeyUpHandler, {
    286      signal: this.#abortController.signal,
    287    });
    288  }
    289 
    290  renderChromeDebugToolbar() {
    291    const { webConsoleUI } = this.props;
    292    if (!webConsoleUI.isBrowserConsole) {
    293      return null;
    294    }
    295    return ChromeDebugToolbar({
    296      // This should always be true at this point
    297      isBrowserConsole: webConsoleUI.isBrowserConsole,
    298    });
    299  }
    300 
    301  renderFilterBar() {
    302    const { closeSplitConsole, filterBarDisplayMode, webConsoleUI } =
    303      this.props;
    304 
    305    return FilterBar({
    306      key: "filterbar",
    307      closeSplitConsole,
    308      displayMode: filterBarDisplayMode,
    309      webConsoleUI,
    310    });
    311  }
    312 
    313  renderEditorToolbar() {
    314    const {
    315      editorMode,
    316      dispatch,
    317      reverseSearchInputVisible,
    318      serviceContainer,
    319      webConsoleUI,
    320      inputEnabled,
    321    } = this.props;
    322 
    323    if (!inputEnabled) {
    324      return null;
    325    }
    326 
    327    return editorMode
    328      ? EditorToolbar({
    329          key: "editor-toolbar",
    330          editorMode,
    331          dispatch,
    332          reverseSearchInputVisible,
    333          serviceContainer,
    334          webConsoleUI,
    335        })
    336      : null;
    337  }
    338 
    339  renderConsoleOutput() {
    340    const { onFirstMeaningfulPaint, serviceContainer, editorMode } = this.props;
    341 
    342    return ConsoleOutput({
    343      key: "console-output",
    344      serviceContainer,
    345      onFirstMeaningfulPaint,
    346      editorMode,
    347    });
    348  }
    349 
    350  renderJsTerm() {
    351    const {
    352      webConsoleUI,
    353      serviceContainer,
    354      autocomplete,
    355      editorMode,
    356      editorWidth,
    357      inputEnabled,
    358    } = this.props;
    359 
    360    return JSTerm({
    361      key: "jsterm",
    362      webConsoleUI,
    363      serviceContainer,
    364      onPaste: this.onPaste,
    365      autocomplete,
    366      editorMode,
    367      editorWidth,
    368      inputEnabled,
    369    });
    370  }
    371 
    372  renderEagerEvaluation() {
    373    const { eagerEvaluationEnabled, serviceContainer, inputEnabled } =
    374      this.props;
    375 
    376    if (!eagerEvaluationEnabled || !inputEnabled) {
    377      return null;
    378    }
    379 
    380    return EagerEvaluation({ serviceContainer });
    381  }
    382 
    383  renderReverseSearch() {
    384    const { serviceContainer, reverseSearchInitialValue } = this.props;
    385 
    386    return ReverseSearchInput({
    387      key: "reverse-search-input",
    388      setInputValue: serviceContainer.setInputValue,
    389      focusInput: serviceContainer.focusInput,
    390      initialValue: reverseSearchInitialValue,
    391    });
    392  }
    393 
    394  renderSideBar() {
    395    const { serviceContainer, sidebarVisible } = this.props;
    396    return sidebarVisible
    397      ? SideBar({
    398          key: "sidebar",
    399          serviceContainer,
    400          visible: sidebarVisible,
    401        })
    402      : null;
    403  }
    404 
    405  renderNotificationBox() {
    406    const { notifications, editorMode } = this.props;
    407 
    408    return notifications && notifications.size > 0
    409      ? NotificationBox({
    410          id: "webconsole-notificationbox",
    411          key: "notification-box",
    412          displayBorderTop: !editorMode,
    413          displayBorderBottom: editorMode,
    414          wrapping: true,
    415          notifications,
    416        })
    417      : null;
    418  }
    419 
    420  renderConfirmDialog() {
    421    const { webConsoleUI, serviceContainer } = this.props;
    422 
    423    return ConfirmDialog({
    424      webConsoleUI,
    425      serviceContainer,
    426      key: "confirm-dialog",
    427    });
    428  }
    429 
    430  renderRootElement(children) {
    431    const { editorMode, sidebarVisible, inputEnabled, eagerEvaluationEnabled } =
    432      this.props;
    433 
    434    const classNames = ["webconsole-app"];
    435    if (sidebarVisible) {
    436      classNames.push("sidebar-visible");
    437    }
    438    if (editorMode && inputEnabled) {
    439      classNames.push("jsterm-editor");
    440    }
    441 
    442    if (eagerEvaluationEnabled && inputEnabled) {
    443      classNames.push("eager-evaluation");
    444    }
    445 
    446    return div(
    447      {
    448        className: classNames.join(" "),
    449        onKeyDown: this.onKeyDown,
    450        onClick: this.onClick,
    451        ref: node => {
    452          this.node = node;
    453        },
    454      },
    455      children
    456    );
    457  }
    458 
    459  render() {
    460    const { webConsoleUI, editorMode, dispatch, inputEnabled } = this.props;
    461 
    462    const chromeDebugToolbar = this.renderChromeDebugToolbar();
    463    const filterBar = this.renderFilterBar();
    464    const editorToolbar = this.renderEditorToolbar();
    465    const consoleOutput = this.renderConsoleOutput();
    466    const notificationBox = this.renderNotificationBox();
    467    const jsterm = this.renderJsTerm();
    468    const eager = this.renderEagerEvaluation();
    469    const evaluationNotification = EvaluationNotification();
    470    const reverseSearch = this.renderReverseSearch();
    471    const sidebar = this.renderSideBar();
    472    const confirmDialog = this.renderConfirmDialog();
    473 
    474    return this.renderRootElement([
    475      chromeDebugToolbar,
    476      filterBar,
    477      editorToolbar,
    478      dom.div(
    479        { className: "flexible-output-input", key: "in-out-container" },
    480        consoleOutput,
    481        notificationBox,
    482        jsterm,
    483        eager,
    484        evaluationNotification
    485      ),
    486      editorMode && inputEnabled
    487        ? GridElementWidthResizer({
    488            key: "editor-resizer",
    489            enabled: editorMode,
    490            position: "end",
    491            className: "editor-resizer",
    492            getControlledElementNode: () => webConsoleUI.jsterm.node,
    493            onResizeEnd: width => dispatch(actions.setEditorWidth(width)),
    494          })
    495        : null,
    496      reverseSearch,
    497      sidebar,
    498      confirmDialog,
    499    ]);
    500  }
    501 }
    502 
    503 const mapStateToProps = state => ({
    504  notifications: getAllNotifications(state),
    505  reverseSearchInputVisible: state.ui.reverseSearchInputVisible,
    506  reverseSearchInitialValue: state.ui.reverseSearchInitialValue,
    507  editorMode: state.ui.editor,
    508  editorWidth: state.ui.editorWidth,
    509  sidebarVisible: state.ui.sidebarVisible,
    510  filterBarDisplayMode: state.ui.filterBarDisplayMode,
    511  eagerEvaluationEnabled: state.prefs.eagerEvaluation,
    512  autocomplete: state.prefs.autocomplete,
    513 });
    514 
    515 const mapDispatchToProps = dispatch => ({
    516  dispatch,
    517 });
    518 
    519 module.exports = connect(mapStateToProps, mapDispatchToProps)(App);