tor-browser

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

RequestListContent.js (17956B)


      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  Component,
      9  createFactory,
     10 } = require("resource://devtools/client/shared/vendor/react.mjs");
     11 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     12 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     13 const {
     14  connect,
     15 } = require("resource://devtools/client/shared/vendor/react-redux.js");
     16 const {
     17  HTMLTooltip,
     18 } = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
     19 
     20 const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js");
     21 const {
     22  formDataURI,
     23 } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
     24 const {
     25  getDisplayedRequests,
     26  getColumns,
     27  getSelectedRequest,
     28  getClickedRequest,
     29  getWaterfallScale,
     30  hasOverride,
     31 } = require("resource://devtools/client/netmonitor/src/selectors/index.js");
     32 
     33 loader.lazyRequireGetter(
     34  this,
     35  "openRequestInTab",
     36  "resource://devtools/client/netmonitor/src/utils/firefox/open-request-in-tab.js",
     37  true
     38 );
     39 loader.lazyGetter(this, "setImageTooltip", function () {
     40  return require("resource://devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js")
     41    .setImageTooltip;
     42 });
     43 loader.lazyGetter(this, "getImageDimensions", function () {
     44  return require("resource://devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js")
     45    .getImageDimensions;
     46 });
     47 
     48 // Components
     49 const RequestListHeader = createFactory(
     50  require("resource://devtools/client/netmonitor/src/components/request-list/RequestListHeader.js")
     51 );
     52 const RequestListItem = createFactory(
     53  require("resource://devtools/client/netmonitor/src/components/request-list/RequestListItem.js")
     54 );
     55 const RequestListContextMenu = require("resource://devtools/client/netmonitor/src/widgets/RequestListContextMenu.js");
     56 
     57 const { div } = dom;
     58 
     59 // Tooltip show / hide delay in ms
     60 const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500;
     61 // Tooltip image maximum dimension in px
     62 const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400;
     63 
     64 const LEFT_MOUSE_BUTTON = 0;
     65 const MIDDLE_MOUSE_BUTTON = 1;
     66 const RIGHT_MOUSE_BUTTON = 2;
     67 
     68 /**
     69 * Renders the actual contents of the request list.
     70 */
     71 class RequestListContentComponent extends Component {
     72  static get propTypes() {
     73    return {
     74      blockedUrls: PropTypes.array.isRequired,
     75      connector: PropTypes.object.isRequired,
     76      columns: PropTypes.object.isRequired,
     77      networkActionOpen: PropTypes.bool,
     78      networkDetailsOpen: PropTypes.bool.isRequired,
     79      networkDetailsWidth: PropTypes.number,
     80      networkDetailsHeight: PropTypes.number,
     81      waterfallScale: PropTypes.number,
     82      slowLimit: PropTypes.number,
     83      cloneRequest: PropTypes.func.isRequired,
     84      clickedRequest: PropTypes.object,
     85      openDetailsPanelTab: PropTypes.func.isRequired,
     86      openHTTPCustomRequestTab: PropTypes.func.isRequired,
     87      closeHTTPCustomRequestTab: PropTypes.func.isRequired,
     88      sendCustomRequest: PropTypes.func.isRequired,
     89      sendHTTPCustomRequest: PropTypes.func.isRequired,
     90      displayedRequests: PropTypes.array.isRequired,
     91      firstRequestStartedMs: PropTypes.number.isRequired,
     92      fromCache: PropTypes.bool,
     93      onInitiatorBadgeMouseDown: PropTypes.func.isRequired,
     94      onItemRightMouseButtonDown: PropTypes.func.isRequired,
     95      onItemMouseDown: PropTypes.func.isRequired,
     96      onSecurityIconMouseDown: PropTypes.func.isRequired,
     97      onSelectDelta: PropTypes.func.isRequired,
     98      onWaterfallMouseDown: PropTypes.func.isRequired,
     99      openStatistics: PropTypes.func.isRequired,
    100      openRequestBlockingAndAddUrl: PropTypes.func.isRequired,
    101      openRequestBlockingAndDisableUrls: PropTypes.func.isRequired,
    102      removeBlockedUrl: PropTypes.func.isRequired,
    103      selectedActionBarTabId: PropTypes.string,
    104      selectRequest: PropTypes.func.isRequired,
    105      selectedRequest: PropTypes.object,
    106      requestFilterTypes: PropTypes.object.isRequired,
    107    };
    108  }
    109 
    110  constructor(props) {
    111    super(props);
    112    this.onHover = this.onHover.bind(this);
    113    this.onScroll = this.onScroll.bind(this);
    114    this.onResize = this.onResize.bind(this);
    115    this.onKeyDown = this.onKeyDown.bind(this);
    116    this.openRequestInTab = this.openRequestInTab.bind(this);
    117    this.onDoubleClick = this.onDoubleClick.bind(this);
    118    this.onDragStart = this.onDragStart.bind(this);
    119    this.onContextMenu = this.onContextMenu.bind(this);
    120    this.onMouseDown = this.onMouseDown.bind(this);
    121    this.hasOverflow = false;
    122    this.onIntersect = this.onIntersect.bind(this);
    123    this.intersectionObserver = null;
    124    this.state = {
    125      onscreenItems: new Set(),
    126    };
    127  }
    128 
    129  // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
    130  UNSAFE_componentWillMount() {
    131    this.tooltip = new HTMLTooltip(window.parent.document, { type: "arrow" });
    132    window.addEventListener("resize", this.onResize);
    133  }
    134 
    135  componentDidMount() {
    136    // Install event handler for displaying a tooltip
    137    this.tooltip.startTogglingOnHover(this.refs.scrollEl, this.onHover, {
    138      toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
    139      interactive: true,
    140    });
    141    // Install event handler to hide the tooltip on scroll
    142    this.refs.scrollEl.addEventListener("scroll", this.onScroll, true);
    143    this.onResize();
    144    this.intersectionObserver = new IntersectionObserver(this.onIntersect, {
    145      root: this.refs.scrollEl,
    146      // Render 10% more columns for a scrolling headstart
    147      rootMargin: "10%",
    148    });
    149    // Prime IntersectionObserver with existing entries
    150    for (const item of this.refs.scrollEl.querySelectorAll(
    151      ".request-list-item"
    152    )) {
    153      this.intersectionObserver.observe(item);
    154    }
    155  }
    156 
    157  componentDidUpdate(prevProps) {
    158    const output = this.refs.scrollEl;
    159    if (!this.hasOverflow && output.scrollHeight > output.clientHeight) {
    160      output.scrollTop = output.scrollHeight;
    161      this.hasOverflow = true;
    162    }
    163    if (
    164      prevProps.networkDetailsOpen !== this.props.networkDetailsOpen ||
    165      prevProps.networkDetailsWidth !== this.props.networkDetailsWidth ||
    166      prevProps.networkDetailsHeight !== this.props.networkDetailsHeight
    167    ) {
    168      this.onResize();
    169    }
    170  }
    171 
    172  componentWillUnmount() {
    173    this.refs.scrollEl.removeEventListener("scroll", this.onScroll, true);
    174 
    175    // Uninstall the tooltip event handler
    176    this.tooltip.stopTogglingOnHover();
    177    window.removeEventListener("resize", this.onResize);
    178    if (this.intersectionObserver !== null) {
    179      this.intersectionObserver.disconnect();
    180      this.intersectionObserver = null;
    181    }
    182  }
    183 
    184  /*
    185   * Removing onResize() method causes perf regression - too many repaints of the panel.
    186   * So it is needed in ComponentDidMount and ComponentDidUpdate. See Bug 1532914.
    187   */
    188  onResize() {
    189    // Wait for the next animation frame to measure the parentNode dimensions.
    190    // Bug 1900682.
    191    requestAnimationFrame(() => {
    192      if (document.visibilityState == "visible") {
    193        const parent = this.refs.scrollEl.parentNode;
    194        this.refs.scrollEl.style.width = parent.offsetWidth + "px";
    195        this.refs.scrollEl.style.height = parent.offsetHeight + "px";
    196      }
    197    });
    198  }
    199 
    200  onIntersect(entries) {
    201    // Track when off screen elements moved on screen to ensure updates
    202    let onscreenDidChange = false;
    203    const onscreenItems = new Set(this.state.onscreenItems);
    204    for (const { target, isIntersecting } of entries) {
    205      const { id } = target.dataset;
    206      if (isIntersecting) {
    207        if (onscreenItems.add(id)) {
    208          onscreenDidChange = true;
    209        }
    210      } else {
    211        onscreenItems.delete(id);
    212      }
    213    }
    214    if (onscreenDidChange) {
    215      // Remove ids that are no longer displayed
    216      const itemIds = new Set(this.props.displayedRequests.map(({ id }) => id));
    217      for (const id of onscreenItems) {
    218        if (!itemIds.has(id)) {
    219          onscreenItems.delete(id);
    220        }
    221      }
    222      this.setState({ onscreenItems });
    223    }
    224  }
    225 
    226  /**
    227   * The predicate used when deciding whether a popup should be shown
    228   * over a request item or not.
    229   *
    230   * @param Node target
    231   *        The element node currently being hovered.
    232   * @param object tooltip
    233   *        The current tooltip instance.
    234   * @return {Promise}
    235   */
    236  async onHover(target, tooltip) {
    237    const itemEl = target.closest(".request-list-item");
    238    if (!itemEl) {
    239      return false;
    240    }
    241    const itemId = itemEl.dataset.id;
    242    if (!itemId) {
    243      return false;
    244    }
    245    const requestItem = this.props.displayedRequests.find(r => r.id == itemId);
    246    if (!requestItem) {
    247      return false;
    248    }
    249 
    250    if (!target.closest(".requests-list-file")) {
    251      return false;
    252    }
    253 
    254    const { mimeType } = requestItem;
    255    if (!mimeType || !mimeType.includes("image/")) {
    256      return false;
    257    }
    258 
    259    const responseContent = await this.props.connector.requestData(
    260      requestItem.id,
    261      "responseContent"
    262    );
    263    const { encoding, text } = responseContent.content;
    264    const src = formDataURI(mimeType, encoding, text);
    265    const maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM;
    266    const { naturalWidth, naturalHeight } = await getImageDimensions(
    267      tooltip.doc,
    268      src
    269    );
    270    const options = { maxDim, naturalWidth, naturalHeight };
    271    setImageTooltip(tooltip, tooltip.doc, src, options);
    272 
    273    return itemEl.querySelector(".requests-list-file");
    274  }
    275 
    276  /**
    277   * Scroll listener for the requests menu view.
    278   */
    279  onScroll() {
    280    this.tooltip.hide();
    281  }
    282 
    283  onMouseDown(evt, id, request) {
    284    if (evt.button === LEFT_MOUSE_BUTTON) {
    285      this.props.selectRequest(id, request);
    286    } else if (evt.button === RIGHT_MOUSE_BUTTON) {
    287      this.props.onItemRightMouseButtonDown(id);
    288    } else if (evt.button === MIDDLE_MOUSE_BUTTON) {
    289      this.onMiddleMouseButtonDown(request);
    290    }
    291  }
    292 
    293  /**
    294   * Handler for keyboard events. For arrow up/down, page up/down, home/end,
    295   * move the selection up or down.
    296   */
    297  onKeyDown(evt) {
    298    let delta;
    299 
    300    switch (evt.key) {
    301      case "ArrowUp":
    302        delta = -1;
    303        break;
    304      case "ArrowDown":
    305        delta = +1;
    306        break;
    307      case "PageUp":
    308        delta = "PAGE_UP";
    309        break;
    310      case "PageDown":
    311        delta = "PAGE_DOWN";
    312        break;
    313      case "Home":
    314        delta = -Infinity;
    315        break;
    316      case "End":
    317        delta = +Infinity;
    318        break;
    319    }
    320 
    321    if (delta) {
    322      // Prevent scrolling when pressing navigation keys.
    323      evt.preventDefault();
    324      evt.stopPropagation();
    325      this.props.onSelectDelta(delta);
    326    }
    327  }
    328 
    329  /**
    330   * Opens selected item in a new tab.
    331   */
    332  async openRequestInTab(id, url, requestHeaders, requestPostData) {
    333    requestHeaders =
    334      requestHeaders ||
    335      (await this.props.connector.requestData(id, "requestHeaders"));
    336 
    337    requestPostData =
    338      requestPostData ||
    339      (await this.props.connector.requestData(id, "requestPostData"));
    340 
    341    openRequestInTab(url, requestHeaders, requestPostData);
    342  }
    343 
    344  onDoubleClick({ id, url, requestHeaders, requestPostData }) {
    345    this.openRequestInTab(id, url, requestHeaders, requestPostData);
    346  }
    347 
    348  onMiddleMouseButtonDown({ id, url, requestHeaders, requestPostData }) {
    349    this.openRequestInTab(id, url, requestHeaders, requestPostData);
    350  }
    351 
    352  onDragStart(evt, { url }) {
    353    evt.dataTransfer.setData("text/plain", url);
    354  }
    355 
    356  onContextMenu(evt) {
    357    evt.preventDefault();
    358    const { clickedRequest, displayedRequests, blockedUrls } = this.props;
    359 
    360    if (!this.contextMenu) {
    361      const {
    362        connector,
    363        cloneRequest,
    364        openDetailsPanelTab,
    365        openHTTPCustomRequestTab,
    366        closeHTTPCustomRequestTab,
    367        sendCustomRequest,
    368        sendHTTPCustomRequest,
    369        openStatistics,
    370        openRequestBlockingAndAddUrl,
    371        openRequestBlockingAndDisableUrls,
    372        removeBlockedUrl,
    373      } = this.props;
    374      this.contextMenu = new RequestListContextMenu({
    375        connector,
    376        cloneRequest,
    377        openDetailsPanelTab,
    378        openHTTPCustomRequestTab,
    379        closeHTTPCustomRequestTab,
    380        sendCustomRequest,
    381        sendHTTPCustomRequest,
    382        openStatistics,
    383        openRequestBlockingAndAddUrl,
    384        openRequestBlockingAndDisableUrls,
    385        removeBlockedUrl,
    386      });
    387    }
    388 
    389    this.contextMenu.open(evt, clickedRequest, displayedRequests, blockedUrls);
    390  }
    391 
    392  render() {
    393    const {
    394      connector,
    395      columns,
    396      displayedRequests,
    397      firstRequestStartedMs,
    398      onInitiatorBadgeMouseDown,
    399      onSecurityIconMouseDown,
    400      onWaterfallMouseDown,
    401      requestFilterTypes,
    402      selectedRequest,
    403      selectedActionBarTabId,
    404      openRequestBlockingAndAddUrl,
    405      openRequestBlockingAndDisableUrls,
    406      networkActionOpen,
    407      networkDetailsOpen,
    408      slowLimit,
    409      waterfallScale,
    410    } = this.props;
    411 
    412    return div(
    413      {
    414        ref: "scrollEl",
    415        className: "requests-list-scroll",
    416      },
    417      [
    418        dom.table(
    419          {
    420            className: "requests-list-table",
    421            key: "table",
    422          },
    423          RequestListHeader(),
    424          dom.tbody(
    425            {
    426              ref: "rowGroupEl",
    427              className: "requests-list-row-group",
    428              tabIndex: 0,
    429              onKeyDown: this.onKeyDown,
    430            },
    431            displayedRequests.map((item, index) => {
    432              return RequestListItem({
    433                blocked: !!item.blockedReason,
    434                firstRequestStartedMs,
    435                fromCache: item.status === "304" || item.fromCache,
    436                networkDetailsOpen,
    437                networkActionOpen,
    438                selectedActionBarTabId,
    439                connector,
    440                columns,
    441                item,
    442                index,
    443                isSelected: item.id === selectedRequest?.id,
    444                isVisible: this.state.onscreenItems.has(item.id),
    445                key: item.id,
    446                intersectionObserver: this.intersectionObserver,
    447                onContextMenu: this.onContextMenu,
    448                onDoubleClick: () => this.onDoubleClick(item),
    449                onDragStart: evt => this.onDragStart(evt, item),
    450                onMouseDown: evt => this.onMouseDown(evt, item.id, item),
    451                onInitiatorBadgeMouseDown: () =>
    452                  onInitiatorBadgeMouseDown(item.cause),
    453                onSecurityIconMouseDown: () =>
    454                  onSecurityIconMouseDown(item.securityState),
    455                onWaterfallMouseDown,
    456                requestFilterTypes,
    457                openRequestBlockingAndAddUrl,
    458                openRequestBlockingAndDisableUrls,
    459                slowLimit,
    460                waterfallScale,
    461              });
    462            })
    463          )
    464        ), // end of requests-list-row-group">
    465        dom.div({
    466          className: "requests-list-anchor",
    467          key: "anchor",
    468        }),
    469      ]
    470    );
    471  }
    472 }
    473 
    474 const RequestListContent = connect(
    475  (state, props) => ({
    476    blockedUrls: state.requestBlocking.blockedUrls,
    477    columns: getColumns(state, props.hasOverride),
    478    networkActionOpen: state.ui.networkActionOpen,
    479    networkDetailsOpen: state.ui.networkDetailsOpen,
    480    networkDetailsWidth: state.ui.networkDetailsWidth,
    481    networkDetailsHeight: state.ui.networkDetailsHeight,
    482    waterfallScale: getWaterfallScale(state),
    483    slowLimit: state.ui.slowLimit,
    484    clickedRequest: getClickedRequest(state),
    485    displayedRequests: getDisplayedRequests(state),
    486    firstRequestStartedMs: state.requests.firstStartedMs,
    487    selectedActionBarTabId: state.ui.selectedActionBarTabId,
    488    selectedRequest: getSelectedRequest(state),
    489    requestFilterTypes: state.filters.requestFilterTypes,
    490  }),
    491  (dispatch, props) => ({
    492    cloneRequest: id => dispatch(Actions.cloneRequest(id)),
    493    openDetailsPanelTab: () => dispatch(Actions.openNetworkDetails(true)),
    494    openHTTPCustomRequestTab: () =>
    495      dispatch(Actions.openHTTPCustomRequest(true)),
    496    closeHTTPCustomRequestTab: () =>
    497      dispatch(Actions.openHTTPCustomRequest(false)),
    498    sendCustomRequest: () => dispatch(Actions.sendCustomRequest()),
    499    sendHTTPCustomRequest: request =>
    500      dispatch(Actions.sendHTTPCustomRequest(request)),
    501    openStatistics: open =>
    502      dispatch(Actions.openStatistics(props.connector, open)),
    503    openRequestBlockingAndAddUrl: url =>
    504      dispatch(Actions.openRequestBlockingAndAddUrl(url)),
    505    removeBlockedUrl: url => dispatch(Actions.removeBlockedUrl(url)),
    506    openRequestBlockingAndDisableUrls: url =>
    507      dispatch(Actions.openRequestBlockingAndDisableUrls(url)),
    508    /**
    509     * A handler that opens the stack trace tab when a stack trace is available
    510     */
    511    onInitiatorBadgeMouseDown: cause => {
    512      if (cause.lastFrame) {
    513        dispatch(Actions.selectDetailsPanelTab("stack-trace"));
    514      }
    515    },
    516    selectRequest: (id, request) =>
    517      dispatch(Actions.selectRequest(id, request)),
    518    onItemRightMouseButtonDown: id => dispatch(Actions.rightClickRequest(id)),
    519    onItemMouseDown: id => dispatch(Actions.selectRequest(id)),
    520    /**
    521     * A handler that opens the security tab in the details view if secure or
    522     * broken security indicator is clicked.
    523     */
    524    onSecurityIconMouseDown: securityState => {
    525      if (securityState && securityState !== "insecure") {
    526        dispatch(Actions.selectDetailsPanelTab("security"));
    527      }
    528    },
    529    onSelectDelta: delta => dispatch(Actions.selectDelta(delta)),
    530    /**
    531     * A handler that opens the timing sidebar panel if the waterfall is clicked.
    532     */
    533    onWaterfallMouseDown: () => {
    534      dispatch(Actions.selectDetailsPanelTab("timings"));
    535    },
    536  })
    537 )(RequestListContentComponent);
    538 
    539 module.exports = connect(
    540  state => {
    541    return {
    542      hasOverride: hasOverride(state),
    543    };
    544  },
    545  {},
    546  undefined,
    547  { storeKey: "toolbox-store" }
    548 )(RequestListContent);