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