tor-browser

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

ActivityStreamMessageChannel.sys.mjs (10660B)


      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 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  AboutHomeStartupCache: "resource:///modules/AboutHomeStartupCache.sys.mjs",
      9  AboutNewTabParent: "resource:///actors/AboutNewTabParent.sys.mjs",
     10 });
     11 
     12 import {
     13  actionCreators as ac,
     14  actionTypes as at,
     15  actionUtils as au,
     16 } from "resource://newtab/common/Actions.mjs";
     17 
     18 const ABOUT_NEW_TAB_URL = "about:newtab";
     19 
     20 export const DEFAULT_OPTIONS = {
     21  dispatch(action) {
     22    throw new Error(
     23      `\nMessageChannel: Received action ${action.type}, but no dispatcher was defined.\n`
     24    );
     25  },
     26  pageURL: ABOUT_NEW_TAB_URL,
     27  outgoingMessageName: "ActivityStream:MainToContent",
     28  incomingMessageName: "ActivityStream:ContentToMain",
     29 };
     30 
     31 export class ActivityStreamMessageChannel {
     32  /**
     33   * ActivityStreamMessageChannel - This module connects a Redux store to the new tab page actor.
     34   *                  You should use the BroadcastToContent, AlsoToOneContent, and AlsoToMain action creators
     35   *                  in common/Actions.sys.mjs to help you create actions that will be automatically routed
     36   *                  to the correct location.
     37   *
     38   * @param  {object} options
     39   * @param  {function} options.dispatch The dispatch method from a Redux store
     40   * @param  {string} options.pageURL The URL to which the channel is attached, such as about:newtab.
     41   * @param  {string} options.outgoingMessageName The name of the message sent to child processes
     42   * @param  {string} options.incomingMessageName The name of the message received from child processes
     43   * @return {ActivityStreamMessageChannel}
     44   */
     45  constructor(options = {}) {
     46    Object.assign(this, DEFAULT_OPTIONS, options);
     47 
     48    this.middleware = this.middleware.bind(this);
     49    this.onMessage = this.onMessage.bind(this);
     50    this.onNewTabLoad = this.onNewTabLoad.bind(this);
     51    this.onNewTabUnload = this.onNewTabUnload.bind(this);
     52    this.onNewTabInit = this.onNewTabInit.bind(this);
     53  }
     54 
     55  /**
     56   * Get an iterator over the loaded tab objects.
     57   */
     58  get loadedTabs() {
     59    // In the test, AboutNewTabParent is not defined.
     60    return lazy.AboutNewTabParent?.loadedTabs || new Map();
     61  }
     62 
     63  /**
     64   * middleware - Redux middleware that looks for AlsoToOneContent and BroadcastToContent type
     65   *              actions, and sends them out.
     66   *
     67   * @param  {object} store A redux store
     68   * @return {function} Redux middleware
     69   */
     70  middleware() {
     71    return next => action => {
     72      const skipMain = action.meta && action.meta.skipMain;
     73      if (au.isSendToOneContent(action)) {
     74        this.send(action);
     75      } else if (au.isBroadcastToContent(action)) {
     76        this.broadcast(action);
     77      } else if (au.isSendToPreloaded(action)) {
     78        this.sendToPreloaded(action);
     79      }
     80 
     81      if (!skipMain) {
     82        next(action);
     83      }
     84    };
     85  }
     86 
     87  /**
     88   * onActionFromContent - Handler for actions from a content processes
     89   *
     90   * @param  {object} action  A Redux action
     91   * @param  {string} targetId The portID of the port that sent the message
     92   */
     93  onActionFromContent(action, targetId) {
     94    this.dispatch(ac.AlsoToMain(action, this.validatePortID(targetId)));
     95  }
     96 
     97  /**
     98   * broadcast - Sends an action to all ports
     99   *
    100   * @param  {object} action A Redux action
    101   */
    102  broadcast(action) {
    103    // We're trying to update all tabs, so signal the AboutHomeStartupCache
    104    // that its likely time to refresh the cache.
    105    lazy.AboutHomeStartupCache.onPreloadedNewTabMessage();
    106 
    107    for (let { actor } of this.loadedTabs.values()) {
    108      try {
    109        actor.sendAsyncMessage(this.outgoingMessageName, action);
    110      } catch (e) {
    111        // The target page is closed/closing by the user or test, so just ignore.
    112      }
    113    }
    114  }
    115 
    116  /**
    117   * send - Sends an action to a specific port
    118   *
    119   * @param  {obj} action A redux action; it should contain a portID in the meta.toTarget property
    120   */
    121  send(action) {
    122    const targetId = action.meta && action.meta.toTarget;
    123    const target = this.getTargetById(targetId);
    124    try {
    125      target.sendAsyncMessage(this.outgoingMessageName, action);
    126    } catch (e) {
    127      // The target page is closed/closing by the user or test, so just ignore.
    128    }
    129  }
    130 
    131  /**
    132   * A valid portID is a combination of process id and a port number.
    133   * It is generated in AboutNewTabChild.sys.mjs.
    134   */
    135  validatePortID(id) {
    136    if (typeof id !== "string" || !id.includes(":")) {
    137      console.error("Invalid portID");
    138    }
    139 
    140    return id;
    141  }
    142 
    143  /**
    144   * getTargetById - Retrieve the message target by portID, if it exists
    145   *
    146   * @param  {string} id A portID
    147   * @return {obj|null} The message target, if it exists.
    148   */
    149  getTargetById(id) {
    150    this.validatePortID(id);
    151 
    152    for (let { portID, actor } of this.loadedTabs.values()) {
    153      if (portID === id) {
    154        return actor;
    155      }
    156    }
    157    return null;
    158  }
    159 
    160  /**
    161   * sendToPreloaded - Sends an action to each preloaded browser, if any
    162   *
    163   * @param  {obj} action A redux action
    164   */
    165  sendToPreloaded(action) {
    166    // We're trying to update the preloaded about:newtab, so signal
    167    // the AboutHomeStartupCache that its likely time to refresh
    168    // the cache.
    169    lazy.AboutHomeStartupCache.onPreloadedNewTabMessage();
    170 
    171    const preloadedActors = this.getPreloadedActors();
    172    if (preloadedActors && action.data) {
    173      for (let preloadedActor of preloadedActors) {
    174        try {
    175          preloadedActor.sendAsyncMessage(this.outgoingMessageName, action);
    176        } catch (e) {
    177          // The preloaded page is no longer available, so just ignore.
    178        }
    179      }
    180    }
    181  }
    182 
    183  /**
    184   * getPreloadedActors - Retrieve the preloaded actors
    185   *
    186   * @return {Array|null} An array of actors belonging to the preloaded browsers, or null
    187   *                      if there aren't any preloaded browsers
    188   */
    189  getPreloadedActors() {
    190    let preloadedActors = [];
    191    for (let { actor, browser } of this.loadedTabs.values()) {
    192      if (this.isPreloadedBrowser(browser)) {
    193        preloadedActors.push(actor);
    194      }
    195    }
    196    return preloadedActors.length ? preloadedActors : null;
    197  }
    198 
    199  /**
    200   * isPreloadedBrowser - Returns true if the passed browser has been preloaded
    201   *                      for faster rendering of new tabs.
    202   *
    203   * @param {<browser>} A <browser> to check.
    204   * @return {bool} True if the browser is preloaded.
    205   *                      if there aren't any preloaded browsers
    206   */
    207  isPreloadedBrowser(browser) {
    208    return browser.getAttribute("preloadedState") === "preloaded";
    209  }
    210 
    211  simulateMessagesForExistingTabs() {
    212    // Some pages might have already loaded, so we won't get the usual message
    213    for (const loadedTab of this.loadedTabs.values()) {
    214      let simulatedDetails = {
    215        actor: loadedTab.actor,
    216        browser: loadedTab.browser,
    217        browsingContext: loadedTab.browsingContext,
    218        portID: loadedTab.portID,
    219        url: loadedTab.url,
    220        simulated: true,
    221      };
    222 
    223      this.onActionFromContent(
    224        {
    225          type: at.NEW_TAB_INIT,
    226          data: simulatedDetails,
    227        },
    228        loadedTab.portID
    229      );
    230 
    231      if (loadedTab.loaded) {
    232        this.tabLoaded(simulatedDetails);
    233      }
    234    }
    235 
    236    // It's possible that those existing tabs had sent some messages up
    237    // to us before the feeds / ActivityStreamMessageChannel was ready.
    238    //
    239    // AboutNewTabParent takes care of queueing those for us, so
    240    // now that we're ready, we can flush these queued messages.
    241    lazy.AboutNewTabParent.flushQueuedMessagesFromContent();
    242  }
    243 
    244  /**
    245   * onNewTabInit - Handler for special RemotePage:Init message fired
    246   * on initialization.
    247   *
    248   * @param  {obj} msg The messsage from a page that was just initialized
    249   * @param  {obj} tabDetails details about a loaded tab
    250   *
    251   * tabDetails contains:
    252   *   actor, browser, browsingContext, portID, url
    253   */
    254  onNewTabInit(msg, tabDetails) {
    255    this.onActionFromContent(
    256      {
    257        type: at.NEW_TAB_INIT,
    258        data: tabDetails,
    259      },
    260      msg.data.portID
    261    );
    262  }
    263 
    264  /**
    265   * onNewTabLoad - Handler for special RemotePage:Load message fired on page load.
    266   *
    267   * @param  {obj} msg The messsage from a page that was just loaded
    268   * @param  {obj} tabDetails details about a loaded tab, similar to onNewTabInit
    269   */
    270  onNewTabLoad(msg, tabDetails) {
    271    this.tabLoaded(tabDetails);
    272  }
    273 
    274  tabLoaded(tabDetails) {
    275    tabDetails.loaded = true;
    276 
    277    let { browser } = tabDetails;
    278    if (
    279      this.isPreloadedBrowser(browser) &&
    280      browser.ownerGlobal.windowState !== browser.ownerGlobal.STATE_MINIMIZED &&
    281      !browser.ownerGlobal.isFullyOccluded
    282    ) {
    283      // As a perceived performance optimization, if this loaded Activity Stream
    284      // happens to be a preloaded browser in a window that is not minimized or
    285      // occluded, have it render its layers to the compositor now to increase
    286      // the odds that by the time we switch to the tab, the layers are already
    287      // ready to present to the user.
    288      browser.renderLayers = true;
    289    }
    290 
    291    this.onActionFromContent({ type: at.NEW_TAB_LOAD }, tabDetails.portID);
    292  }
    293 
    294  /**
    295   * onNewTabUnloadLoad - Handler for special RemotePage:Unload message fired
    296   * on page unload.
    297   *
    298   * @param  {obj} msg The messsage from a page that was just unloaded
    299   * @param  {obj} tabDetails details about a loaded tab, similar to onNewTabInit
    300   */
    301  onNewTabUnload(msg, tabDetails) {
    302    this.onActionFromContent({ type: at.NEW_TAB_UNLOAD }, tabDetails.portID);
    303  }
    304 
    305  /**
    306   * onMessage - Handles custom messages from content. It expects all messages to
    307   *             be formatted as Redux actions, and dispatches them to this.store
    308   *
    309   * @param  {obj} msg A custom message from content
    310   * @param  {obj} msg.action A Redux action (e.g. {type: "HELLO_WORLD"})
    311   * @param  {obj} msg.target A message target
    312   * @param  {obj} tabDetails details about a loaded tab, similar to onNewTabInit
    313   */
    314  onMessage(msg, tabDetails) {
    315    if (!msg.data || !msg.data.type) {
    316      console.error(
    317        new Error(
    318          `Received an improperly formatted message from ${tabDetails.portID}`
    319        )
    320      );
    321      return;
    322    }
    323    let action = {};
    324    Object.assign(action, msg.data);
    325    // target is used to access a browser reference that came from the content
    326    // and should only be used in feeds (not reducers)
    327    action._target = {
    328      browser: tabDetails.browser,
    329    };
    330 
    331    this.onActionFromContent(action, tabDetails.portID);
    332  }
    333 }