tor-browser

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

Message.js (13846B)


      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  createElement,
     12 } = require("resource://devtools/client/shared/vendor/react.mjs");
     13 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     14 const {
     15  l10n,
     16 } = require("resource://devtools/client/webconsole/utils/messages.js");
     17 const actions = require("resource://devtools/client/webconsole/actions/index.js");
     18 const {
     19  MESSAGE_LEVEL,
     20  MESSAGE_SOURCE,
     21  MESSAGE_TYPE,
     22 } = require("resource://devtools/client/webconsole/constants.js");
     23 const {
     24  MessageIndent,
     25 } = require("resource://devtools/client/webconsole/components/Output/MessageIndent.js");
     26 const MessageIcon = require("resource://devtools/client/webconsole/components/Output/MessageIcon.js");
     27 const FrameView = createFactory(
     28  require("resource://devtools/client/shared/components/Frame.js")
     29 );
     30 
     31 loader.lazyRequireGetter(
     32  this,
     33  "CollapseButton",
     34  "resource://devtools/client/webconsole/components/Output/CollapseButton.js"
     35 );
     36 loader.lazyRequireGetter(
     37  this,
     38  "MessageRepeat",
     39  "resource://devtools/client/webconsole/components/Output/MessageRepeat.js"
     40 );
     41 loader.lazyRequireGetter(
     42  this,
     43  "PropTypes",
     44  "resource://devtools/client/shared/vendor/react-prop-types.js"
     45 );
     46 loader.lazyRequireGetter(
     47  this,
     48  "SmartTrace",
     49  "resource://devtools/client/shared/components/SmartTrace.js"
     50 );
     51 
     52 class Message extends Component {
     53  static get propTypes() {
     54    return {
     55      open: PropTypes.bool,
     56      collapsible: PropTypes.bool,
     57      collapseTitle: PropTypes.string,
     58      disabled: PropTypes.bool,
     59      onToggle: PropTypes.func,
     60      source: PropTypes.string.isRequired,
     61      type: PropTypes.string.isRequired,
     62      level: PropTypes.string.isRequired,
     63      indent: PropTypes.number.isRequired,
     64      inWarningGroup: PropTypes.bool,
     65      isBlockedNetworkMessage: PropTypes.bool,
     66      topLevelClasses: PropTypes.array.isRequired,
     67      messageBody: PropTypes.any.isRequired,
     68      repeat: PropTypes.any,
     69      frame: PropTypes.any,
     70      attachment: PropTypes.any,
     71      stacktrace: PropTypes.any,
     72      messageId: PropTypes.string,
     73      scrollToMessage: PropTypes.bool,
     74      exceptionDocURL: PropTypes.string,
     75      request: PropTypes.object,
     76      dispatch: PropTypes.func,
     77      timeStamp: PropTypes.number,
     78      timestampsVisible: PropTypes.bool.isRequired,
     79      serviceContainer: PropTypes.shape({
     80        emitForTests: PropTypes.func.isRequired,
     81        onViewSource: PropTypes.func.isRequired,
     82        onViewSourceInDebugger: PropTypes.func,
     83        onViewSourceInStyleEditor: PropTypes.func,
     84        openContextMenu: PropTypes.func.isRequired,
     85        openLink: PropTypes.func.isRequired,
     86        sourceMapURLService: PropTypes.any,
     87        preventStacktraceInitialRenderDelay: PropTypes.bool,
     88      }),
     89      notes: PropTypes.arrayOf(
     90        PropTypes.shape({
     91          messageBody: PropTypes.string.isRequired,
     92          frame: PropTypes.any,
     93        })
     94      ),
     95      maybeScrollToBottom: PropTypes.func,
     96      message: PropTypes.object.isRequired,
     97    };
     98  }
     99 
    100  static get defaultProps() {
    101    return {
    102      indent: 0,
    103    };
    104  }
    105 
    106  constructor(props) {
    107    super(props);
    108    this.onLearnMoreClick = this.onLearnMoreClick.bind(this);
    109    this.toggleMessage = this.toggleMessage.bind(this);
    110    this.onContextMenu = this.onContextMenu.bind(this);
    111    this.renderIcon = this.renderIcon.bind(this);
    112  }
    113 
    114  componentDidMount() {
    115    if (this.messageNode) {
    116      if (this.props.scrollToMessage) {
    117        this.messageNode.scrollIntoView();
    118      }
    119 
    120      this.emitNewMessage(this.messageNode);
    121    }
    122  }
    123 
    124  componentDidCatch(e) {
    125    this.setState({ error: e });
    126  }
    127 
    128  // Event used in tests. Some message types don't pass it in because existing tests
    129  // did not emit for them.
    130  emitNewMessage(node) {
    131    const { serviceContainer, messageId, timeStamp } = this.props;
    132    serviceContainer.emitForTests(
    133      "new-messages",
    134      new Set([{ node, messageId, timeStamp }])
    135    );
    136  }
    137 
    138  onLearnMoreClick(e) {
    139    const { exceptionDocURL } = this.props;
    140    this.props.serviceContainer.openLink(exceptionDocURL, e);
    141    e.preventDefault();
    142  }
    143 
    144  toggleMessage(e) {
    145    // Don't bubble up to the main App component, which  redirects focus to input,
    146    // making difficult for screen reader users to review output
    147    e.stopPropagation();
    148    const { open, dispatch, messageId, onToggle, disabled } = this.props;
    149 
    150    if (disabled) {
    151      return;
    152    }
    153 
    154    // Early exit the function to avoid the message to collapse if the user is
    155    // selecting a range in the toggle message.
    156    const window = e.target.ownerDocument.defaultView;
    157    if (window.getSelection && window.getSelection().type === "Range") {
    158      return;
    159    }
    160 
    161    // If defined on props, we let the onToggle() method handle the toggling,
    162    // otherwise we toggle the message open/closed ourselves.
    163    if (onToggle) {
    164      onToggle(messageId, e);
    165    } else if (open) {
    166      dispatch(actions.messageClose(messageId));
    167    } else {
    168      dispatch(actions.messageOpen(messageId));
    169    }
    170  }
    171 
    172  onContextMenu(e) {
    173    const { serviceContainer, source, request, messageId } = this.props;
    174    const messageInfo = {
    175      source,
    176      request,
    177      messageId,
    178    };
    179    serviceContainer.openContextMenu(e, messageInfo);
    180    e.stopPropagation();
    181    e.preventDefault();
    182  }
    183 
    184  renderIcon() {
    185    const { level, inWarningGroup, isBlockedNetworkMessage, type, disabled } =
    186      this.props;
    187 
    188    if (inWarningGroup) {
    189      return undefined;
    190    }
    191 
    192    if (disabled) {
    193      return MessageIcon({
    194        level: MESSAGE_LEVEL.INFO,
    195        type,
    196        title: l10n.getStr("webconsole.disableIcon.title"),
    197      });
    198    }
    199 
    200    if (isBlockedNetworkMessage) {
    201      return MessageIcon({
    202        level: MESSAGE_LEVEL.ERROR,
    203        type: "blockedReason",
    204      });
    205    }
    206 
    207    return MessageIcon({
    208      level,
    209      type,
    210    });
    211  }
    212 
    213  renderTimestamp() {
    214    if (!this.props.timestampsVisible) {
    215      return null;
    216    }
    217 
    218    const timestamp = this.props.timeStamp || Date.now();
    219 
    220    return dom.span(
    221      {
    222        className: "timestamp devtools-monospace",
    223        title: l10n.dateString(timestamp),
    224      },
    225      l10n.timestampString(timestamp)
    226    );
    227  }
    228 
    229  renderErrorState() {
    230    const newBugUrl =
    231      "https://bugzilla.mozilla.org/enter_bug.cgi?product=DevTools&component=Console";
    232    const timestampEl = this.renderTimestamp();
    233 
    234    return dom.div(
    235      {
    236        className: "message error message-did-catch",
    237      },
    238      timestampEl,
    239      MessageIcon({ level: "error" }),
    240      dom.span(
    241        { className: "message-body-wrapper" },
    242        dom.span(
    243          {
    244            className: "message-flex-body",
    245          },
    246          // Add whitespaces for formatting when copying to the clipboard.
    247          timestampEl ? " " : null,
    248          dom.span(
    249            { className: "message-body devtools-monospace" },
    250            l10n.getFormatStr("webconsole.message.componentDidCatch.label", [
    251              newBugUrl,
    252            ]),
    253            dom.button(
    254              {
    255                className: "devtools-button",
    256                onClick: () =>
    257                  navigator.clipboard.writeText(
    258                    JSON.stringify(
    259                      this.props.message,
    260                      function (key, value) {
    261                        if (key === "targetFront") {
    262                          return null;
    263                        }
    264 
    265                        // The message can hold one or multiple fronts that we need to serialize
    266                        if (value?.getGrip) {
    267                          return value.getGrip();
    268                        }
    269                        return value;
    270                      },
    271                      2
    272                    )
    273                  ),
    274              },
    275              l10n.getStr(
    276                "webconsole.message.componentDidCatch.copyButton.label"
    277              )
    278            )
    279          )
    280        )
    281      ),
    282      dom.br()
    283    );
    284  }
    285 
    286  // eslint-disable-next-line complexity
    287  render() {
    288    if (this.state && this.state.error) {
    289      return this.renderErrorState();
    290    }
    291 
    292    const {
    293      open,
    294      collapsible,
    295      collapseTitle,
    296      disabled,
    297      source,
    298      type,
    299      level,
    300      indent,
    301      inWarningGroup,
    302      topLevelClasses,
    303      messageBody,
    304      frame,
    305      stacktrace,
    306      serviceContainer,
    307      exceptionDocURL,
    308      messageId,
    309      notes,
    310    } = this.props;
    311 
    312    topLevelClasses.push("message", source, type, level);
    313    if (open) {
    314      topLevelClasses.push("open");
    315    }
    316 
    317    if (disabled) {
    318      topLevelClasses.push("disabled");
    319    }
    320 
    321    const timestampEl = this.renderTimestamp();
    322    const icon = this.renderIcon();
    323 
    324    // Figure out if there is an expandable part to the message.
    325    let attachment = null;
    326    if (this.props.attachment) {
    327      attachment = this.props.attachment;
    328    } else if (stacktrace && open) {
    329      const smartTraceAttributes = {
    330        stacktrace,
    331        onViewSourceInDebugger:
    332          serviceContainer.onViewSourceInDebugger ||
    333          serviceContainer.onViewSource,
    334        onViewSource: serviceContainer.onViewSource,
    335        onReady: this.props.maybeScrollToBottom,
    336        sourceMapURLService: serviceContainer.sourceMapURLService,
    337      };
    338 
    339      if (serviceContainer.preventStacktraceInitialRenderDelay) {
    340        smartTraceAttributes.initialRenderDelay = 0;
    341      }
    342 
    343      attachment = dom.div(
    344        {
    345          className: "stacktrace devtools-monospace",
    346        },
    347        createElement(SmartTrace, smartTraceAttributes)
    348      );
    349    }
    350 
    351    // If there is an expandable part, make it collapsible.
    352    let collapse = null;
    353    if (collapsible && !disabled) {
    354      collapse = createElement(CollapseButton, {
    355        open,
    356        title: collapseTitle,
    357        onClick: this.toggleMessage,
    358      });
    359    }
    360 
    361    let notesNodes;
    362    if (notes) {
    363      notesNodes = notes.map(note =>
    364        dom.span(
    365          { className: "message-flex-body error-note" },
    366          dom.span(
    367            { className: "message-body devtools-monospace" },
    368            "note: " + note.messageBody
    369          ),
    370          dom.span(
    371            { className: "message-location devtools-monospace" },
    372            note.frame
    373              ? FrameView({
    374                  frame: note.frame,
    375                  onClick: serviceContainer
    376                    ? serviceContainer.onViewSourceInDebugger ||
    377                      serviceContainer.onViewSource
    378                    : undefined,
    379                  showEmptyPathAsHost: true,
    380                  sourceMapURLService: serviceContainer
    381                    ? serviceContainer.sourceMapURLService
    382                    : undefined,
    383                })
    384              : null
    385          )
    386        )
    387      );
    388    } else {
    389      notesNodes = [];
    390    }
    391 
    392    const repeat =
    393      this.props.repeat && this.props.repeat > 1
    394        ? createElement(MessageRepeat, { repeat: this.props.repeat })
    395        : null;
    396 
    397    let onFrameClick;
    398    if (serviceContainer && frame) {
    399      if (source === MESSAGE_SOURCE.CSS) {
    400        onFrameClick =
    401          serviceContainer.onViewSourceInStyleEditor ||
    402          serviceContainer.onViewSource;
    403      } else {
    404        // Point everything else to debugger, if source not available,
    405        // it will fall back to view-source.
    406        onFrameClick =
    407          serviceContainer.onViewSourceInDebugger ||
    408          serviceContainer.onViewSource;
    409      }
    410    }
    411 
    412    // Configure the location.
    413    const location = frame
    414      ? FrameView({
    415          className: "message-location devtools-monospace",
    416          frame,
    417          onClick: onFrameClick,
    418          showEmptyPathAsHost: true,
    419          sourceMapURLService: serviceContainer
    420            ? serviceContainer.sourceMapURLService
    421            : undefined,
    422          messageSource: source,
    423        })
    424      : null;
    425 
    426    let learnMore;
    427    if (exceptionDocURL) {
    428      learnMore = dom.a(
    429        {
    430          className: "learn-more-link webconsole-learn-more-link",
    431          href: exceptionDocURL,
    432          title: exceptionDocURL.split("?")[0],
    433          onClick: this.onLearnMoreClick,
    434        },
    435        `[${l10n.getStr("webConsoleMoreInfoLabel")}]`
    436      );
    437    }
    438 
    439    const bodyElements = Array.isArray(messageBody)
    440      ? messageBody
    441      : [messageBody];
    442 
    443    return dom.div(
    444      {
    445        className: topLevelClasses.join(" "),
    446        onContextMenu: this.onContextMenu,
    447        ref: node => {
    448          this.messageNode = node;
    449        },
    450        "data-message-id": messageId,
    451        "data-indent": indent || 0,
    452        "aria-live": type === MESSAGE_TYPE.COMMAND ? "off" : "polite",
    453      },
    454      timestampEl,
    455      MessageIndent({
    456        indent,
    457        inWarningGroup,
    458      }),
    459      this.props.isBlockedNetworkMessage ? collapse : icon,
    460      this.props.isBlockedNetworkMessage ? icon : collapse,
    461      dom.span(
    462        { className: "message-body-wrapper" },
    463        dom.span(
    464          {
    465            className: "message-flex-body",
    466            onClick: collapsible ? this.toggleMessage : undefined,
    467          },
    468          // Add whitespaces for formatting when copying to the clipboard.
    469          timestampEl ? " " : null,
    470          dom.span(
    471            { className: "message-body devtools-monospace" },
    472            ...bodyElements,
    473            learnMore
    474          ),
    475          repeat ? " " : null,
    476          repeat,
    477          " ",
    478          location
    479        ),
    480        attachment,
    481        ...notesNodes
    482      ),
    483      // If an attachment is displayed, the final newline is handled by the attachment.
    484      attachment ? null : dom.br()
    485    );
    486  }
    487 }
    488 
    489 module.exports = Message;