interframe.js revision cedac228d2dd51db4b79ea1e72c7f249408ee061
1// Copyright 2014 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.
4
5/**
6 * @fileoverview Tools for interframe communication. To use this class, every
7 * window that wants to communicate with its child iframes should enumerate
8 * them using document.getElementsByTagName('iframe'), create an ID to
9 * associate with that iframe, then call cvox.Interframe.sendIdToIFrame
10 * on each of them. Then use cvox.Interframe.sendMessageToIFrame to send
11 * messages to that iframe and cvox.Interframe.addListener to receive
12 * replies. When a reply is received, it will automatically contain the ID of
13 * that iframe as a parameter.
14 *
15 */
16
17goog.provide('cvox.Interframe');
18
19goog.require('cvox.ChromeVoxJSON');
20goog.require('cvox.DomUtil');
21
22/**
23 * @constructor
24 */
25cvox.Interframe = function() {
26};
27
28/**
29 * The prefix of all interframe messages.
30 * @type {string}
31 * @const
32 */
33cvox.Interframe.IF_MSG_PREFIX = 'cvox.INTERFRAME:';
34
35/**
36 * The message used to set the ID of a child frame so that it can send replies
37 * to its parent frame.
38 * @type {string}
39 * @const
40 */
41cvox.Interframe.SET_ID = 'cvox.INTERFRAME_SET_ID';
42
43/**
44 * The ID of this window (relative to its parent farme).
45 * @type {number|string|undefined}
46 */
47cvox.Interframe.id;
48
49/**
50 * Array of functions that have been registered as listeners to interframe
51 * messages send to this window.
52 * @type {Array.<function(Object)>}
53 */
54cvox.Interframe.listeners = [];
55
56/**
57 * Flag for unit testing. When false, skips over iframe.contentWindow check
58 * in sendMessageToIframe. This is needed because in the wild, ChromeVox may
59 * not have access to iframe.contentWindow due to the same-origin security
60 * policy. There is no reason to set this outside of a test.
61 * @type {boolean}
62 */
63cvox.Interframe.allowAccessToIframeContentWindow = true;
64
65/**
66 * Initializes the cvox.Interframe module. (This is called automatically.)
67 */
68cvox.Interframe.init = function() {
69  cvox.Interframe.messageListener = function(event) {
70    if (typeof event.data === 'string' &&
71        event.data.indexOf(cvox.Interframe.IF_MSG_PREFIX) == 0) {
72      var suffix = event.data.substr(cvox.Interframe.IF_MSG_PREFIX.length);
73      var message = /** @type {Object} */ (
74          cvox.ChromeVoxJSON.parse(suffix));
75      if (message['command'] == cvox.Interframe.SET_ID) {
76        cvox.Interframe.id = message['id'];
77      }
78      for (var i = 0, listener; listener = cvox.Interframe.listeners[i]; i++) {
79        listener(message);
80      }
81    }
82    return false;
83  };
84  window.addEventListener('message', cvox.Interframe.messageListener, true);
85};
86
87/**
88 * Unregister the main window event listener. Intended for clean unit testing;
89 * normally there's no reason to call this outside of a test.
90 */
91cvox.Interframe.shutdown = function() {
92  window.removeEventListener('message', cvox.Interframe.messageListener, true);
93};
94
95/**
96 * Register a function to listen to all interframe communication messages.
97 * Messages from a child frame will have a parameter 'id' that you assigned
98 * when you called cvox.Interframe.sendIdToIFrame.
99 * @param {function(Object)} listener The listener function.
100 */
101cvox.Interframe.addListener = function(listener) {
102  cvox.Interframe.listeners.push(listener);
103};
104
105/**
106 * Send a message to another window.
107 * @param {Object} message The message to send.
108 * @param {Window} window The window to receive the message.
109 */
110cvox.Interframe.sendMessageToWindow = function(message, window) {
111  var encodedMessage = cvox.Interframe.IF_MSG_PREFIX +
112      cvox.ChromeVoxJSON.stringify(message, null, null);
113  window.postMessage(encodedMessage, '*');
114};
115
116/**
117 * Send a message to another iframe.
118 * @param {Object} message The message to send. The message must have an 'id'
119 *     parameter in order to be sent.
120 * @param {HTMLIFrameElement} iframe The iframe to send the message to.
121 */
122cvox.Interframe.sendMessageToIFrame = function(message, iframe) {
123  if (cvox.Interframe.allowAccessToIframeContentWindow &&
124      iframe.contentWindow) {
125    cvox.Interframe.sendMessageToWindow(message, iframe.contentWindow);
126    return;
127  }
128
129  // A content script can't access window.parent, but the page can, so
130  // inject a tiny bit of javascript into the page.
131  var encodedMessage = cvox.Interframe.IF_MSG_PREFIX +
132      cvox.ChromeVoxJSON.stringify(message, null, null);
133  var script = document.createElement('script');
134  script.type = 'text/javascript';
135
136  // TODO: Make this logic more like makeNodeReference_ inside api.js
137  // (line 126) so we can use an attribute instead of a classname
138  if (iframe.hasAttribute('id') &&
139      document.getElementById(iframe.id) == iframe) {
140    // Ideally, try to send it based on the iframe's existing id.
141    script.innerHTML =
142        'document.getElementById(decodeURI(\'' +
143        encodeURI(iframe.id) + '\')).contentWindow.postMessage(decodeURI(\'' +
144        encodeURI(encodedMessage) + '\'), \'*\');';
145  } else {
146    // If not, add a style name and send it based on that.
147    var styleName = 'cvox_iframe' + message['id'];
148    if (iframe.className === '') {
149      iframe.className = styleName;
150    } else if (iframe.className.indexOf(styleName) == -1) {
151      iframe.className += ' ' + styleName;
152    }
153
154    script.innerHTML =
155        'document.getElementsByClassName(decodeURI(\'' +
156        encodeURI(styleName) +
157        '\'))[0].contentWindow.postMessage(decodeURI(\'' +
158        encodeURI(encodedMessage) + '\'), \'*\');';
159  }
160
161  // Remove the script so we don't leave any clutter.
162  document.head.appendChild(script);
163  window.setTimeout(function() {
164    document.head.removeChild(script);
165  }, 1000);
166};
167
168/**
169 * Send a message to the parent window of this window, if any. If the parent
170 * assigned this window an ID, sends back the ID in the reply automatically.
171 * @param {Object} message The message to send.
172 */
173cvox.Interframe.sendMessageToParentWindow = function(message) {
174  if (!cvox.Interframe.isIframe()) {
175    return;
176  }
177
178  message['sourceId'] = cvox.Interframe.id;
179  if (window.parent) {
180    cvox.Interframe.sendMessageToWindow(message, window.parent);
181    return;
182  }
183
184  // A content script can't access window.parent, but the page can, so
185  // use window.location.href to execute a simple line of javascript in
186  // the page context.
187  var encodedMessage = cvox.Interframe.IF_MSG_PREFIX +
188      cvox.ChromeVoxJSON.stringify(message, null, null);
189  window.location.href =
190      'javascript:window.parent.postMessage(\'' +
191      encodeURI(encodedMessage) + '\', \'*\');';
192};
193
194/**
195 * Send the given ID to a child iframe.
196 * @param {number|string} id The ID you want to receive in replies from
197 *     this iframe.
198 * @param {HTMLIFrameElement} iframe The iframe to assign.
199 */
200cvox.Interframe.sendIdToIFrame = function(id, iframe) {
201  var message = {'command': cvox.Interframe.SET_ID, 'id': id};
202  cvox.Interframe.sendMessageToIFrame(message, iframe);
203};
204
205/**
206 * Returns true if inside iframe
207 * @return {boolean} true if inside iframe.
208 */
209cvox.Interframe.isIframe = function() {
210  return (window != window.parent);
211};
212
213cvox.Interframe.init();
214