iframerelaytransport.js

// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Contains the iframe relay tranport.
 */


goog.provide('goog.net.xpc.IframeRelayTransport');

goog.require('goog.dom');
goog.require('goog.dom.safe');
goog.require('goog.events');
goog.require('goog.html.SafeHtml');
goog.require('goog.log');
goog.require('goog.log.Level');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CfgFields');
goog.require('goog.net.xpc.Transport');
goog.require('goog.net.xpc.TransportTypes');
goog.require('goog.string');
goog.require('goog.string.Const');
goog.require('goog.userAgent');



/**
 * Iframe relay transport. Creates hidden iframes containing a document
 * from the peer's origin. Data is transferred in the fragment identifier.
 * Therefore the document loaded in the iframes can be served from the
 * browser's cache.
 *
 * @param {goog.net.xpc.CrossPageChannel} channel The channel this
 *     transport belongs to.
 * @param {goog.dom.DomHelper=} opt_domHelper The dom helper to use for finding
 *     the correct window.
 * @constructor
 * @extends {goog.net.xpc.Transport}
 * @final
 */
goog.net.xpc.IframeRelayTransport = function(channel, opt_domHelper) {
  goog.net.xpc.IframeRelayTransport.base(this, 'constructor', opt_domHelper);

  /**
   * The channel this transport belongs to.
   * @type {goog.net.xpc.CrossPageChannel}
   * @private
   */
  this.channel_ = channel;

  /**
   * The URI used to relay data to the peer.
   * @type {string}
   * @private
   */
  this.peerRelayUri_ =
      this.channel_.getConfig()[goog.net.xpc.CfgFields.PEER_RELAY_URI];

  /**
   * The id of the iframe the peer page lives in.
   * @type {string}
   * @private
   */
  this.peerIframeId_ =
      this.channel_.getConfig()[goog.net.xpc.CfgFields.IFRAME_ID];

  if (goog.userAgent.WEBKIT) {
    goog.net.xpc.IframeRelayTransport.startCleanupTimer_();
  }
};
goog.inherits(goog.net.xpc.IframeRelayTransport, goog.net.xpc.Transport);


if (goog.userAgent.WEBKIT) {
  /**
   * Array to keep references to the relay-iframes. Used only if
   * there is no way to detect when the iframes are loaded. In that
   * case the relay-iframes are removed after a timeout.
   * @type {Array.<Object>}
   * @private
   */
  goog.net.xpc.IframeRelayTransport.iframeRefs_ = [];


  /**
   * Interval at which iframes are destroyed.
   * @type {number}
   * @private
   */
  goog.net.xpc.IframeRelayTransport.CLEANUP_INTERVAL_ = 1000;


  /**
   * Time after which a relay-iframe is destroyed.
   * @type {number}
   * @private
   */
  goog.net.xpc.IframeRelayTransport.IFRAME_MAX_AGE_ = 3000;


  /**
   * The cleanup timer id.
   * @type {number}
   * @private
   */
  goog.net.xpc.IframeRelayTransport.cleanupTimer_ = 0;


  /**
   * Starts the cleanup timer.
   * @private
   */
  goog.net.xpc.IframeRelayTransport.startCleanupTimer_ = function() {
    if (!goog.net.xpc.IframeRelayTransport.cleanupTimer_) {
      goog.net.xpc.IframeRelayTransport.cleanupTimer_ = window.setTimeout(
          function() { goog.net.xpc.IframeRelayTransport.cleanup_(); },
          goog.net.xpc.IframeRelayTransport.CLEANUP_INTERVAL_);
    }
  };


  /**
   * Remove all relay-iframes which are older than the maximal age.
   * @param {number=} opt_maxAge The maximal age in milliseconds.
   * @private
   */
  goog.net.xpc.IframeRelayTransport.cleanup_ = function(opt_maxAge) {
    var now = goog.now();
    var maxAge =
        opt_maxAge || goog.net.xpc.IframeRelayTransport.IFRAME_MAX_AGE_;

    while (goog.net.xpc.IframeRelayTransport.iframeRefs_.length &&
           now - goog.net.xpc.IframeRelayTransport.iframeRefs_[0].timestamp >=
           maxAge) {
      var ifr = goog.net.xpc.IframeRelayTransport.iframeRefs_.
          shift().iframeElement;
      goog.dom.removeNode(ifr);
      goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
          'iframe removed');
    }

    goog.net.xpc.IframeRelayTransport.cleanupTimer_ = window.setTimeout(
        goog.net.xpc.IframeRelayTransport.cleanupCb_,
        goog.net.xpc.IframeRelayTransport.CLEANUP_INTERVAL_);
  };


  /**
   * Function which wraps cleanup_().
   * @private
   */
  goog.net.xpc.IframeRelayTransport.cleanupCb_ = function() {
    goog.net.xpc.IframeRelayTransport.cleanup_();
  };
}


/**
 * Maximum sendable size of a payload via a single iframe in IE.
 * @type {number}
 * @private
 */
goog.net.xpc.IframeRelayTransport.IE_PAYLOAD_MAX_SIZE_ = 1800;


/**
 * @typedef {{fragments: !Array.<string>, received: number, expected: number}}
 */
goog.net.xpc.IframeRelayTransport.FragmentInfo;


/**
 * Used to track incoming payload fragments. The implementation can process
 * incoming fragments from several channels at a time, even if data is
 * out-of-order or interleaved.
 *
 * @type {!Object.<string, !goog.net.xpc.IframeRelayTransport.FragmentInfo>}
 * @private
 */
goog.net.xpc.IframeRelayTransport.fragmentMap_ = {};


/**
 * The transport type.
 * @type {number}
 * @override
 */
goog.net.xpc.IframeRelayTransport.prototype.transportType =
    goog.net.xpc.TransportTypes.IFRAME_RELAY;


/**
 * Connects this transport.
 * @override
 */
goog.net.xpc.IframeRelayTransport.prototype.connect = function() {
  if (!this.getWindow()['xpcRelay']) {
    this.getWindow()['xpcRelay'] =
        goog.net.xpc.IframeRelayTransport.receiveMessage_;
  }

  this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP);
};


/**
 * Processes an incoming message.
 *
 * @param {string} channelName The name of the channel.
 * @param {string} frame The raw frame content.
 * @private
 */
goog.net.xpc.IframeRelayTransport.receiveMessage_ =
    function(channelName, frame) {
  var pos = frame.indexOf(':');
  var header = frame.substr(0, pos);
  var payload = frame.substr(pos + 1);

  if (!goog.userAgent.IE || (pos = header.indexOf('|')) == -1) {
    // First, the easy case.
    var service = header;
  } else {
    // There was a fragment id in the header, so this is a message
    // fragment, not a whole message.
    var service = header.substr(0, pos);
    var fragmentIdStr = header.substr(pos + 1);

    // Separate the message id string and the fragment number. Note that
    // there may be a single leading + in the argument to parseInt, but
    // this is harmless.
    pos = fragmentIdStr.indexOf('+');
    var messageIdStr = fragmentIdStr.substr(0, pos);
    var fragmentNum = parseInt(fragmentIdStr.substr(pos + 1), 10);
    var fragmentInfo =
        goog.net.xpc.IframeRelayTransport.fragmentMap_[messageIdStr];
    if (!fragmentInfo) {
      fragmentInfo =
          goog.net.xpc.IframeRelayTransport.fragmentMap_[messageIdStr] =
          {fragments: [], received: 0, expected: 0};
    }

    if (goog.string.contains(fragmentIdStr, '++')) {
      fragmentInfo.expected = fragmentNum + 1;
    }
    fragmentInfo.fragments[fragmentNum] = payload;
    fragmentInfo.received++;

    if (fragmentInfo.received != fragmentInfo.expected) {
      return;
    }

    // We've received all outstanding fragments; combine what we've received
    // into payload and fall out to the call to xpcDeliver.
    payload = fragmentInfo.fragments.join('');
    delete goog.net.xpc.IframeRelayTransport.fragmentMap_[messageIdStr];
  }

  goog.net.xpc.channels[channelName].
      xpcDeliver(service, decodeURIComponent(payload));
};


/**
 * Handles transport service messages (internal signalling).
 * @param {string} payload The message content.
 * @override
 */
goog.net.xpc.IframeRelayTransport.prototype.transportServiceHandler =
    function(payload) {
  if (payload == goog.net.xpc.SETUP) {
    // TODO(user) Safari swallows the SETUP_ACK from the iframe to the
    // container after hitting reload.
    this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP_ACK_);
    this.channel_.notifyConnected();
  }
  else if (payload == goog.net.xpc.SETUP_ACK_) {
    this.channel_.notifyConnected();
  }
};


/**
 * Sends a message.
 *
 * @param {string} service Name of service this the message has to be delivered.
 * @param {string} payload The message content.
 * @override
 */
goog.net.xpc.IframeRelayTransport.prototype.send = function(service, payload) {
  // If we're on IE and the post-encoding payload is large, split it
  // into multiple payloads and send each one separately. Otherwise,
  // just send the whole thing.
  var encodedPayload = encodeURIComponent(payload);
  var encodedLen = encodedPayload.length;
  var maxSize = goog.net.xpc.IframeRelayTransport.IE_PAYLOAD_MAX_SIZE_;

  if (goog.userAgent.IE && encodedLen > maxSize) {
    // A probabilistically-unique string used to link together all fragments
    // in this message.
    var messageIdStr = goog.string.getRandomString();

    for (var startIndex = 0, fragmentNum = 0; startIndex < encodedLen;
         fragmentNum++) {
      var payloadFragment = encodedPayload.substr(startIndex, maxSize);
      startIndex += maxSize;
      var fragmentIdStr =
          messageIdStr + (startIndex >= encodedLen ? '++' : '+') + fragmentNum;
      this.send_(service, payloadFragment, fragmentIdStr);
    }
  } else {
    this.send_(service, encodedPayload);
  }
};


/**
 * Sends an encoded message or message fragment.
 * @param {string} service Name of service this the message has to be delivered.
 * @param {string} encodedPayload The message content, URI encoded.
 * @param {string=} opt_fragmentIdStr If sending a fragment, a string that
 *     identifies the fragment.
 * @private
 */
goog.net.xpc.IframeRelayTransport.prototype.send_ =
    function(service, encodedPayload, opt_fragmentIdStr) {
  // IE requires that we create the onload attribute inline, otherwise the
  // handler is not triggered
  if (goog.userAgent.IE) {
    var div = this.getWindow().document.createElement('div');
    goog.dom.safe.setInnerHtml(div, goog.html.SafeHtml.create('iframe', {
      'onload': goog.string.Const.from('this.xpcOnload()')
    }));
    var ifr = div.childNodes[0];
    div = null;
    ifr['xpcOnload'] = goog.net.xpc.IframeRelayTransport.iframeLoadHandler_;
  } else {
    var ifr = this.getWindow().document.createElement('iframe');

    if (goog.userAgent.WEBKIT) {
      // safari doesn't fire load-events on iframes.
      // keep a reference and remove after a timeout.
      goog.net.xpc.IframeRelayTransport.iframeRefs_.push({
        timestamp: goog.now(),
        iframeElement: ifr
      });
    } else {
      goog.events.listen(ifr, 'load',
                         goog.net.xpc.IframeRelayTransport.iframeLoadHandler_);
    }
  }

  var style = ifr.style;
  style.visibility = 'hidden';
  style.width = ifr.style.height = '0px';
  style.position = 'absolute';

  var url = this.peerRelayUri_;
  url += '#' + this.channel_.name;
  if (this.peerIframeId_) {
    url += ',' + this.peerIframeId_;
  }
  url += '|' + service;
  if (opt_fragmentIdStr) {
    url += '|' + opt_fragmentIdStr;
  }
  url += ':' + encodedPayload;

  ifr.src = url;

  this.getWindow().document.body.appendChild(ifr);

  goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST, 'msg sent: ' + url);
};


/**
 * The iframe load handler. Gets called as method on the iframe element.
 * @private
 * @this Element
 */
goog.net.xpc.IframeRelayTransport.iframeLoadHandler_ = function() {
  goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST, 'iframe-load');
  goog.dom.removeNode(this);
  this.xpcOnload = null;
};


/** @override */
goog.net.xpc.IframeRelayTransport.prototype.disposeInternal = function() {
  goog.net.xpc.IframeRelayTransport.base(this, 'disposeInternal');
  if (goog.userAgent.WEBKIT) {
    goog.net.xpc.IframeRelayTransport.cleanup_(0);
  }
};