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.
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.
17 goog
.provide('cvox.Interframe');
19 goog
.require('cvox.ChromeVoxJSON');
20 goog
.require('cvox.DomUtil');
25 cvox
.Interframe = function() {
29 * The prefix of all interframe messages.
33 cvox
.Interframe
.IF_MSG_PREFIX
= 'cvox.INTERFRAME:';
36 * The message used to set the ID of a child frame so that it can send replies
37 * to its parent frame.
41 cvox
.Interframe
.SET_ID
= 'cvox.INTERFRAME_SET_ID';
44 * The message used by a child frame to acknowledge an id was set (sent to its
49 cvox
.Interframe
.ACK_SET_ID
= 'cvox.INTERFRAME_ACK_SET_ID';
52 * The ID of this window (relative to its parent farme).
53 * @type {number|string|undefined}
58 * Array of functions that have been registered as listeners to interframe
59 * messages send to this window.
60 * @type {Array<function(Object)>}
62 cvox
.Interframe
.listeners
= [];
65 * Maps an id to a function which gets called when a frame first sends an ack
67 @dict {!Object<number|string, function()>}
70 cvox
.Interframe
.idToCallback_
= {};
73 * Flag for unit testing. When false, skips over iframe.contentWindow check
74 * in sendMessageToIframe. This is needed because in the wild, ChromeVox may
75 * not have access to iframe.contentWindow due to the same-origin security
76 * policy. There is no reason to set this outside of a test.
79 cvox
.Interframe
.allowAccessToIframeContentWindow
= true;
82 * Initializes the cvox.Interframe module. (This is called automatically.)
84 cvox
.Interframe
.init = function() {
85 cvox
.Interframe
.messageListener = function(event
) {
86 if (typeof event
.data
=== 'string' &&
87 event
.data
.indexOf(cvox
.Interframe
.IF_MSG_PREFIX
) == 0) {
88 var suffix
= event
.data
.substr(cvox
.Interframe
.IF_MSG_PREFIX
.length
);
89 var message
= /** @type {Object} */ (
90 cvox
.ChromeVoxJSON
.parse(suffix
));
91 if (message
['command'] == cvox
.Interframe
.SET_ID
) {
92 cvox
.Interframe
.id
= message
['id'];
93 message
['command'] = cvox
.Interframe
.ACK_SET_ID
;
94 cvox
.Interframe
.sendMessageToParentWindow(message
);
95 } else if (message
['command'] == cvox
.Interframe
.ACK_SET_ID
) {
96 cvox
.Interframe
.id
= message
['id'];
97 var callback
= cvox
.Interframe
.idToCallback_
[cvox
.Interframe
.id
];
100 for (var i
= 0, listener
; listener
= cvox
.Interframe
.listeners
[i
]; i
++) {
106 window
.addEventListener('message', cvox
.Interframe
.messageListener
, true);
110 * Unregister the main window event listener. Intended for clean unit testing;
111 * normally there's no reason to call this outside of a test.
113 cvox
.Interframe
.shutdown = function() {
114 window
.removeEventListener('message', cvox
.Interframe
.messageListener
, true);
118 * Register a function to listen to all interframe communication messages.
119 * Messages from a child frame will have a parameter 'id' that you assigned
120 * when you called cvox.Interframe.sendIdToIFrame.
121 * @param {function(Object)} listener The listener function.
123 cvox
.Interframe
.addListener = function(listener
) {
124 cvox
.Interframe
.listeners
.push(listener
);
128 * Send a message to another window.
129 * @param {Object} message The message to send.
130 * @param {Window} window The window to receive the message.
132 cvox
.Interframe
.sendMessageToWindow = function(message
, window
) {
133 var encodedMessage
= cvox
.Interframe
.IF_MSG_PREFIX
+
134 cvox
.ChromeVoxJSON
.stringify(message
, null, null);
135 window
.postMessage(encodedMessage
, '*');
139 * Send a message to another iframe.
140 * @param {Object} message The message to send. The message must have an 'id'
141 * parameter in order to be sent.
142 * @param {HTMLIFrameElement} iframe The iframe to send the message to.
144 cvox
.Interframe
.sendMessageToIFrame = function(message
, iframe
) {
145 if (cvox
.Interframe
.allowAccessToIframeContentWindow
&&
146 iframe
.contentWindow
) {
147 cvox
.Interframe
.sendMessageToWindow(message
, iframe
.contentWindow
);
151 // A content script can't access window.parent, but the page can, so
152 // inject a tiny bit of javascript into the page.
153 var encodedMessage
= cvox
.Interframe
.IF_MSG_PREFIX
+
154 cvox
.ChromeVoxJSON
.stringify(message
, null, null);
155 var script
= document
.createElement('script');
156 script
.type
= 'text/javascript';
158 // TODO: Make this logic more like makeNodeReference_ inside api.js
159 // (line 126) so we can use an attribute instead of a classname
160 if (iframe
.hasAttribute('id') &&
161 document
.getElementById(iframe
.id
) == iframe
) {
162 // Ideally, try to send it based on the iframe's existing id.
164 'document.getElementById(decodeURI(\'' +
165 encodeURI(iframe
.id
) + '\')).contentWindow.postMessage(decodeURI(\'' +
166 encodeURI(encodedMessage
) + '\'), \'*\');';
168 // If not, add a style name and send it based on that.
169 var styleName
= 'cvox_iframe' + message
['id'];
170 if (iframe
.className
=== '') {
171 iframe
.className
= styleName
;
172 } else if (iframe
.className
.indexOf(styleName
) == -1) {
173 iframe
.className
+= ' ' + styleName
;
177 'document.getElementsByClassName(decodeURI(\'' +
178 encodeURI(styleName
) +
179 '\'))[0].contentWindow.postMessage(decodeURI(\'' +
180 encodeURI(encodedMessage
) + '\'), \'*\');';
183 // Remove the script so we don't leave any clutter.
184 document
.head
.appendChild(script
);
185 window
.setTimeout(function() {
186 document
.head
.removeChild(script
);
191 * Send a message to the parent window of this window, if any. If the parent
192 * assigned this window an ID, sends back the ID in the reply automatically.
193 * @param {Object} message The message to send.
195 cvox
.Interframe
.sendMessageToParentWindow = function(message
) {
196 if (!cvox
.Interframe
.isIframe()) {
200 message
['sourceId'] = cvox
.Interframe
.id
;
202 cvox
.Interframe
.sendMessageToWindow(message
, window
.parent
);
206 // A content script can't access window.parent, but the page can, so
207 // use window.location.href to execute a simple line of javascript in
209 var encodedMessage
= cvox
.Interframe
.IF_MSG_PREFIX
+
210 cvox
.ChromeVoxJSON
.stringify(message
, null, null);
211 window
.location
.href
=
212 'javascript:window.parent.postMessage(\'' +
213 encodeURI(encodedMessage
) + '\', \'*\');';
217 * Send the given ID to a child iframe.
218 * @param {number|string} id The ID you want to receive in replies from
220 * @param {HTMLIFrameElement} iframe The iframe to assign.
221 * @param {function()=} opt_callback Called when a ack msg arrives from the
224 cvox
.Interframe
.sendIdToIFrame = function(id
, iframe
, opt_callback
) {
226 cvox
.Interframe
.idToCallback_
[id
] = opt_callback
;
228 var message
= {'command': cvox
.Interframe
.SET_ID
, 'id': id
};
229 cvox
.Interframe
.sendMessageToIFrame(message
, iframe
);
233 * Returns true if inside iframe
234 * @return {boolean} true if inside iframe.
236 cvox
.Interframe
.isIframe = function() {
237 return (window
!= window
.parent
);
240 cvox
.Interframe
.init();