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