tor-browser

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

SearchPanel.js (12906B)


      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  createRef,
     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 const { div, span } = dom;
     14 const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js");
     15 const {
     16  PANELS,
     17 } = require("resource://devtools/client/netmonitor/src/constants.js");
     18 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     19 const {
     20  connect,
     21 } = require("resource://devtools/client/shared/vendor/react-redux.js");
     22 const TreeViewClass = ChromeUtils.importESModule(
     23  "resource://devtools/client/shared/components/tree/TreeView.mjs"
     24 ).default;
     25 const TreeView = createFactory(TreeViewClass);
     26 const LabelCell = createFactory(
     27  ChromeUtils.importESModule(
     28    "resource://devtools/client/shared/components/tree/LabelCell.mjs"
     29  ).default
     30 );
     31 const {
     32  SearchProvider,
     33 } = require("resource://devtools/client/netmonitor/src/components/search/search-provider.js");
     34 const Toolbar = createFactory(
     35  require("resource://devtools/client/netmonitor/src/components/search/Toolbar.js")
     36 );
     37 const StatusBar = createFactory(
     38  require("resource://devtools/client/netmonitor/src/components/search/StatusBar.js")
     39 );
     40 const {
     41  limitTooltipLength,
     42 } = require("resource://devtools/client/netmonitor/src/utils/tooltips.js");
     43 const {
     44  L10N,
     45 } = require("resource://devtools/client/netmonitor/src/utils/l10n.js");
     46 loader.lazyRequireGetter(
     47  this,
     48  "showMenu",
     49  "resource://devtools/client/shared/components/menu/utils.js",
     50  true
     51 );
     52 
     53 const PropertiesViewContextMenu = require("resource://devtools/client/netmonitor/src/widgets/PropertiesViewContextMenu.js");
     54 const RequestListContextMenu = require("resource://devtools/client/netmonitor/src/widgets/RequestListContextMenu.js");
     55 
     56 // There are two levels in the search panel tree hierarchy:
     57 // 0: Resource - represents the source request object
     58 // 1: Search Result - represents a match coming from the parent resource
     59 const RESOURCE_LEVEL = 0;
     60 const SEARCH_RESULT_LEVEL = 1;
     61 
     62 /**
     63 * This component is responsible for rendering all search results
     64 * coming from the current search.
     65 */
     66 class SearchPanel extends Component {
     67  static get propTypes() {
     68    return {
     69      clearSearchResults: PropTypes.func.isRequired,
     70      openSearch: PropTypes.func.isRequired,
     71      closeSearch: PropTypes.func.isRequired,
     72      search: PropTypes.func.isRequired,
     73      caseSensitive: PropTypes.bool,
     74      connector: PropTypes.object.isRequired,
     75      addSearchQuery: PropTypes.func.isRequired,
     76      query: PropTypes.string.isRequired,
     77      results: PropTypes.array,
     78      navigate: PropTypes.func.isRequired,
     79      isDisplaying: PropTypes.bool.isRequired,
     80      blockedUrls: PropTypes.array.isRequired,
     81      requests: PropTypes.array.isRequired,
     82      cloneRequest: PropTypes.func.isRequired,
     83      openDetailsPanelTab: PropTypes.func.isRequired,
     84      openHTTPCustomRequestTab: PropTypes.func.isRequired,
     85      closeHTTPCustomRequestTab: PropTypes.func.isRequired,
     86      sendCustomRequest: PropTypes.func.isRequired,
     87      sendHTTPCustomRequest: PropTypes.func.isRequired,
     88      openStatistics: PropTypes.func.isRequired,
     89      openRequestBlockingAndAddUrl: PropTypes.func.isRequired,
     90      openRequestBlockingAndDisableUrls: PropTypes.func.isRequired,
     91      removeBlockedUrl: PropTypes.func.isRequired,
     92    };
     93  }
     94 
     95  constructor(props) {
     96    super(props);
     97 
     98    this.searchboxRef = createRef();
     99    this.renderValue = this.renderValue.bind(this);
    100    this.renderLabel = this.renderLabel.bind(this);
    101    this.onClickTreeRow = this.onClickTreeRow.bind(this);
    102    this.onContextMenuTreeRow = this.onContextMenuTreeRow.bind(this);
    103    this.provider = SearchProvider;
    104    this.expandedNodes = new Set();
    105  }
    106 
    107  componentDidMount() {
    108    if (this.searchboxRef) {
    109      this.searchboxRef.current.focus();
    110    }
    111  }
    112 
    113  componentDidUpdate(prevProps) {
    114    if (this.props.isDisplaying && !prevProps.isDisplaying) {
    115      this.searchboxRef.current.focus();
    116    }
    117  }
    118 
    119  onClickTreeRow(path, event, member) {
    120    if (member.object.parentResource) {
    121      this.props.navigate(member.object);
    122    }
    123  }
    124 
    125  /**
    126   * Custom TreeView label rendering. The search result
    127   * value isn't rendered in separate column, but in the
    128   * same column as the label (to save space).
    129   */
    130  renderLabel(props) {
    131    const { member } = props;
    132    const level = member.level || 0;
    133    const className = level == RESOURCE_LEVEL ? "resourceCell" : "resultCell";
    134 
    135    // Customize label rendering by adding a suffix/value
    136    const renderSuffix = () => {
    137      return dom.span(
    138        {
    139          className,
    140        },
    141        " ",
    142        this.renderValue(props)
    143      );
    144    };
    145 
    146    return LabelCell({
    147      ...props,
    148      title:
    149        member.level == 1
    150          ? limitTooltipLength(member.object.value)
    151          : this.provider.getResourceTooltipLabel(member.object),
    152      renderSuffix,
    153    });
    154  }
    155 
    156  onContextMenuTreeRow(member, evt) {
    157    evt.preventDefault();
    158 
    159    // if selected item is associated to a request --> show suitable contextmenu
    160    const request = member?.object?.resource;
    161    if (typeof request === "object") {
    162      // test if request is still available:
    163      const requestId = request.id;
    164      const storedRequest = this.props.requests.find(
    165        currentStoredRequest => requestId === currentStoredRequest?.id
    166      );
    167 
    168      if (typeof storedRequest === "object") {
    169        // request is in cache --> open full context menu:
    170        this.openRequestListContextMenu(evt, request);
    171      } else {
    172        // request is not in the cache anymore --> open context menu with note about it:
    173        const menuItems = [
    174          {
    175            id: "simple-view-context-menu-request-not-available-anymore",
    176            label: L10N.getStr("netmonitor.context.hintRequestNotAvailable"),
    177            disabled: true,
    178          },
    179        ];
    180 
    181        showMenu(menuItems, {
    182          screenX: evt.screenX,
    183          screenY: evt.screenY,
    184        });
    185      }
    186    } else {
    187      // for other content -> open simple context menu with copy only
    188      if (!this.contextMenuSimple) {
    189        this.contextMenuSimple = new PropertiesViewContextMenu();
    190      }
    191      this.contextMenuSimple.open(evt, window.getSelection(), {
    192        member,
    193      });
    194    }
    195  }
    196 
    197  openRequestListContextMenu(evt, request) {
    198    // reuse context menu of request list:
    199    if (!this.contextMenuRequest) {
    200      const {
    201        connector,
    202        cloneRequest,
    203        openDetailsPanelTab,
    204        openHTTPCustomRequestTab,
    205        closeHTTPCustomRequestTab,
    206        sendCustomRequest,
    207        sendHTTPCustomRequest,
    208        openStatistics,
    209        openRequestBlockingAndAddUrl,
    210        openRequestBlockingAndDisableUrls,
    211        removeBlockedUrl,
    212      } = this.props;
    213      this.contextMenuRequest = new RequestListContextMenu({
    214        connector,
    215        cloneRequest,
    216        openDetailsPanelTab,
    217        openHTTPCustomRequestTab,
    218        closeHTTPCustomRequestTab,
    219        sendCustomRequest,
    220        sendHTTPCustomRequest,
    221        openStatistics,
    222        openRequestBlockingAndAddUrl,
    223        openRequestBlockingAndDisableUrls,
    224        removeBlockedUrl,
    225      });
    226    }
    227 
    228    const { blockedUrls, results } = this.props;
    229    const allRequestsInResults = results.map(r => r.resource);
    230 
    231    this.contextMenuRequest.open(
    232      evt,
    233      request,
    234      allRequestsInResults,
    235      blockedUrls
    236    );
    237  }
    238 
    239  renderTree() {
    240    const { results } = this.props;
    241    return TreeView({
    242      object: results,
    243      provider: this.provider,
    244      expandableStrings: false,
    245      // Ensure passing a stable expanded Set,
    246      // so that TreeView doesn't reset to default prop's Set
    247      // on each new received props.
    248      expandedNodes: this.expandedNodes,
    249      renderLabelCell: this.renderLabel,
    250      onContextMenuRow: this.onContextMenuTreeRow,
    251      columns: [],
    252      onClickRow: this.onClickTreeRow,
    253    });
    254  }
    255 
    256  /**
    257   * Custom tree value rendering. This method is responsible for
    258   * rendering highlighted query string within the search result
    259   * result tree.
    260   */
    261  renderValue(props) {
    262    const { member } = props;
    263    const { query, caseSensitive } = this.props;
    264 
    265    // Handle only second level (zero based) that displays
    266    // the search result. Find the query string inside the
    267    // search result value (`props.object`) and render it
    268    // within a span element with proper class name.
    269    // level 0 = resource name
    270    if (member.level === SEARCH_RESULT_LEVEL) {
    271      const { object } = member;
    272 
    273      // Handles multiple matches in a string
    274      if (object.startIndex && object.startIndex.length > 1) {
    275        let indexStart = 0;
    276        const allMatches = object.startIndex.map((match, index) => {
    277          if (index === 0) {
    278            indexStart = match - 50;
    279          }
    280 
    281          const highlightedMatch = [
    282            span(
    283              { key: "match-" + match },
    284              object.value.substring(indexStart, match)
    285            ),
    286            span(
    287              {
    288                className: "query-match",
    289                key: "match-" + match + "-highlight",
    290              },
    291              object.value.substring(match, match + query.length)
    292            ),
    293          ];
    294 
    295          indexStart = match + query.length;
    296 
    297          return highlightedMatch;
    298        });
    299 
    300        return span(
    301          {
    302            title: limitTooltipLength(object.value),
    303          },
    304          allMatches
    305        );
    306      }
    307 
    308      const indexStart = caseSensitive
    309        ? object.value.indexOf(query)
    310        : object.value.toLowerCase().indexOf(query.toLowerCase());
    311      const indexEnd = indexStart + query.length;
    312 
    313      // Handles a match in a string
    314      if (indexStart >= 0) {
    315        return span(
    316          { title: limitTooltipLength(object.value) },
    317          span({}, object.value.substring(0, indexStart)),
    318          span(
    319            { className: "query-match" },
    320            object.value.substring(indexStart, indexStart + query.length)
    321          ),
    322          span({}, object.value.substring(indexEnd, object.value.length))
    323        );
    324      }
    325 
    326      // Default for key:value matches where query might not
    327      // be present in the value, but found in the key.
    328      return span(
    329        { title: limitTooltipLength(object.value) },
    330        span({}, object.value)
    331      );
    332    }
    333 
    334    return this.provider.getValue(member.object);
    335  }
    336 
    337  render() {
    338    const {
    339      openSearch,
    340      closeSearch,
    341      clearSearchResults,
    342      connector,
    343      addSearchQuery,
    344      search,
    345    } = this.props;
    346    return div(
    347      { className: "search-panel", style: { width: "100%" } },
    348      Toolbar({
    349        searchboxRef: this.searchboxRef,
    350        openSearch,
    351        closeSearch,
    352        clearSearchResults,
    353        addSearchQuery,
    354        search,
    355        connector,
    356      }),
    357      div(
    358        { className: "search-panel-content", style: { width: "100%" } },
    359        this.renderTree()
    360      ),
    361      StatusBar()
    362    );
    363  }
    364 }
    365 
    366 module.exports = connect(
    367  state => ({
    368    query: state.search.query,
    369    caseSensitive: state.search.caseSensitive,
    370    results: state.search.results,
    371    ongoingSearch: state.search.ongoingSearch,
    372    isDisplaying: state.ui.selectedActionBarTabId === PANELS.SEARCH,
    373    status: state.search.status,
    374    blockedUrls: state.requestBlocking.blockedUrls,
    375    requests: state.requests.requests,
    376  }),
    377  (dispatch, props) => ({
    378    closeSearch: () => dispatch(Actions.closeSearch()),
    379    openSearch: () => dispatch(Actions.openSearch()),
    380    search: () => dispatch(Actions.search()),
    381    clearSearchResults: () => dispatch(Actions.clearSearchResults()),
    382    addSearchQuery: query => dispatch(Actions.addSearchQuery(query)),
    383    navigate: searchResult => dispatch(Actions.navigate(searchResult)),
    384    cloneRequest: id => dispatch(Actions.cloneRequest(id)),
    385    openDetailsPanelTab: () => dispatch(Actions.openNetworkDetails(true)),
    386    openHTTPCustomRequestTab: () =>
    387      dispatch(Actions.openHTTPCustomRequest(true)),
    388    closeHTTPCustomRequestTab: () =>
    389      dispatch(Actions.openHTTPCustomRequest(false)),
    390    sendCustomRequest: () => dispatch(Actions.sendCustomRequest()),
    391    sendHTTPCustomRequest: request =>
    392      dispatch(Actions.sendHTTPCustomRequest(request)),
    393    openStatistics: open =>
    394      dispatch(Actions.openStatistics(props.connector, open)),
    395    openRequestBlockingAndAddUrl: url =>
    396      dispatch(Actions.openRequestBlockingAndAddUrl(url)),
    397    openRequestBlockingAndDisableUrls: url =>
    398      dispatch(Actions.openRequestBlockingAndDisableUrls(url)),
    399    removeBlockedUrl: url => dispatch(Actions.removeBlockedUrl(url)),
    400  })
    401 )(SearchPanel);