tor-browser

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

panel.js (13032B)


      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  MultiLocalizationHelper,
      9 } = require("resource://devtools/shared/l10n.js");
     10 const {
     11  FluentL10n,
     12 } = require("resource://devtools/client/shared/fluent-l10n/fluent-l10n.js");
     13 
     14 loader.lazyRequireGetter(
     15  this,
     16  "openContentLink",
     17  "resource://devtools/client/shared/link.js",
     18  true
     19 );
     20 
     21 loader.lazyRequireGetter(
     22  this,
     23  "registerStoreObserver",
     24  "resource://devtools/client/shared/redux/subscriber.js",
     25  true
     26 );
     27 
     28 const DBG_STRINGS_URI = [
     29  "devtools/client/locales/debugger.properties",
     30  // Used by App.js to map the sourceeditor goto line shortcut
     31  "devtools/client/locales/sourceeditor.properties",
     32  // These are used in the AppErrorBoundary component
     33  "devtools/client/locales/startup.properties",
     34  "devtools/client/locales/components.properties",
     35  // Used by SourceMapLoader
     36  "devtools/client/locales/toolbox.properties",
     37 ];
     38 const L10N = new MultiLocalizationHelper(...DBG_STRINGS_URI);
     39 
     40 async function getNodeFront(gripOrFront, toolbox) {
     41  // Given a NodeFront
     42  if ("actorID" in gripOrFront) {
     43    return new Promise(resolve => resolve(gripOrFront));
     44  }
     45 
     46  const inspectorFront = await toolbox.target.getFront("inspector");
     47  return inspectorFront.getNodeFrontFromNodeGrip(gripOrFront);
     48 }
     49 
     50 class DebuggerPanel {
     51  constructor(iframeWindow, toolbox, commands) {
     52    this.panelWin = iframeWindow;
     53    this.panelWin.L10N = L10N;
     54    this.panelWin.sourceMapURLService = toolbox.sourceMapURLService;
     55 
     56    this.toolbox = toolbox;
     57    this.commands = commands;
     58 
     59    // Somewhat equivalent of global `lazy` object used for ChromeUtils.defineESModuleGetter,
     60    // but instantiated once per Debugger document as the modules will be loaded in the
     61    // document scope.
     62    // It is also important to release references to them in destroy in order to prevent leaks.
     63    this.lazyModules = {};
     64  }
     65 
     66  async open() {
     67    // whypaused-* strings are in devtools/shared as they're used in the PausedDebuggerOverlay as well
     68    const fluentL10n = new FluentL10n();
     69    await fluentL10n.init(["devtools/shared/debugger-paused-reasons.ftl"]);
     70 
     71    // Ensure loading all debugger modules via the Browser Loader
     72    // in order to prevent loading duplicated instances of them,
     73    // and also ensure loading them with debugger document as global.
     74    const { browserLoader } = this.panelWin;
     75    browserLoader.lazyRequireGetter(
     76      this.lazyModules,
     77      "features",
     78      "resource://devtools/client/debugger/src/utils/prefs.js",
     79      true
     80    );
     81    browserLoader.lazyRequireGetter(
     82      this.lazyModules,
     83      "getOriginalLocation",
     84      "resource://devtools/client/debugger/src/utils/source-maps.js",
     85      true
     86    );
     87    browserLoader.lazyRequireGetter(
     88      this.lazyModules,
     89      "createLocation",
     90      "resource://devtools/client/debugger/src/utils/location.js",
     91      true
     92    );
     93    browserLoader.lazyRequireGetter(
     94      this.lazyModules,
     95      "getMappedExpression",
     96      "resource://devtools/client/debugger/src/actions/expressions.js",
     97      true
     98    );
     99 
    100    const { actions, store, selectors, client } =
    101      await this.panelWin.Debugger.bootstrap({
    102        commands: this.commands,
    103        fluentBundles: fluentL10n.getBundles(),
    104        resourceCommand: this.commands.resourceCommand,
    105        workers: {
    106          sourceMapLoader: this.toolbox.sourceMapLoader,
    107          parserWorker: this.toolbox.parserWorker,
    108        },
    109        panel: this,
    110      });
    111 
    112    this._actions = actions;
    113    this._store = store;
    114    this._selectors = selectors;
    115    this._client = client;
    116 
    117    registerStoreObserver(this._store, this._onDebuggerStateChange.bind(this));
    118 
    119    return this;
    120  }
    121 
    122  async _onDebuggerStateChange(state, oldState) {
    123    const { getCurrentThread } = this._selectors;
    124    const currentThreadActorID = getCurrentThread(state);
    125 
    126    if (
    127      currentThreadActorID &&
    128      currentThreadActorID !== getCurrentThread(oldState)
    129    ) {
    130      const threadFront =
    131        this.commands.client.getFrontByID(currentThreadActorID);
    132      this.toolbox.selectTarget(threadFront?.targetFront.actorID);
    133    }
    134 
    135    this.toolbox.emit(
    136      "show-original-variable-mapping-warnings",
    137      this.shouldShowOriginalVariableMappingWarnings()
    138    );
    139  }
    140 
    141  shouldShowOriginalVariableMappingWarnings() {
    142    const { getSelectedSource, isMapScopesEnabled } = this._selectors;
    143    if (!this.isPaused() || isMapScopesEnabled(this._getState())) {
    144      return false;
    145    }
    146    const selectedSource = getSelectedSource(this._getState());
    147    return selectedSource?.isOriginal && !selectedSource?.isPrettyPrinted;
    148  }
    149 
    150  getVarsForTests() {
    151    return {
    152      store: this._store,
    153      selectors: this._selectors,
    154      actions: this._actions,
    155      client: this._client,
    156    };
    157  }
    158 
    159  _getState() {
    160    return this._store.getState();
    161  }
    162 
    163  getToolboxStore() {
    164    return this.toolbox.store;
    165  }
    166 
    167  openLink(url) {
    168    openContentLink(url);
    169  }
    170 
    171  async openConsoleAndEvaluate(input) {
    172    const { hud } = await this.toolbox.selectTool("webconsole");
    173    hud.ui.wrapper.dispatchEvaluateExpression(input);
    174  }
    175 
    176  async openInspector() {
    177    this.toolbox.selectTool("inspector");
    178  }
    179 
    180  async openElementInInspector(gripOrFront) {
    181    const onSelectInspector = this.toolbox.selectTool("inspector");
    182    const onGripNodeToFront = getNodeFront(gripOrFront, this.toolbox);
    183 
    184    const [front, inspector] = await Promise.all([
    185      onGripNodeToFront,
    186      onSelectInspector,
    187    ]);
    188 
    189    const onInspectorUpdated = inspector.once("inspector-updated");
    190    const onNodeFrontSet = this.toolbox.selection.setNodeFront(front, {
    191      reason: "debugger",
    192    });
    193 
    194    return Promise.all([onNodeFrontSet, onInspectorUpdated]);
    195  }
    196 
    197  highlightDomElement(gripOrFront) {
    198    if (!this._highlight) {
    199      const { highlight, unhighlight } = this.toolbox.getHighlighter();
    200      this._highlight = highlight;
    201      this._unhighlight = unhighlight;
    202    }
    203 
    204    return this._highlight(gripOrFront);
    205  }
    206 
    207  unHighlightDomElement() {
    208    if (!this._unhighlight) {
    209      return Promise.resolve();
    210    }
    211 
    212    return this._unhighlight();
    213  }
    214 
    215  /**
    216   * Return the Frame Actor ID of the currently selected frame,
    217   * or null if the debugger isn't paused.
    218   */
    219  getSelectedFrameActorID() {
    220    const selectedFrame = this._selectors.getSelectedFrame(this._getState());
    221    if (selectedFrame) {
    222      return selectedFrame.id;
    223    }
    224    return null;
    225  }
    226 
    227  getMappedExpression(expression) {
    228    const thread = this._selectors.getCurrentThread(this._getState());
    229    return this.lazyModules.getMappedExpression(expression, thread, {
    230      getState: this._store.getState,
    231      parserWorker: this.toolbox.parserWorker,
    232    });
    233  }
    234 
    235  /**
    236   * Return the source-mapped variables for the current scope.
    237   *
    238   * @returns {{[string]: string} | null} A dictionary mapping original variable names to generated
    239   * variable names if map scopes is enabled, otherwise null.
    240   */
    241  getMappedVariables() {
    242    if (!this._selectors.isMapScopesEnabled(this._getState())) {
    243      return null;
    244    }
    245    const thread = this._selectors.getCurrentThread(this._getState());
    246    return this._selectors.getSelectedScopeMappings(this._getState(), thread);
    247  }
    248 
    249  isPaused() {
    250    const thread = this._selectors.getCurrentThread(this._getState());
    251    return this._selectors.getIsPaused(this._getState(), thread);
    252  }
    253 
    254  selectSourceURL(url, line, column) {
    255    return this._actions.selectSourceURL(url, { line, column });
    256  }
    257 
    258  /**
    259   * This is called when some other panels wants to open a given source
    260   * in the debugger at a precise line/column.
    261   *
    262   * @param {string} generatedURL
    263   * @param {number} generatedLine
    264   * @param {number} generatedColumn
    265   * @param {string} sourceActorId (optional)
    266   *        If the callsite knows about a particular sourceActorId,
    267   *        or if the source doesn't have a URL, you have to pass a sourceActorId.
    268   * @param {string} reason
    269   *        A telemetry identifier to record when opening the debugger.
    270   *        This help differentiate why we opened the debugger.
    271   *
    272   * @return {boolean}
    273   *         Returns true if the location is known by the debugger
    274   *         and the debugger opens it.
    275   */
    276  async openSourceInDebugger({
    277    generatedURL,
    278    generatedLine,
    279    generatedColumn,
    280    sourceActorId,
    281    reason,
    282  }) {
    283    // Resolve the URL in case this is an URL like http://example.org/./test.js
    284    // as the frontend only supports final resolved URLs.
    285    generatedURL = URL.parse(generatedURL)?.href || generatedURL;
    286 
    287    const generatedSource = sourceActorId
    288      ? this._selectors.getSourceByActorId(this._getState(), sourceActorId)
    289      : this._selectors.getSourceByURL(this._getState(), generatedURL);
    290    // We won't try opening source in the debugger when we can't find the related source actor in the reducer,
    291    // or, when it doesn't have any related source actor registered.
    292    if (
    293      !generatedSource ||
    294      // Note: We're not entirely sure when this can happen,
    295      // so we may want to revisit that at some point.
    296      !this._selectors.getSourceActorsForSource(
    297        this._getState(),
    298        generatedSource.id
    299      ).length
    300    ) {
    301      return false;
    302    }
    303 
    304    const generatedLocation = this.lazyModules.createLocation({
    305      source: generatedSource,
    306      line: generatedLine,
    307      column: generatedColumn,
    308    });
    309 
    310    // Note that getOriginalLocation can easily return generatedLocation
    311    // if the location can't be mapped to any original source.
    312    // So that we may open either regular source or original sources here.
    313    const originalLocation = await this.lazyModules.getOriginalLocation(
    314      generatedLocation,
    315      {
    316        // Reproduce a minimal thunkArgs for getOriginalLocation.
    317        sourceMapLoader: this.toolbox.sourceMapLoader,
    318        getState: this._store.getState,
    319      }
    320    );
    321 
    322    // view-source module only forced the load of debugger in the background.
    323    // Now that we know we want to show a source, force displaying it in foreground.
    324    //
    325    // Note that browser_markup_view-source.js doesn't wait for the debugger
    326    // to be fully loaded with the source and requires the debugger to be loaded late.
    327    // But we might try to load display it early to improve user perception.
    328    await this.toolbox.selectTool("jsdebugger", reason);
    329 
    330    const hasLogpoint = this._selectors.hasLogpoint(
    331      this._getState(),
    332      originalLocation
    333    );
    334    await this._actions.selectLocation(originalLocation, {
    335      // We want to select the precise given location and do not try to map to original/bundle
    336      // depending on the currently selected source type.
    337      keepContext: false,
    338      // We don't want to highlight/focus CodeMirror if there is a log point as we will open the log point panel right after and focus it
    339      highlight: !hasLogpoint,
    340    });
    341 
    342    // XXX: should this be moved to selectSpecificLocation??
    343    if (hasLogpoint) {
    344      this._actions.openConditionalPanel(originalLocation, true);
    345    }
    346 
    347    return true;
    348  }
    349 
    350  async selectServiceWorker(workerDescriptorFront) {
    351    // The descriptor used by the application panel isn't fetching the worker target,
    352    // but the debugger will fetch it via the watcher actor and TargetCommand.
    353    // So try to match the descriptor with its related target.
    354    const targets = this.commands.targetCommand.getAllTargets([
    355      this.commands.targetCommand.TYPES.SERVICE_WORKER,
    356    ]);
    357    const workerTarget = targets.find(
    358      target => target.id == workerDescriptorFront.id
    359    );
    360 
    361    const threadFront = await workerTarget.getFront("thread");
    362    const threadActorID = threadFront?.actorID;
    363    const isThreadAvailable = this._selectors
    364      .getThreads(this._getState())
    365      .find(x => x.actor === threadActorID);
    366 
    367    if (!this.lazyModules.features.windowlessServiceWorkers) {
    368      console.error(
    369        "Selecting a worker needs the pref debugger.features.windowless-service-workers set to true"
    370      );
    371      return;
    372    }
    373 
    374    if (!isThreadAvailable) {
    375      console.error(`Worker ${threadActorID} is not available for debugging`);
    376      return;
    377    }
    378 
    379    // select worker's thread
    380    this.selectThread(threadActorID);
    381 
    382    // select worker's source
    383    const source = this._selectors.getSourceByURL(
    384      this._getState(),
    385      workerDescriptorFront._url
    386    );
    387    const sourceActor = this._selectors.getFirstSourceActorForGeneratedSource(
    388      this._getState(),
    389      source.id,
    390      threadActorID
    391    );
    392    await this._actions.selectSource(source, sourceActor);
    393  }
    394 
    395  selectThread(threadActorID) {
    396    this._actions.selectThread(threadActorID);
    397  }
    398 
    399  showTracerSidebar() {
    400    this._actions.setPrimaryPaneTab("tracer");
    401  }
    402 
    403  destroy() {
    404    this.panelWin.Debugger.destroy();
    405    this.lazyModules = {};
    406    this.emit("destroyed");
    407  }
    408 }
    409 
    410 exports.DebuggerPanel = DebuggerPanel;