tor-browser

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

webconsole-wrapper.js (15546B)


      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 "use strict";
      5 
      6 const {
      7  createElement,
      8  createFactory,
      9 } = require("resource://devtools/client/shared/vendor/react.mjs");
     10 const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.mjs");
     11 const {
     12  Provider,
     13  createProvider,
     14 } = require("resource://devtools/client/shared/vendor/react-redux.js");
     15 
     16 const actions = require("resource://devtools/client/webconsole/actions/index.js");
     17 const {
     18  configureStore,
     19 } = require("resource://devtools/client/webconsole/store.js");
     20 
     21 const {
     22  isPacketPrivate,
     23 } = require("resource://devtools/client/webconsole/utils/messages.js");
     24 const {
     25  getMutableMessagesById,
     26  getMessage,
     27  getAllNetworkMessagesUpdateById,
     28 } = require("resource://devtools/client/webconsole/selectors/messages.js");
     29 
     30 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
     31 const App = createFactory(
     32  require("resource://devtools/client/webconsole/components/App.js")
     33 );
     34 const {
     35  getAllFilters,
     36 } = require("resource://devtools/client/webconsole/selectors/filters.js");
     37 
     38 loader.lazyGetter(this, "AppErrorBoundary", () =>
     39  createFactory(
     40    require("resource://devtools/client/shared/components/AppErrorBoundary.js")
     41  )
     42 );
     43 
     44 const {
     45  setupServiceContainer,
     46 } = require("resource://devtools/client/webconsole/service-container.js");
     47 
     48 loader.lazyRequireGetter(
     49  this,
     50  "Constants",
     51  "resource://devtools/client/webconsole/constants.js"
     52 );
     53 
     54 // Localized strings for (devtools/client/locales/en-US/startup.properties)
     55 loader.lazyGetter(this, "L10N", function () {
     56  const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
     57  return new LocalizationHelper("devtools/client/locales/startup.properties");
     58 });
     59 
     60 // Only Browser Console needs Fluent bundles at the moment
     61 loader.lazyRequireGetter(
     62  this,
     63  "FluentL10n",
     64  "resource://devtools/client/shared/fluent-l10n/fluent-l10n.js",
     65  true
     66 );
     67 loader.lazyRequireGetter(
     68  this,
     69  "LocalizationProvider",
     70  "resource://devtools/client/shared/vendor/fluent-react.js",
     71  true
     72 );
     73 
     74 let store = null;
     75 
     76 class WebConsoleWrapper {
     77  /**
     78   *
     79   * @param {HTMLElement} parentNode
     80   * @param {WebConsoleUI} webConsoleUI
     81   * @param {Toolbox} toolbox
     82   * @param {Document} document
     83   */
     84  constructor(parentNode, webConsoleUI, toolbox, document) {
     85    EventEmitter.decorate(this);
     86 
     87    this.parentNode = parentNode;
     88    this.webConsoleUI = webConsoleUI;
     89    this.toolbox = toolbox;
     90    this.hud = this.webConsoleUI.hud;
     91    this.document = document;
     92 
     93    this.init = this.init.bind(this);
     94 
     95    this.queuedMessageAdds = [];
     96    this.queuedMessageUpdates = [];
     97    this.queuedRequestUpdates = [];
     98    this.throttledDispatchPromise = null;
     99 
    100    this.telemetry = this.hud.telemetry;
    101  }
    102 
    103  #serviceContainer;
    104 
    105  async init() {
    106    const { webConsoleUI } = this;
    107 
    108    let fluentBundles;
    109    if (webConsoleUI.isBrowserConsole) {
    110      const fluentL10n = new FluentL10n();
    111      await fluentL10n.init(["devtools/client/toolbox.ftl"]);
    112      fluentBundles = fluentL10n.getBundles();
    113    }
    114 
    115    return new Promise(resolve => {
    116      store = configureStore(this.webConsoleUI, {
    117        // We may not have access to the toolbox (e.g. in the browser console).
    118        telemetry: this.telemetry,
    119        thunkArgs: {
    120          webConsoleUI,
    121          hud: this.hud,
    122          toolbox: this.toolbox,
    123          commands: this.hud.commands,
    124        },
    125      });
    126 
    127      const serviceContainer = this.getServiceContainer();
    128 
    129      const app = AppErrorBoundary(
    130        {
    131          componentName: "Console",
    132          panel: L10N.getStr("ToolboxTabWebconsole.label"),
    133          // The AppErrorBoundary renders a link to file a bug, but in the case of the
    134          // browser console, we need to have a specific handler to open the link in the
    135          // main Firefox window
    136          openLink: webConsoleUI.isBrowserConsole
    137            ? serviceContainer.openLink
    138            : null,
    139        },
    140        App({
    141          serviceContainer,
    142          webConsoleUI,
    143          onFirstMeaningfulPaint: resolve,
    144          closeSplitConsole: this.closeSplitConsole.bind(this),
    145          inputEnabled:
    146            !webConsoleUI.isBrowserConsole ||
    147            Services.prefs.getBoolPref("devtools.chrome.enabled"),
    148        })
    149      );
    150 
    151      // Render the root Application component.
    152      if (this.parentNode) {
    153        const maybeLocalizedElement = fluentBundles
    154          ? createElement(LocalizationProvider, { bundles: fluentBundles }, app)
    155          : app;
    156 
    157        this.body = ReactDOM.render(
    158          createElement(
    159            Provider,
    160            { store },
    161            createElement(
    162              createProvider(this.hud.commands.targetCommand.storeId),
    163              { store: this.hud.commands.targetCommand.store },
    164              maybeLocalizedElement
    165            )
    166          ),
    167          this.parentNode
    168        );
    169      } else {
    170        // If there's no parentNode, we are in a test. So we can resolve immediately.
    171        resolve();
    172      }
    173    });
    174  }
    175 
    176  destroy() {
    177    // This component can be instantiated from jest test, in which case we don't have
    178    // a parentNode reference.
    179    if (this.parentNode) {
    180      ReactDOM.unmountComponentAtNode(this.parentNode);
    181    }
    182  }
    183 
    184  /**
    185   * Query the reducer store for the current state of filtering
    186   * a given type of message
    187   *
    188   * @param {string} filter
    189   *        Type of message to be filtered.
    190   * @return {boolean}
    191   *         True if this type of message should be displayed.
    192   */
    193  getFilterState(filter) {
    194    return getAllFilters(this.getStore().getState())[filter];
    195  }
    196 
    197  dispatchMessageAdd(packet) {
    198    this.batchedMessagesAdd([packet]);
    199  }
    200 
    201  dispatchMessagesAdd(messages) {
    202    this.batchedMessagesAdd(messages);
    203  }
    204 
    205  dispatchNetworkMessagesDisable() {
    206    const networkMessageIds = Object.keys(
    207      getAllNetworkMessagesUpdateById(store.getState())
    208    );
    209    store.dispatch(actions.messagesDisable(networkMessageIds));
    210  }
    211 
    212  dispatchMessagesClear() {
    213    // We might still have pending message additions and updates when the clear action is
    214    // triggered, so we need to flush them to make sure we don't have unexpected behavior
    215    // in the ConsoleOutput. *But* we want to keep any pending navigation request,
    216    // as we want to keep displaying them even if we received a clear request.
    217    function filter(l) {
    218      return l.filter(update => update.isNavigationRequest);
    219    }
    220    this.queuedMessageAdds = filter(this.queuedMessageAdds);
    221    this.queuedMessageUpdates = filter(this.queuedMessageUpdates);
    222    this.queuedRequestUpdates = this.queuedRequestUpdates.filter(
    223      update => update.data.isNavigationRequest
    224    );
    225 
    226    store?.dispatch(actions.messagesClear());
    227    this.webConsoleUI.emitForTests("messages-cleared");
    228  }
    229 
    230  dispatchPrivateMessagesClear() {
    231    // We might still have pending private message additions when the private messages
    232    // clear action is triggered. We need to remove any private-window-issued packets from
    233    // the queue so they won't appear in the output.
    234 
    235    // For (network) message updates, we need to check both messages queue and the state
    236    // since we can receive updates even if the message isn't rendered yet.
    237    const messages = [...getMutableMessagesById(store.getState()).values()];
    238    this.queuedMessageUpdates = this.queuedMessageUpdates.filter(
    239      ({ actor }) => {
    240        const queuedNetworkMessage = this.queuedMessageAdds.find(
    241          p => p.actor === actor
    242        );
    243        if (queuedNetworkMessage && isPacketPrivate(queuedNetworkMessage)) {
    244          return false;
    245        }
    246 
    247        const requestMessage = messages.find(
    248          message => actor === message.actor
    249        );
    250        if (requestMessage && requestMessage.private === true) {
    251          return false;
    252        }
    253 
    254        return true;
    255      }
    256    );
    257 
    258    // For (network) requests updates, we can check only the state, since there must be a
    259    // user interaction to get an update (i.e. the network message is displayed and thus
    260    // in the state).
    261    this.queuedRequestUpdates = this.queuedRequestUpdates.filter(({ id }) => {
    262      const requestMessage = getMessage(store.getState(), id);
    263      if (requestMessage && requestMessage.private === true) {
    264        return false;
    265      }
    266 
    267      return true;
    268    });
    269 
    270    // Finally we clear the messages queue. This needs to be done here since we use it to
    271    // clean the other queues.
    272    this.queuedMessageAdds = this.queuedMessageAdds.filter(
    273      p => !isPacketPrivate(p)
    274    );
    275 
    276    store.dispatch(actions.privateMessagesClear());
    277  }
    278 
    279  dispatchTargetMessagesRemove(targetFront) {
    280    // We might still have pending packets in the queues from the target that we need to remove
    281    // to prevent messages appearing in the output.
    282 
    283    for (let i = this.queuedMessageUpdates.length - 1; i >= 0; i--) {
    284      const packet = this.queuedMessageUpdates[i];
    285      if (packet.targetFront == targetFront) {
    286        this.queuedMessageUpdates.splice(i, 1);
    287      }
    288    }
    289 
    290    for (let i = this.queuedRequestUpdates.length - 1; i >= 0; i--) {
    291      const packet = this.queuedRequestUpdates[i];
    292      if (packet.data.targetFront == targetFront) {
    293        this.queuedRequestUpdates.splice(i, 1);
    294      }
    295    }
    296 
    297    for (let i = this.queuedMessageAdds.length - 1; i >= 0; i--) {
    298      const packet = this.queuedMessageAdds[i];
    299      // Keep in sync with the check done in the reducer for the TARGET_MESSAGES_REMOVE action.
    300      if (
    301        packet.targetFront == targetFront &&
    302        packet.type !== Constants.MESSAGE_TYPE.COMMAND &&
    303        packet.type !== Constants.MESSAGE_TYPE.RESULT
    304      ) {
    305        this.queuedMessageAdds.splice(i, 1);
    306      }
    307    }
    308 
    309    store.dispatch(actions.targetMessagesRemove(targetFront));
    310  }
    311 
    312  dispatchMessagesUpdate(messages) {
    313    this.batchedMessagesUpdates(messages);
    314  }
    315 
    316  dispatchSidebarClose() {
    317    store.dispatch(actions.sidebarClose());
    318  }
    319 
    320  dispatchSplitConsoleCloseButtonToggle() {
    321    store.dispatch(
    322      actions.splitConsoleCloseButtonToggle(
    323        this.toolbox && this.toolbox.currentToolId !== "webconsole"
    324      )
    325    );
    326  }
    327 
    328  dispatchTabWillNavigate(packet) {
    329    const { ui } = store.getState();
    330 
    331    // For the browser console, we receive tab navigation
    332    // when the original top level window we attached to is closed,
    333    // but we don't want to reset console history and just switch to
    334    // the next available window.
    335    if (ui.persistLogs || this.webConsoleUI.isBrowserConsole) {
    336      // Add a type in order for this event packet to be identified by
    337      // utils/messages.js's `transformPacket`
    338      packet.type = "will-navigate";
    339      this.dispatchMessageAdd(packet);
    340    } else {
    341      this.dispatchMessagesClear();
    342      store.dispatch({
    343        type: Constants.WILL_NAVIGATE,
    344      });
    345    }
    346  }
    347 
    348  batchedMessagesUpdates(messages) {
    349    if (messages.length) {
    350      this.queuedMessageUpdates.push(...messages);
    351      this.setTimeoutIfNeeded();
    352    }
    353  }
    354 
    355  batchedRequestUpdates(message) {
    356    this.queuedRequestUpdates.push(message);
    357    return this.setTimeoutIfNeeded();
    358  }
    359 
    360  batchedMessagesAdd(messages) {
    361    if (messages.length) {
    362      this.queuedMessageAdds.push(...messages);
    363      this.setTimeoutIfNeeded();
    364    }
    365  }
    366 
    367  dispatchClearHistory() {
    368    store.dispatch(actions.clearHistory());
    369  }
    370 
    371  /**
    372   *
    373   * @param {string} expression: The expression to evaluate
    374   */
    375  dispatchEvaluateExpression(expression) {
    376    store.dispatch(actions.evaluateExpression(expression));
    377  }
    378 
    379  dispatchUpdateInstantEvaluationResultForCurrentExpression() {
    380    store.dispatch(actions.updateInstantEvaluationResultForCurrentExpression());
    381  }
    382 
    383  /**
    384   * Returns a Promise that resolves once any async dispatch is finally dispatched.
    385   */
    386  waitAsyncDispatches() {
    387    if (!this.throttledDispatchPromise) {
    388      return Promise.resolve();
    389    }
    390    // When closing the console during initialization,
    391    // setTimeoutIfNeeded may never resolve its promise
    392    // as window.setTimeout will be disabled on document destruction.
    393    const onUnload = new Promise(r =>
    394      window.addEventListener("unload", r, { once: true })
    395    );
    396    return Promise.race([this.throttledDispatchPromise, onUnload]);
    397  }
    398 
    399  setTimeoutIfNeeded() {
    400    if (this.throttledDispatchPromise) {
    401      return this.throttledDispatchPromise;
    402    }
    403    this.throttledDispatchPromise = new Promise(done => {
    404      setTimeout(async () => {
    405        this.throttledDispatchPromise = null;
    406 
    407        if (!store) {
    408          // The store is not initialized yet, we can call setTimeoutIfNeeded so the
    409          // messages will be handled in the next timeout when the store is ready.
    410          this.setTimeoutIfNeeded();
    411          done();
    412          return;
    413        }
    414 
    415        const { ui } = store.getState();
    416        store.dispatch(
    417          actions.messagesAdd(this.queuedMessageAdds, null, ui.persistLogs)
    418        );
    419 
    420        const { length } = this.queuedMessageAdds;
    421 
    422        // This telemetry event is only useful when we have a toolbox so only
    423        // send it when we have one.
    424        if (this.toolbox) {
    425          this.telemetry.addEventProperty(
    426            this.toolbox,
    427            "enter",
    428            "webconsole",
    429            null,
    430            "message_count",
    431            length
    432          );
    433        }
    434 
    435        this.queuedMessageAdds = [];
    436 
    437        if (this.queuedMessageUpdates.length) {
    438          await store.dispatch(
    439            actions.networkMessageUpdates(this.queuedMessageUpdates)
    440          );
    441          this.webConsoleUI.emitForTests("network-messages-updated");
    442          this.queuedMessageUpdates = [];
    443        }
    444        if (this.queuedRequestUpdates.length) {
    445          await store.dispatch(
    446            actions.networkUpdateRequests(this.queuedRequestUpdates)
    447          );
    448          const updateCount = this.queuedRequestUpdates.length;
    449          this.queuedRequestUpdates = [];
    450 
    451          // Fire an event indicating that all data fetched from
    452          // the backend has been received. This is based on
    453          // 'FirefoxDataProvider.isQueuePayloadReady', see more
    454          // comments in that method.
    455          // (netmonitor/src/connector/firefox-data-provider).
    456          // This event might be utilized in tests to find the right
    457          // time when to finish.
    458 
    459          this.webConsoleUI.emitForTests(
    460            "network-request-payload-ready",
    461            updateCount
    462          );
    463        }
    464        done();
    465      }, 50);
    466    });
    467    return this.throttledDispatchPromise;
    468  }
    469 
    470  getStore() {
    471    return store;
    472  }
    473 
    474  getServiceContainer() {
    475    if (!this.#serviceContainer) {
    476      this.#serviceContainer = setupServiceContainer({
    477        webConsoleUI: this.webConsoleUI,
    478        toolbox: this.toolbox,
    479        hud: this.hud,
    480        webConsoleWrapper: this,
    481      });
    482    }
    483    return this.#serviceContainer;
    484  }
    485 
    486  subscribeToStore(callback) {
    487    store.subscribe(() => callback(store.getState()));
    488  }
    489 
    490  createElement(nodename) {
    491    return this.document.createElement(nodename);
    492  }
    493 
    494  // Called by pushing close button.
    495  closeSplitConsole() {
    496    this.toolbox.closeSplitConsole();
    497  }
    498 
    499  toggleOriginalVariableMappingEvaluationNotification(show) {
    500    store.dispatch(
    501      actions.showEvaluationNotification(
    502        show ? Constants.ORIGINAL_VARIABLE_MAPPING : ""
    503      )
    504    );
    505  }
    506 }
    507 
    508 // Exports from this module
    509 module.exports = WebConsoleWrapper;