tor-browser

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

Frame.js (12398B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 const {
      8  Component,
      9 } = require("resource://devtools/client/shared/vendor/react.mjs");
     10 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     11 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     12 const {
     13  getUnicodeUrl,
     14  getUnicodeUrlPath,
     15  getUnicodeHostname,
     16 } = require("resource://devtools/client/shared/unicode-url.js");
     17 const {
     18  getSourceNames,
     19  parseURL,
     20  getSourceMappedFile,
     21 } = require("resource://devtools/client/shared/source-utils.js");
     22 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
     23 const {
     24  MESSAGE_SOURCE,
     25 } = require("resource://devtools/client/webconsole/constants.js");
     26 
     27 const l10n = new LocalizationHelper(
     28  "devtools/client/locales/components.properties"
     29 );
     30 const webl10n = new LocalizationHelper(
     31  "devtools/client/locales/webconsole.properties"
     32 );
     33 
     34 function savedFrameToDebuggerLocation(frame) {
     35  const { source: url, line, column, sourceId } = frame;
     36  return {
     37    url,
     38 
     39    // Line is 1-based everywhere.
     40    line,
     41 
     42    // The column received from spidermonkey Frame objects are 1-based,
     43    // and RDP's console message as well as page errors are providing 1-based columns,
     44    // but most of DevTools frontend consider it to be 0-based, especially the debugger.
     45    //
     46    // Column set to 0 is unknown column location.
     47    column: column >= 1 ? column - 1 : null,
     48 
     49    // The sourceId will be a string if it's a source actor ID, otherwise
     50    // it is either a Spidermonkey-internal ID from a SavedFrame or missing,
     51    // and in either case we can't use the ID for anything useful.
     52    id: typeof sourceId === "string" ? sourceId : null,
     53  };
     54 }
     55 
     56 /**
     57 * Get the tooltip message.
     58 *
     59 * @param {string|undefined} messageSource
     60 * @param {string} url
     61 * @returns {string}
     62 */
     63 function getTooltipMessage(messageSource, url) {
     64  if (messageSource && messageSource === MESSAGE_SOURCE.CSS) {
     65    return l10n.getFormatStr("frame.viewsourceinstyleeditor", url);
     66  }
     67  return l10n.getFormatStr("frame.viewsourceindebugger", url);
     68 }
     69 
     70 class Frame extends Component {
     71  static get propTypes() {
     72    return {
     73      // Optional className that will be put into the element.
     74      className: PropTypes.string,
     75      // SavedFrame, or an object containing all the required properties.
     76      frame: PropTypes.shape({
     77        functionDisplayName: PropTypes.string,
     78        // This could be a SavedFrame with a numeric sourceId, or it could
     79        // be a SavedFrame-like client-side object, in which case the
     80        // "sourceId" will be a source actor ID.
     81        sourceId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
     82        source: PropTypes.string.isRequired,
     83        line: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
     84        column: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
     85      }).isRequired,
     86      // Clicking on the frame link -- probably should link to the debugger.
     87      onClick: PropTypes.func,
     88      // Option to display a function name before the source link.
     89      showFunctionName: PropTypes.bool,
     90      // Option to display a function name even if it's anonymous.
     91      showAnonymousFunctionName: PropTypes.bool,
     92      // Option to display a host name after the source link.
     93      showHost: PropTypes.bool,
     94      // Option to display a host name if the filename is empty or just '/'
     95      showEmptyPathAsHost: PropTypes.bool,
     96      // Option to display a full source instead of just the filename.
     97      showFullSourceUrl: PropTypes.bool,
     98      // Service to enable the source map feature for console.
     99      sourceMapURLService: PropTypes.object,
    100      // The source of the message
    101      messageSource: PropTypes.string,
    102    };
    103  }
    104 
    105  static get defaultProps() {
    106    return {
    107      showFunctionName: false,
    108      showAnonymousFunctionName: false,
    109      showHost: false,
    110      showEmptyPathAsHost: false,
    111      showFullSourceUrl: false,
    112    };
    113  }
    114 
    115  constructor(props) {
    116    super(props);
    117    this.state = {
    118      originalLocation: null,
    119    };
    120    this._locationChanged = this._locationChanged.bind(this);
    121  }
    122 
    123  componentDidMount() {
    124    if (this.props.sourceMapURLService) {
    125      const location = savedFrameToDebuggerLocation(this.props.frame);
    126      // Many things that make use of this component either:
    127      // a) Pass in no sourceId because they have no way to know.
    128      // b) Pass in no sourceId because the actor wasn't created when the
    129      //    server sent its response.
    130      //
    131      // and due to that, we need to use subscribeByLocation in order to
    132      // handle both cases with an without an ID.
    133      this.unsubscribeSourceMapURLService =
    134        this.props.sourceMapURLService.subscribeByLocation(
    135          location,
    136          this._locationChanged
    137        );
    138    }
    139  }
    140 
    141  componentWillUnmount() {
    142    if (this.unsubscribeSourceMapURLService) {
    143      this.unsubscribeSourceMapURLService();
    144    }
    145  }
    146 
    147  _locationChanged(originalLocation) {
    148    this.setState({ originalLocation });
    149  }
    150 
    151  /**
    152   * Get current location's source, line, and column.
    153   *
    154   * @returns {{sourceURL: string, line: number|null, column: number|null}}
    155   */
    156  #getCurrentLocationInfo = () => {
    157    const { frame } = this.props;
    158    const { originalLocation } = this.state;
    159 
    160    const generatedLocation = savedFrameToDebuggerLocation(frame);
    161    const currentLocation = originalLocation || generatedLocation;
    162 
    163    const column = Number.parseInt(currentLocation.column, 10);
    164 
    165    return {
    166      sourceURL: currentLocation.url || "",
    167      // line is 1-based
    168      line: Number(currentLocation.line) || null,
    169      // column is 0-based while we display 1-based numbers
    170      column: typeof column == "number" ? column + 1 : null,
    171    };
    172  };
    173 
    174  /**
    175   * Get unicode hostname of the source link.
    176   *
    177   * @returns {string}
    178   */
    179  #getCurrentLocationUnicodeHostName = () => {
    180    const { sourceURL } = this.#getCurrentLocationInfo();
    181 
    182    const { host } = getSourceNames(sourceURL);
    183    return host ? getUnicodeHostname(host) : "";
    184  };
    185 
    186  /**
    187   * Check if the current location is linkable.
    188   *
    189   * @returns {boolean}
    190   */
    191  #isCurrentLocationLinkable = () => {
    192    const { frame } = this.props;
    193    const { originalLocation } = this.state;
    194 
    195    const generatedLocation = savedFrameToDebuggerLocation(frame);
    196 
    197    // Reparse the URL to determine if we should link this; `getSourceNames`
    198    // has already cached this indirectly. We don't want to attempt to
    199    // link to "self-hosted" and "(unknown)".
    200    // Source mapped sources might not necessary linkable, but they
    201    // are still valid in the debugger.
    202    // If we have a source ID then we can show the source in the debugger.
    203    return !!(
    204      originalLocation ||
    205      generatedLocation.id ||
    206      !!parseURL(generatedLocation.url)
    207    );
    208  };
    209 
    210  /**
    211   * Get the props of the top element.
    212   */
    213  #getTopElementProps = () => {
    214    const { className } = this.props;
    215 
    216    const { sourceURL, line, column } = this.#getCurrentLocationInfo();
    217    const { long } = getSourceNames(sourceURL);
    218    const props = {
    219      "data-url": long,
    220      className: "frame-link" + (className ? ` ${className}` : ""),
    221    };
    222 
    223    // If we have a line number > 0.
    224    if (line) {
    225      // Add `data-line` attribute for testing
    226      props["data-line"] = line;
    227 
    228      // Intentionally exclude 0
    229      if (column) {
    230        // Add `data-column` attribute for testing
    231        props["data-column"] = column;
    232      }
    233    }
    234    return props;
    235  };
    236 
    237  /**
    238   * Get the props of the source element.
    239   */
    240  #getSourceElementsProps = () => {
    241    const { frame, onClick, messageSource } = this.props;
    242 
    243    const generatedLocation = savedFrameToDebuggerLocation(frame);
    244    const { sourceURL, line, column } = this.#getCurrentLocationInfo();
    245    const { long } = getSourceNames(sourceURL);
    246    let url = getUnicodeUrl(long);
    247 
    248    // Exclude all falsy values, including `0`, as line numbers start with 1.
    249    if (line) {
    250      url += `:${line}`;
    251      // Intentionally exclude 0
    252      if (column) {
    253        url += `:${column}`;
    254      }
    255    }
    256 
    257    const isLinkable = this.#isCurrentLocationLinkable();
    258 
    259    // Inner el is useful for achieving ellipsis on the left and correct LTR/RTL
    260    // ordering. See CSS styles for frame-link-source-[inner] and bug 1290056.
    261    const tooltipMessage = getTooltipMessage(messageSource, url);
    262 
    263    const sourceElConfig = {
    264      key: "source",
    265      className: "frame-link-source",
    266      title: isLinkable ? tooltipMessage : url,
    267    };
    268 
    269    if (isLinkable) {
    270      return {
    271        ...sourceElConfig,
    272        onClick: e => {
    273          // We always need to prevent the default behavior of <a> link
    274          e.preventDefault();
    275          if (onClick) {
    276            e.stopPropagation();
    277 
    278            onClick(generatedLocation);
    279          }
    280        },
    281        href: sourceURL,
    282        draggable: false,
    283      };
    284    }
    285 
    286    return sourceElConfig;
    287  };
    288 
    289  /**
    290   * Render the source elements.
    291   *
    292   * @returns {React.ReactNode}
    293   */
    294  #renderSourceElements = () => {
    295    const { line, column } = this.#getCurrentLocationInfo();
    296 
    297    const sourceElements = [this.#renderDisplaySource()];
    298 
    299    if (line) {
    300      let lineInfo = `:${line}`;
    301 
    302      // Intentionally exclude 0
    303      if (column) {
    304        lineInfo += `:${column}`;
    305      }
    306 
    307      sourceElements.push(
    308        dom.span(
    309          {
    310            key: "line",
    311            className: "frame-link-line",
    312          },
    313          lineInfo
    314        )
    315      );
    316    }
    317 
    318    if (this.#isCurrentLocationLinkable()) {
    319      return dom.a(this.#getSourceElementsProps(), sourceElements);
    320    }
    321    // If source is not a URL (self-hosted, eval, etc.), don't make
    322    // it an anchor link, as we can't link to it.
    323    return dom.span(this.#getSourceElementsProps(), sourceElements);
    324  };
    325 
    326  /**
    327   * Render the display source.
    328   *
    329   * @returns {React.ReactNode}
    330   */
    331  #renderDisplaySource = () => {
    332    const { showEmptyPathAsHost, showFullSourceUrl } = this.props;
    333    const { originalLocation } = this.state;
    334 
    335    const { sourceURL } = this.#getCurrentLocationInfo();
    336    const { short, long, host } = getSourceNames(sourceURL);
    337    const unicodeShort = getUnicodeUrlPath(short);
    338    const unicodeLong = getUnicodeUrl(long);
    339    let displaySource = showFullSourceUrl ? unicodeLong : unicodeShort;
    340    if (originalLocation) {
    341      displaySource = getSourceMappedFile(displaySource);
    342 
    343      // In case of pretty-printed HTML file, we would only get the formatted suffix; replace
    344      // it with the full URL instead
    345      if (showEmptyPathAsHost && displaySource == ":formatted") {
    346        displaySource = host + displaySource;
    347      }
    348    } else if (
    349      showEmptyPathAsHost &&
    350      (displaySource === "" || displaySource === "/")
    351    ) {
    352      displaySource = host;
    353    }
    354 
    355    return dom.span(
    356      {
    357        key: "filename",
    358        className: "frame-link-filename",
    359      },
    360      displaySource
    361    );
    362  };
    363 
    364  /**
    365   * Render the function display name.
    366   *
    367   * @returns {React.ReactNode}
    368   */
    369  #renderFunctionDisplayName = () => {
    370    const { frame, showFunctionName, showAnonymousFunctionName } = this.props;
    371    if (!showFunctionName) {
    372      return null;
    373    }
    374    const functionDisplayName = frame.functionDisplayName;
    375    if (functionDisplayName || showAnonymousFunctionName) {
    376      return [
    377        dom.span(
    378          {
    379            key: "function-display-name",
    380            className: "frame-link-function-display-name",
    381          },
    382          functionDisplayName || webl10n.getStr("stacktrace.anonymousFunction")
    383        ),
    384        " ",
    385      ];
    386    }
    387    return null;
    388  };
    389 
    390  render() {
    391    const { showHost } = this.props;
    392 
    393    const elements = [
    394      this.#renderFunctionDisplayName(),
    395      this.#renderSourceElements(),
    396    ];
    397 
    398    const unicodeHost = showHost
    399      ? this.#getCurrentLocationUnicodeHostName()
    400      : null;
    401    if (unicodeHost) {
    402      elements.push(" ");
    403      elements.push(
    404        dom.span(
    405          {
    406            key: "host",
    407            className: "frame-link-host",
    408          },
    409          unicodeHost
    410        )
    411      );
    412    }
    413 
    414    return dom.span(this.#getTopElementProps(), ...elements);
    415  }
    416 }
    417 
    418 module.exports = Frame;