1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
7 * Provides a HTML5 postMessage channel to the injected JS to talk back
12 <include src="../gaia_auth/channel.js">
14 var PostMessageChannel = (function() {
16 * Allowed origins of the hosting page.
17 * @type {Array<string>}
19 var ALLOWED_ORIGINS = [
21 'chrome://chrome-signin'
25 var PORT_MESSAGE = 'post-message-port-message';
28 var CHANNEL_INIT_MESSAGE = 'post-message-channel-init';
31 var CHANNEL_CONNECT_MESSAGE = 'post-message-channel-connect';
34 * Whether the script runs in a top level window.
36 function isTopLevel() {
37 return window === window.top;
41 * A simple event target.
43 function EventTarget() {
47 EventTarget.prototype = {
49 * Add an event listener.
51 addListener: function(listener) {
52 this.listeners_.push(listener);
56 * Dispatches a given event to all listeners.
58 dispatch: function(e) {
59 for (var i = 0; i < this.listeners_.length; ++i) {
60 this.listeners_[i].call(undefined, e);
66 * ChannelManager handles window message events by dispatching them to
67 * PostMessagePorts or forwarding to other windows (up/down the hierarchy).
70 function ChannelManager() {
72 * Window and origin to forward message up the hierarchy. For subframes,
73 * they defaults to window.parent and any origin. For top level window,
74 * this would be set to the hosting webview on CHANNEL_INIT_MESSAGE.
76 this.upperWindow = isTopLevel() ? null : window.parent;
77 this.upperOrigin = isTopLevel() ? '' : '*';
80 * Channle Id to port map.
81 * @type {Object<number, PostMessagePort>}
86 * Deferred messages to be posted to |upperWindow|.
89 this.deferredUpperWindowMessages_ = [];
92 * Ports that depend on upperWindow and need to be setup when its available.
94 this.deferredUpperWindowPorts_ = [];
97 * Whether the ChannelManager runs in daemon mode and accepts connections.
99 this.isDaemon = false;
102 * Fires when ChannelManager is in listening mode and a
103 * CHANNEL_CONNECT_MESSAGE is received.
105 this.onConnect = new EventTarget();
107 window.addEventListener('message', this.onMessage_.bind(this));
110 ChannelManager.prototype = {
112 * Gets a global unique id to use.
115 createChannelId_: function() {
116 return (new Date()).getTime();
120 * Posts data to upperWindow. Queue it if upperWindow is not available.
122 postToUpperWindow: function(data) {
123 if (this.upperWindow == null) {
124 this.deferredUpperWindowMessages_.push(data);
128 this.upperWindow.postMessage(data, this.upperOrigin);
132 * Creates a port and register it in |channels_|.
133 * @param {number} channelId
134 * @param {string} channelName
135 * @param {DOMWindow=} opt_targetWindow
136 * @param {string=} opt_targetOrigin
138 createPort: function(
139 channelId, channelName, opt_targetWindow, opt_targetOrigin) {
140 var port = new PostMessagePort(channelId, channelName);
141 if (opt_targetWindow)
142 port.setTarget(opt_targetWindow, opt_targetOrigin);
143 this.channels_[channelId] = port;
148 * Returns a message forward handler for the given proxy port.
151 getProxyPortForwardHandler_: function(proxyPort) {
152 return function(msg) { proxyPort.postMessage(msg); };
156 * Creates a forwarding porxy port.
157 * @param {number} channelId
158 * @param {string} channelName
159 * @param {!DOMWindow} targetWindow
160 * @param {!string} targetOrigin
162 createProxyPort: function(
163 channelId, channelName, targetWindow, targetOrigin) {
164 var port = this.createPort(
165 channelId, channelName, targetWindow, targetOrigin);
166 port.onMessage.addListener(this.getProxyPortForwardHandler_(port));
171 * Creates a connecting port to the daemon and request connection.
172 * @param {string} name
173 * @return {PostMessagePort}
175 connectToDaemon: function(name) {
178 'Error: Connecting from the daemon page is not supported.');
182 var port = this.createPort(this.createChannelId_(), name);
183 if (this.upperWindow) {
184 port.setTarget(this.upperWindow, this.upperOrigin);
186 this.deferredUpperWindowPorts_.push(port);
189 this.postToUpperWindow({
190 type: CHANNEL_CONNECT_MESSAGE,
191 channelId: port.channelId,
192 channelName: port.name
198 * Dispatches a 'message' event to port.
201 dispatchMessageToPort_: function(e) {
202 var channelId = e.data.channelId;
203 var port = this.channels_[channelId];
205 console.error('Error: Unable to dispatch message. Unknown channel.');
209 port.handleWindowMessage(e);
213 * Window 'message' handler.
215 onMessage_: function(e) {
216 if (typeof e.data != 'object' ||
217 !e.data.hasOwnProperty('type')) {
221 if (e.data.type === PORT_MESSAGE) {
222 // Dispatch port message to ports if this is the daemon page or
223 // the message is from upperWindow. In case of null upperWindow,
224 // the message is assumed to be forwarded to upperWindow and queued.
226 (this.upperWindow && e.source === this.upperWindow)) {
227 this.dispatchMessageToPort_(e);
229 this.postToUpperWindow(e.data);
231 } else if (e.data.type === CHANNEL_CONNECT_MESSAGE) {
232 var channelId = e.data.channelId;
233 var channelName = e.data.channelName;
236 var port = this.createPort(
237 channelId, channelName, e.source, e.origin);
238 this.onConnect.dispatch(port);
240 this.createProxyPort(channelId, channelName, e.source, e.origin);
241 this.postToUpperWindow(e.data);
243 } else if (e.data.type === CHANNEL_INIT_MESSAGE) {
244 if (ALLOWED_ORIGINS.indexOf(e.origin) == -1)
247 this.upperWindow = e.source;
248 this.upperOrigin = e.origin;
250 for (var i = 0; i < this.deferredUpperWindowMessages_.length; ++i) {
251 this.upperWindow.postMessage(this.deferredUpperWindowMessages_[i],
254 this.deferredUpperWindowMessages_ = [];
256 for (var i = 0; i < this.deferredUpperWindowPorts_.length; ++i) {
257 this.deferredUpperWindowPorts_[i].setTarget(this.upperWindow,
260 this.deferredUpperWindowPorts_ = [];
266 * Singleton instance of ChannelManager.
267 * @type {ChannelManager}
269 var channelManager = new ChannelManager();
272 * A HTML5 postMessage based port that provides the same port interface
273 * as the messaging API port.
274 * @param {number} channelId
275 * @param {string} name
277 function PostMessagePort(channelId, name) {
278 this.channelId = channelId;
280 this.targetWindow = null;
281 this.targetOrigin = '';
282 this.deferredMessages_ = [];
284 this.onMessage = new EventTarget();
287 PostMessagePort.prototype = {
289 * Sets the target window and origin.
290 * @param {DOMWindow} targetWindow
291 * @param {string} targetOrigin
293 setTarget: function(targetWindow, targetOrigin) {
294 this.targetWindow = targetWindow;
295 this.targetOrigin = targetOrigin;
297 for (var i = 0; i < this.deferredMessages_.length; ++i) {
298 this.postMessage(this.deferredMessages_[i]);
300 this.deferredMessages_ = [];
303 postMessage: function(msg) {
304 if (!this.targetWindow) {
305 this.deferredMessages_.push(msg);
309 this.targetWindow.postMessage({
311 channelId: this.channelId,
313 }, this.targetOrigin);
316 handleWindowMessage: function(e) {
317 this.onMessage.dispatch(e.data.payload);
322 * A message channel based on PostMessagePort.
326 function PostMessageChannel() {
327 Channel.apply(this, arguments);
330 PostMessageChannel.prototype = {
331 __proto__: Channel.prototype,
334 connect: function(name) {
335 this.port_ = channelManager.connectToDaemon(name);
336 this.port_.onMessage.addListener(this.onMessage_.bind(this));
341 * Initialize webview content window for postMessage channel.
342 * @param {DOMWindow} webViewContentWindow Content window of the webview.
344 PostMessageChannel.init = function(webViewContentWindow) {
345 webViewContentWindow.postMessage({
346 type: CHANNEL_INIT_MESSAGE
351 * Run in daemon mode and listen for incoming connections. Note that the
352 * current implementation assumes the daemon runs in the hosting page
353 * at the upper layer of the DOM tree. That is, all connect requests go
354 * up the DOM tree instead of going into sub frames.
355 * @param {function(PostMessagePort)} callback Invoked when a connection is
358 PostMessageChannel.runAsDaemon = function(callback) {
359 channelManager.isDaemon = true;
361 var onConnect = function(port) {
364 channelManager.onConnect.addListener(onConnect);
367 return PostMessageChannel;
371 Channel.create = function() {
372 return new PostMessageChannel();