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