Home | History | Annotate | Download | only in common
      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 
     17 goog.provide('cvox.Interframe');
     18 
     19 goog.require('cvox.ChromeVoxJSON');
     20 goog.require('cvox.DomUtil');
     21 
     22 /**
     23  * @constructor
     24  */
     25 cvox.Interframe = function() {
     26 };
     27 
     28 /**
     29  * The prefix of all interframe messages.
     30  * @type {string}
     31  * @const
     32  */
     33 cvox.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  */
     41 cvox.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  */
     47 cvox.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  */
     54 cvox.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  */
     63 cvox.Interframe.allowAccessToIframeContentWindow = true;
     64 
     65 /**
     66  * Initializes the cvox.Interframe module. (This is called automatically.)
     67  */
     68 cvox.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  */
     91 cvox.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  */
    101 cvox.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  */
    110 cvox.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  */
    122 cvox.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  */
    173 cvox.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  */
    200 cvox.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  */
    209 cvox.Interframe.isIframe = function() {
    210   return (window != window.parent);
    211 };
    212 
    213 cvox.Interframe.init();
    214