Update wmts-hidpi, add nicer-api-docs

This commit is contained in:
Andreas Hocevar
2014-05-06 13:02:46 -05:00
parent b3ac1afd00
commit 1e25fc5585
2239 changed files with 3726515 additions and 37010 deletions

View File

@@ -0,0 +1,805 @@
// 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 Provides the class CrossPageChannel, the main class in
* goog.net.xpc.
*
* @see ../../demos/xpc/index.html
*/
goog.provide('goog.net.xpc.CrossPageChannel');
goog.require('goog.Disposable');
goog.require('goog.Uri');
goog.require('goog.async.Deferred');
goog.require('goog.async.Delay');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.EventHandler');
goog.require('goog.json');
goog.require('goog.messaging.AbstractChannel');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.FrameElementMethodTransport');
goog.require('goog.net.xpc.IframePollingTransport');
goog.require('goog.net.xpc.IframeRelayTransport');
goog.require('goog.net.xpc.NativeMessagingTransport');
goog.require('goog.net.xpc.NixTransport');
goog.require('goog.net.xpc.Transport');
goog.require('goog.userAgent');
/**
* A communication channel between two documents from different domains.
* Provides asynchronous messaging.
*
* @param {Object} cfg Channel configuration object.
* @param {goog.dom.DomHelper=} opt_domHelper The optional dom helper to
* use for looking up elements in the dom.
* @constructor
* @extends {goog.messaging.AbstractChannel}
*/
goog.net.xpc.CrossPageChannel = function(cfg, opt_domHelper) {
goog.base(this);
for (var i = 0, uriField; uriField = goog.net.xpc.UriCfgFields[i]; i++) {
if (uriField in cfg && !/^https?:\/\//.test(cfg[uriField])) {
throw Error('URI ' + cfg[uriField] + ' is invalid for field ' + uriField);
}
}
/**
* The configuration for this channel.
* @type {Object}
* @private
*/
this.cfg_ = cfg;
/**
* The name of the channel.
* @type {string}
*/
this.name = this.cfg_[goog.net.xpc.CfgFields.CHANNEL_NAME] ||
goog.net.xpc.getRandomString(10);
/**
* The dom helper to use for accessing the dom.
* @type {goog.dom.DomHelper}
* @private
*/
this.domHelper_ = opt_domHelper || goog.dom.getDomHelper();
/**
* Collects deferred function calls which will be made once the connection
* has been fully set up.
* @type {!Array.<function()>}
* @private
*/
this.deferredDeliveries_ = [];
/**
* An event handler used to listen for load events on peer iframes.
* @type {!goog.events.EventHandler}
* @private
*/
this.peerLoadHandler_ = new goog.events.EventHandler(this);
// If LOCAL_POLL_URI or PEER_POLL_URI is not available, try using
// robots.txt from that host.
cfg[goog.net.xpc.CfgFields.LOCAL_POLL_URI] =
cfg[goog.net.xpc.CfgFields.LOCAL_POLL_URI] ||
goog.uri.utils.getHost(this.domHelper_.getWindow().location.href) +
'/robots.txt';
// PEER_URI is sometimes undefined in tests.
cfg[goog.net.xpc.CfgFields.PEER_POLL_URI] =
cfg[goog.net.xpc.CfgFields.PEER_POLL_URI] ||
goog.uri.utils.getHost(cfg[goog.net.xpc.CfgFields.PEER_URI] || '') +
'/robots.txt';
goog.net.xpc.channels[this.name] = this;
goog.events.listen(window, 'unload',
goog.net.xpc.CrossPageChannel.disposeAll_);
goog.log.info(goog.net.xpc.logger, 'CrossPageChannel created: ' + this.name);
};
goog.inherits(goog.net.xpc.CrossPageChannel, goog.messaging.AbstractChannel);
/**
* Regexp for escaping service names.
* @type {RegExp}
* @private
*/
goog.net.xpc.CrossPageChannel.TRANSPORT_SERVICE_ESCAPE_RE_ =
new RegExp('^%*' + goog.net.xpc.TRANSPORT_SERVICE_ + '$');
/**
* Regexp for unescaping service names.
* @type {RegExp}
* @private
*/
goog.net.xpc.CrossPageChannel.TRANSPORT_SERVICE_UNESCAPE_RE_ =
new RegExp('^%+' + goog.net.xpc.TRANSPORT_SERVICE_ + '$');
/**
* A delay between the transport reporting as connected and the calling of the
* connection callback. Sometimes used to paper over timing vulnerabilities.
* @type {goog.async.Delay}
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.connectionDelay_ = null;
/**
* A deferred which is set to non-null while a peer iframe is being created
* but has not yet thrown its load event, and which fires when that load event
* arrives.
* @type {goog.async.Deferred}
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.peerWindowDeferred_ = null;
/**
* The transport.
* @type {goog.net.xpc.Transport?}
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.transport_ = null;
/**
* The channel state.
* @type {number}
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.state_ =
goog.net.xpc.ChannelStates.NOT_CONNECTED;
/**
* @override
* @return {boolean} Whether the channel is connected.
*/
goog.net.xpc.CrossPageChannel.prototype.isConnected = function() {
return this.state_ == goog.net.xpc.ChannelStates.CONNECTED;
};
/**
* Reference to the window-object of the peer page.
* @type {Object}
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.peerWindowObject_ = null;
/**
* Reference to the iframe-element.
* @type {Object}
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.iframeElement_ = null;
/**
* Returns the configuration object for this channel.
* Package private. Do not call from outside goog.net.xpc.
*
* @return {Object} The configuration object for this channel.
*/
goog.net.xpc.CrossPageChannel.prototype.getConfig = function() {
return this.cfg_;
};
/**
* Returns a reference to the iframe-element.
* Package private. Do not call from outside goog.net.xpc.
*
* @return {Object} A reference to the iframe-element.
*/
goog.net.xpc.CrossPageChannel.prototype.getIframeElement = function() {
return this.iframeElement_;
};
/**
* Sets the window object the foreign document resides in.
*
* @param {Object} peerWindowObject The window object of the peer.
*/
goog.net.xpc.CrossPageChannel.prototype.setPeerWindowObject =
function(peerWindowObject) {
this.peerWindowObject_ = peerWindowObject;
};
/**
* Returns the window object the foreign document resides in.
* Package private. Do not call from outside goog.net.xpc.
*
* @return {Object} The window object of the peer.
*/
goog.net.xpc.CrossPageChannel.prototype.getPeerWindowObject = function() {
return this.peerWindowObject_;
};
/**
* Determines whether the peer window is available (e.g. not closed).
* Package private. Do not call from outside goog.net.xpc.
*
* @return {boolean} Whether the peer window is available.
*/
goog.net.xpc.CrossPageChannel.prototype.isPeerAvailable = function() {
// NOTE(user): This check is not reliable in IE, where a document in an
// iframe does not get unloaded when removing the iframe element from the DOM.
// TODO(user): Find something that works in IE as well.
// NOTE(user): "!this.peerWindowObject_.closed" evaluates to 'false' in IE9
// sometimes even though typeof(this.peerWindowObject_.closed) is boolean and
// this.peerWindowObject_.closed evaluates to 'false'. Casting it to a Boolean
// results in sane evaluation. When this happens, it's in the inner iframe
// when querying its parent's 'closed' status. Note that this is a different
// case than mibuerge@'s note above.
try {
return !!this.peerWindowObject_ && !Boolean(this.peerWindowObject_.closed);
} catch (e) {
// If the window is closing, an error may be thrown.
return false;
}
};
/**
* Determine which transport type to use for this channel / useragent.
* @return {goog.net.xpc.TransportTypes|undefined} The best transport type.
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.determineTransportType_ = function() {
var transportType;
if (goog.isFunction(document.postMessage) ||
goog.isFunction(window.postMessage) ||
// IE8 supports window.postMessage, but
// typeof window.postMessage returns "object"
(goog.userAgent.IE && window.postMessage)) {
transportType = goog.net.xpc.TransportTypes.NATIVE_MESSAGING;
} else if (goog.userAgent.GECKO) {
transportType = goog.net.xpc.TransportTypes.FRAME_ELEMENT_METHOD;
} else if (goog.userAgent.IE &&
this.cfg_[goog.net.xpc.CfgFields.PEER_RELAY_URI]) {
transportType = goog.net.xpc.TransportTypes.IFRAME_RELAY;
} else if (goog.userAgent.IE && goog.net.xpc.NixTransport.isNixSupported()) {
transportType = goog.net.xpc.TransportTypes.NIX;
} else {
transportType = goog.net.xpc.TransportTypes.IFRAME_POLLING;
}
return transportType;
};
/**
* Creates the transport for this channel. Chooses from the available
* transport based on the user agent and the configuration.
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.createTransport_ = function() {
// return, if the transport has already been created
if (this.transport_) {
return;
}
if (!this.cfg_[goog.net.xpc.CfgFields.TRANSPORT]) {
this.cfg_[goog.net.xpc.CfgFields.TRANSPORT] =
this.determineTransportType_();
}
switch (this.cfg_[goog.net.xpc.CfgFields.TRANSPORT]) {
case goog.net.xpc.TransportTypes.NATIVE_MESSAGING:
var protocolVersion = this.cfg_[
goog.net.xpc.CfgFields.NATIVE_TRANSPORT_PROTOCOL_VERSION] || 2;
this.transport_ = new goog.net.xpc.NativeMessagingTransport(
this,
this.cfg_[goog.net.xpc.CfgFields.PEER_HOSTNAME],
this.domHelper_,
!!this.cfg_[goog.net.xpc.CfgFields.ONE_SIDED_HANDSHAKE],
protocolVersion);
break;
case goog.net.xpc.TransportTypes.NIX:
this.transport_ = new goog.net.xpc.NixTransport(this, this.domHelper_);
break;
case goog.net.xpc.TransportTypes.FRAME_ELEMENT_METHOD:
this.transport_ =
new goog.net.xpc.FrameElementMethodTransport(this, this.domHelper_);
break;
case goog.net.xpc.TransportTypes.IFRAME_RELAY:
this.transport_ =
new goog.net.xpc.IframeRelayTransport(this, this.domHelper_);
break;
case goog.net.xpc.TransportTypes.IFRAME_POLLING:
this.transport_ =
new goog.net.xpc.IframePollingTransport(this, this.domHelper_);
break;
}
if (this.transport_) {
goog.log.info(goog.net.xpc.logger,
'Transport created: ' + this.transport_.getName());
} else {
throw Error('CrossPageChannel: No suitable transport found!');
}
};
/**
* Returns the transport type in use for this channel.
* @return {number} Transport-type identifier.
*/
goog.net.xpc.CrossPageChannel.prototype.getTransportType = function() {
return this.transport_.getType();
};
/**
* Returns the tranport name in use for this channel.
* @return {string} The transport name.
*/
goog.net.xpc.CrossPageChannel.prototype.getTransportName = function() {
return this.transport_.getName();
};
/**
* @return {Object} Configuration-object to be used by the peer to
* initialize the channel.
*/
goog.net.xpc.CrossPageChannel.prototype.getPeerConfiguration = function() {
var peerCfg = {};
peerCfg[goog.net.xpc.CfgFields.CHANNEL_NAME] = this.name;
peerCfg[goog.net.xpc.CfgFields.TRANSPORT] =
this.cfg_[goog.net.xpc.CfgFields.TRANSPORT];
peerCfg[goog.net.xpc.CfgFields.ONE_SIDED_HANDSHAKE] =
this.cfg_[goog.net.xpc.CfgFields.ONE_SIDED_HANDSHAKE];
if (this.cfg_[goog.net.xpc.CfgFields.LOCAL_RELAY_URI]) {
peerCfg[goog.net.xpc.CfgFields.PEER_RELAY_URI] =
this.cfg_[goog.net.xpc.CfgFields.LOCAL_RELAY_URI];
}
if (this.cfg_[goog.net.xpc.CfgFields.LOCAL_POLL_URI]) {
peerCfg[goog.net.xpc.CfgFields.PEER_POLL_URI] =
this.cfg_[goog.net.xpc.CfgFields.LOCAL_POLL_URI];
}
if (this.cfg_[goog.net.xpc.CfgFields.PEER_POLL_URI]) {
peerCfg[goog.net.xpc.CfgFields.LOCAL_POLL_URI] =
this.cfg_[goog.net.xpc.CfgFields.PEER_POLL_URI];
}
var role = this.cfg_[goog.net.xpc.CfgFields.ROLE];
if (role) {
peerCfg[goog.net.xpc.CfgFields.ROLE] =
role == goog.net.xpc.CrossPageChannelRole.INNER ?
goog.net.xpc.CrossPageChannelRole.OUTER :
goog.net.xpc.CrossPageChannelRole.INNER;
}
return peerCfg;
};
/**
* Creates the iframe containing the peer page in a specified parent element.
* This method does not connect the channel, connect() still has to be called
* separately.
*
* @param {!Element} parentElm The container element the iframe is appended to.
* @param {Function=} opt_configureIframeCb If present, this function gets
* called with the iframe element as parameter to allow setting properties
* on it before it gets added to the DOM. If absent, the iframe's width and
* height are set to '100%'.
* @param {boolean=} opt_addCfgParam Whether to add the peer configuration as
* URL parameter (default: true).
* @return {!HTMLIFrameElement} The iframe element.
*/
goog.net.xpc.CrossPageChannel.prototype.createPeerIframe = function(
parentElm, opt_configureIframeCb, opt_addCfgParam) {
goog.log.info(goog.net.xpc.logger, 'createPeerIframe()');
var iframeId = this.cfg_[goog.net.xpc.CfgFields.IFRAME_ID];
if (!iframeId) {
// Create a randomized ID for the iframe element to avoid
// bfcache-related issues.
iframeId = this.cfg_[goog.net.xpc.CfgFields.IFRAME_ID] =
'xpcpeer' + goog.net.xpc.getRandomString(4);
}
// TODO(user) Opera creates a history-entry when creating an iframe
// programmatically as follows. Find a way which avoids this.
var iframeElm = goog.dom.getDomHelper(parentElm).createElement('IFRAME');
iframeElm.id = iframeElm.name = iframeId;
if (opt_configureIframeCb) {
opt_configureIframeCb(iframeElm);
} else {
iframeElm.style.width = iframeElm.style.height = '100%';
}
this.cleanUpIncompleteConnection_();
this.peerWindowDeferred_ =
new goog.async.Deferred(undefined, this);
var peerUri = this.getPeerUri(opt_addCfgParam);
this.peerLoadHandler_.listenOnce(iframeElm, 'load',
this.peerWindowDeferred_.callback, false, this.peerWindowDeferred_);
if (goog.userAgent.GECKO || goog.userAgent.WEBKIT) {
// Appending the iframe in a timeout to avoid a weird fastback issue, which
// is present in Safari and Gecko.
window.setTimeout(
goog.bind(function() {
parentElm.appendChild(iframeElm);
iframeElm.src = peerUri.toString();
goog.log.info(goog.net.xpc.logger,
'peer iframe created (' + iframeId + ')');
}, this), 1);
} else {
iframeElm.src = peerUri.toString();
parentElm.appendChild(iframeElm);
goog.log.info(goog.net.xpc.logger,
'peer iframe created (' + iframeId + ')');
}
return /** @type {!HTMLIFrameElement} */ (iframeElm);
};
/**
* Clean up after any incomplete attempt to establish and connect to a peer
* iframe.
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.cleanUpIncompleteConnection_ =
function() {
if (this.peerWindowDeferred_) {
this.peerWindowDeferred_.cancel();
this.peerWindowDeferred_ = null;
}
this.deferredDeliveries_.length = 0;
this.peerLoadHandler_.removeAll();
};
/**
* Returns the peer URI, with an optional URL parameter for configuring the peer
* window.
*
* @param {boolean=} opt_addCfgParam Whether to add the peer configuration as
* URL parameter (default: true).
* @return {!goog.Uri} The peer URI.
*/
goog.net.xpc.CrossPageChannel.prototype.getPeerUri = function(opt_addCfgParam) {
var peerUri = this.cfg_[goog.net.xpc.CfgFields.PEER_URI];
if (goog.isString(peerUri)) {
peerUri = this.cfg_[goog.net.xpc.CfgFields.PEER_URI] =
new goog.Uri(peerUri);
}
// Add the channel configuration used by the peer as URL parameter.
if (opt_addCfgParam !== false) {
peerUri.setParameterValue('xpc',
goog.json.serialize(
this.getPeerConfiguration()));
}
return peerUri;
};
/**
* Initiates connecting the channel. When this method is called, all the
* information needed to connect the channel has to be available.
*
* @override
* @param {Function=} opt_connectCb The function to be called when the
* channel has been connected and is ready to be used.
*/
goog.net.xpc.CrossPageChannel.prototype.connect = function(opt_connectCb) {
this.connectCb_ = opt_connectCb || goog.nullFunction;
// If we know of a peer window whose creation has been requested but is not
// complete, peerWindowDeferred_ will be non-null, and we should block on it.
if (this.peerWindowDeferred_) {
this.peerWindowDeferred_.addCallback(this.continueConnection_);
} else {
this.continueConnection_();
}
};
/**
* Continues the connection process once we're as sure as we can be that the
* peer iframe has been created.
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.continueConnection_ = function() {
goog.log.info(goog.net.xpc.logger, 'continueConnection_()');
this.peerWindowDeferred_ = null;
if (this.cfg_[goog.net.xpc.CfgFields.IFRAME_ID]) {
this.iframeElement_ = this.domHelper_.getElement(
this.cfg_[goog.net.xpc.CfgFields.IFRAME_ID]);
}
if (this.iframeElement_) {
var winObj = this.iframeElement_.contentWindow;
// accessing the window using contentWindow doesn't work in safari
if (!winObj) {
winObj = window.frames[this.cfg_[goog.net.xpc.CfgFields.IFRAME_ID]];
}
this.setPeerWindowObject(winObj);
}
// if the peer window object has not been set at this point, we assume
// being in an iframe and the channel is meant to be to the containing page
if (!this.peerWindowObject_) {
// throw an error if we are in the top window (== not in an iframe)
if (window == window.top) {
throw Error(
"CrossPageChannel: Can't connect, peer window-object not set.");
} else {
this.setPeerWindowObject(window.parent);
}
}
this.createTransport_();
this.transport_.connect();
// Now we run any deferred deliveries collected while connection was deferred.
while (this.deferredDeliveries_.length > 0) {
this.deferredDeliveries_.shift()();
}
};
/**
* Closes the channel.
*/
goog.net.xpc.CrossPageChannel.prototype.close = function() {
this.cleanUpIncompleteConnection_();
this.state_ = goog.net.xpc.ChannelStates.CLOSED;
goog.dispose(this.transport_);
this.transport_ = null;
this.connectCb_ = null;
goog.dispose(this.connectionDelay_);
this.connectionDelay_ = null;
goog.log.info(goog.net.xpc.logger, 'Channel "' + this.name + '" closed');
};
/**
* Package-private.
* Called by the transport when the channel is connected.
* @param {number=} opt_delay Delay this number of milliseconds before calling
* the connection callback. Usage is discouraged, but can be used to paper
* over timing vulnerabilities when there is no alternative.
*/
goog.net.xpc.CrossPageChannel.prototype.notifyConnected = function(opt_delay) {
if (this.isConnected() ||
(this.connectionDelay_ && this.connectionDelay_.isActive())) {
return;
}
this.state_ = goog.net.xpc.ChannelStates.CONNECTED;
goog.log.info(goog.net.xpc.logger, 'Channel "' + this.name + '" connected');
goog.dispose(this.connectionDelay_);
if (opt_delay) {
this.connectionDelay_ =
new goog.async.Delay(this.connectCb_, opt_delay);
this.connectionDelay_.start();
} else {
this.connectionDelay_ = null;
this.connectCb_();
}
};
/**
* Alias for notifyConected, for backward compatibility reasons.
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.notifyConnected_ =
goog.net.xpc.CrossPageChannel.prototype.notifyConnected;
/**
* Called by the transport in case of an unrecoverable failure.
* Package private. Do not call from outside goog.net.xpc.
*/
goog.net.xpc.CrossPageChannel.prototype.notifyTransportError = function() {
goog.log.info(goog.net.xpc.logger, 'Transport Error');
this.close();
};
/** @override */
goog.net.xpc.CrossPageChannel.prototype.send = function(serviceName, payload) {
if (!this.isConnected()) {
goog.log.error(goog.net.xpc.logger, 'Can\'t send. Channel not connected.');
return;
}
// Check if the peer is still around.
if (!this.isPeerAvailable()) {
goog.log.error(goog.net.xpc.logger, 'Peer has disappeared.');
this.close();
return;
}
if (goog.isObject(payload)) {
payload = goog.json.serialize(payload);
}
// Partially URL-encode the service name because some characters (: and |) are
// used as delimiters for some transports, and we want to allow those
// characters in service names.
this.transport_.send(this.escapeServiceName_(serviceName), payload);
};
/**
* Delivers messages to the appropriate service-handler. Named xpcDeliver to
* avoid name conflict with {@code deliver} function in superclass
* goog.messaging.AbstractChannel.
*
* Package private. Do not call from outside goog.net.xpc.
*
* @param {string} serviceName The name of the port.
* @param {string} payload The payload.
* @param {string=} opt_origin An optional origin for the message, where the
* underlying transport makes that available. If this is specified, and
* the PEER_HOSTNAME parameter was provided, they must match or the message
* will be rejected.
*/
goog.net.xpc.CrossPageChannel.prototype.xpcDeliver = function(
serviceName, payload, opt_origin) {
// This check covers the very rare (but producable) case where the inner frame
// becomes ready and sends its setup message while the outer frame is
// deferring its connect method waiting for the inner frame to be ready. The
// resulting deferral ensures the message will not be processed until the
// channel is fully configured.
if (this.peerWindowDeferred_) {
this.deferredDeliveries_.push(
goog.bind(this.xpcDeliver, this, serviceName, payload, opt_origin));
return;
}
// Check whether the origin of the message is as expected.
if (!this.isMessageOriginAcceptable_(opt_origin)) {
goog.log.warning(goog.net.xpc.logger,
'Message received from unapproved origin "' +
opt_origin + '" - rejected.');
return;
}
if (this.isDisposed()) {
goog.log.warning(goog.net.xpc.logger,
'CrossPageChannel::xpcDeliver(): Disposed.');
} else if (!serviceName ||
serviceName == goog.net.xpc.TRANSPORT_SERVICE_) {
this.transport_.transportServiceHandler(payload);
} else {
// only deliver messages if connected
if (this.isConnected()) {
this.deliver(this.unescapeServiceName_(serviceName), payload);
} else {
goog.log.info(goog.net.xpc.logger,
'CrossPageChannel::xpcDeliver(): Not connected.');
}
}
};
/**
* Escape the user-provided service name for sending across the channel. This
* URL-encodes certain special characters so they don't conflict with delimiters
* used by some of the transports, and adds a special prefix if the name
* conflicts with the reserved transport service name.
*
* This is the opposite of {@link #unescapeServiceName_}.
*
* @param {string} name The name of the service to escape.
* @return {string} The escaped service name.
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.escapeServiceName_ = function(name) {
if (goog.net.xpc.CrossPageChannel.TRANSPORT_SERVICE_ESCAPE_RE_.test(name)) {
name = '%' + name;
}
return name.replace(/[%:|]/g, encodeURIComponent);
};
/**
* Unescape the escaped service name that was sent across the channel. This is
* the opposite of {@link #escapeServiceName_}.
*
* @param {string} name The name of the service to unescape.
* @return {string} The unescaped service name.
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.unescapeServiceName_ = function(name) {
name = name.replace(/%[0-9a-f]{2}/gi, decodeURIComponent);
if (goog.net.xpc.CrossPageChannel.TRANSPORT_SERVICE_UNESCAPE_RE_.test(name)) {
return name.substring(1);
} else {
return name;
}
};
/**
* Returns the role of this channel (either inner or outer).
* @return {number} The role of this channel.
*/
goog.net.xpc.CrossPageChannel.prototype.getRole = function() {
var role = this.cfg_[goog.net.xpc.CfgFields.ROLE];
if (role) {
return role;
} else {
return window.parent == this.peerWindowObject_ ?
goog.net.xpc.CrossPageChannelRole.INNER :
goog.net.xpc.CrossPageChannelRole.OUTER;
}
};
/**
* Returns whether an incoming message with the given origin is acceptable.
* If an incoming request comes with a specified (non-empty) origin, and the
* PEER_HOSTNAME config parameter has also been provided, the two must match,
* or the message is unacceptable.
* @param {string=} opt_origin The origin associated with the incoming message.
* @return {boolean} Whether the message is acceptable.
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.isMessageOriginAcceptable_ = function(
opt_origin) {
var peerHostname = this.cfg_[goog.net.xpc.CfgFields.PEER_HOSTNAME];
return goog.string.isEmptySafe(opt_origin) ||
goog.string.isEmptySafe(peerHostname) ||
opt_origin == this.cfg_[goog.net.xpc.CfgFields.PEER_HOSTNAME];
};
/** @override */
goog.net.xpc.CrossPageChannel.prototype.disposeInternal = function() {
this.close();
this.peerWindowObject_ = null;
this.iframeElement_ = null;
delete goog.net.xpc.channels[this.name];
goog.dispose(this.peerLoadHandler_);
delete this.peerLoadHandler_;
goog.base(this, 'disposeInternal');
};
/**
* Disposes all channels.
* @private
*/
goog.net.xpc.CrossPageChannel.disposeAll_ = function() {
for (var name in goog.net.xpc.channels) {
goog.dispose(goog.net.xpc.channels[name]);
}
};

View File

@@ -0,0 +1,30 @@
// 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 Provides the enum for the role of the CrossPageChannel.
*
*/
goog.provide('goog.net.xpc.CrossPageChannelRole');
/**
* The role of the peer.
* @enum {number}
*/
goog.net.xpc.CrossPageChannelRole = {
OUTER: 0,
INNER: 1
};

View File

@@ -0,0 +1,254 @@
// 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 frame element method transport for cross-domain
* communication. It exploits the fact that FF lets a page in an
* iframe call a method on the iframe-element it is contained in, even if the
* containing page is from a different domain.
*
*/
goog.provide('goog.net.xpc.FrameElementMethodTransport');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.Transport');
/**
* Frame-element method transport.
*
* Firefox allows a document within an iframe to call methods on the
* iframe-element added by the containing document.
* NOTE(user): Tested in all FF versions starting from 1.0
*
* @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}
*/
goog.net.xpc.FrameElementMethodTransport = function(channel, opt_domHelper) {
goog.base(this, opt_domHelper);
/**
* The channel this transport belongs to.
* @type {goog.net.xpc.CrossPageChannel}
* @private
*/
this.channel_ = channel;
// To transfer messages, this transport basically uses normal function calls,
// which are synchronous. To avoid endless recursion, the delivery has to
// be artificially made asynchronous.
/**
* Array for queued messages.
* @type {Array}
* @private
*/
this.queue_ = [];
/**
* Callback function which wraps deliverQueued_.
* @type {Function}
* @private
*/
this.deliverQueuedCb_ = goog.bind(this.deliverQueued_, this);
};
goog.inherits(goog.net.xpc.FrameElementMethodTransport, goog.net.xpc.Transport);
/**
* The transport type.
* @type {number}
* @protected
* @override
*/
goog.net.xpc.FrameElementMethodTransport.prototype.transportType =
goog.net.xpc.TransportTypes.FRAME_ELEMENT_METHOD;
/**
* Flag used to enforce asynchronous messaging semantics.
* @type {boolean}
* @private
*/
goog.net.xpc.FrameElementMethodTransport.prototype.recursive_ = false;
/**
* Timer used to enforce asynchronous message delivery.
* @type {number}
* @private
*/
goog.net.xpc.FrameElementMethodTransport.prototype.timer_ = 0;
/**
* Holds the function to send messages to the peer
* (once it becomes available).
* @type {Function}
* @private
*/
goog.net.xpc.FrameElementMethodTransport.outgoing_ = null;
/**
* Connect this transport.
* @override
*/
goog.net.xpc.FrameElementMethodTransport.prototype.connect = function() {
if (this.channel_.getRole() == goog.net.xpc.CrossPageChannelRole.OUTER) {
// get shortcut to iframe-element
this.iframeElm_ = this.channel_.getIframeElement();
// add the gateway function to the iframe-element
// (to be called by the peer)
this.iframeElm_['XPC_toOuter'] = goog.bind(this.incoming_, this);
// at this point we just have to wait for a notification from the peer...
} else {
this.attemptSetup_();
}
};
/**
* Only used from within an iframe. Attempts to attach the method
* to be used for sending messages by the containing document. Has to
* wait until the containing document has finished. Therefore calls
* itself in a timeout if not successful.
* @private
*/
goog.net.xpc.FrameElementMethodTransport.prototype.attemptSetup_ = function() {
var retry = true;
/** @preserveTry */
try {
if (!this.iframeElm_) {
// throws security exception when called too early
this.iframeElm_ = this.getWindow().frameElement;
}
// check if iframe-element and the gateway-function to the
// outer-frame are present
// TODO(user) Make sure the following code doesn't throw any exceptions
if (this.iframeElm_ && this.iframeElm_['XPC_toOuter']) {
// get a reference to the gateway function
this.outgoing_ = this.iframeElm_['XPC_toOuter'];
// attach the gateway function the other document will use
this.iframeElm_['XPC_toOuter']['XPC_toInner'] =
goog.bind(this.incoming_, this);
// stop retrying
retry = false;
// notify outer frame
this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP_ACK_);
// notify channel that the transport is ready
this.channel_.notifyConnected();
}
}
catch (e) {
goog.log.error(goog.net.xpc.logger,
'exception caught while attempting setup: ' + e);
}
// retry necessary?
if (retry) {
if (!this.attemptSetupCb_) {
this.attemptSetupCb_ = goog.bind(this.attemptSetup_, this);
}
this.getWindow().setTimeout(this.attemptSetupCb_, 100);
}
};
/**
* Handles transport service messages.
* @param {string} payload The message content.
* @override
*/
goog.net.xpc.FrameElementMethodTransport.prototype.transportServiceHandler =
function(payload) {
if (this.channel_.getRole() == goog.net.xpc.CrossPageChannelRole.OUTER &&
!this.channel_.isConnected() && payload == goog.net.xpc.SETUP_ACK_) {
// get a reference to the gateway function
this.outgoing_ = this.iframeElm_['XPC_toOuter']['XPC_toInner'];
// notify the channel we're ready
this.channel_.notifyConnected();
} else {
throw Error('Got unexpected transport message.');
}
};
/**
* Process incoming message.
* @param {string} serviceName The name of the service the message is to be
* delivered to.
* @param {string} payload The message to process.
* @private
*/
goog.net.xpc.FrameElementMethodTransport.prototype.incoming_ =
function(serviceName, payload) {
if (!this.recursive_ && this.queue_.length == 0) {
this.channel_.xpcDeliver(serviceName, payload);
}
else {
this.queue_.push({serviceName: serviceName, payload: payload});
if (this.queue_.length == 1) {
this.timer_ = this.getWindow().setTimeout(this.deliverQueuedCb_, 1);
}
}
};
/**
* Delivers queued messages.
* @private
*/
goog.net.xpc.FrameElementMethodTransport.prototype.deliverQueued_ =
function() {
while (this.queue_.length) {
var msg = this.queue_.shift();
this.channel_.xpcDeliver(msg.serviceName, msg.payload);
}
};
/**
* Send a message
* @param {string} service The name off the service the message is to be
* delivered to.
* @param {string} payload The message content.
* @override
*/
goog.net.xpc.FrameElementMethodTransport.prototype.send =
function(service, payload) {
this.recursive_ = true;
this.outgoing_(service, payload);
this.recursive_ = false;
};
/** @override */
goog.net.xpc.FrameElementMethodTransport.prototype.disposeInternal =
function() {
goog.net.xpc.FrameElementMethodTransport.superClass_.disposeInternal.call(
this);
this.outgoing_ = null;
this.iframeElm_ = null;
};

View File

@@ -0,0 +1,921 @@
// 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 polling transport.
*/
goog.provide('goog.net.xpc.IframePollingTransport');
goog.provide('goog.net.xpc.IframePollingTransport.Receiver');
goog.provide('goog.net.xpc.IframePollingTransport.Sender');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.Transport');
goog.require('goog.userAgent');
/**
* Iframe polling transport. Uses hidden iframes to transfer data
* in the fragment identifier of the URL. The peer polls the iframe's location
* for changes.
* Unfortunately, in Safari this screws up the history, because Safari doesn't
* allow to call location.replace() on a window containing a document from a
* different domain (last version tested: 2.0.4).
*
* @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}
*/
goog.net.xpc.IframePollingTransport = function(channel, opt_domHelper) {
goog.base(this, opt_domHelper);
/**
* The channel this transport belongs to.
* @type {goog.net.xpc.CrossPageChannel}
* @private
*/
this.channel_ = channel;
/**
* The URI used to send messages.
* @type {string}
* @private
*/
this.sendUri_ =
this.channel_.getConfig()[goog.net.xpc.CfgFields.PEER_POLL_URI];
/**
* The URI which is polled for incoming messages.
* @type {string}
* @private
*/
this.rcvUri_ =
this.channel_.getConfig()[goog.net.xpc.CfgFields.LOCAL_POLL_URI];
/**
* The queue to hold messages which can't be sent immediately.
* @type {Array}
* @private
*/
this.sendQueue_ = [];
};
goog.inherits(goog.net.xpc.IframePollingTransport, goog.net.xpc.Transport);
/**
* The number of times the inner frame will check for evidence of the outer
* frame before it tries its reconnection sequence. These occur at 100ms
* intervals, making this an effective max waiting period of 500ms.
* @type {number}
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.pollsBeforeReconnect_ = 5;
/**
* The transport type.
* @type {number}
* @protected
* @override
*/
goog.net.xpc.IframePollingTransport.prototype.transportType =
goog.net.xpc.TransportTypes.IFRAME_POLLING;
/**
* Sequence counter.
* @type {number}
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.sequence_ = 0;
/**
* Flag indicating whether we are waiting for an acknoledgement.
* @type {boolean}
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.waitForAck_ = false;
/**
* Flag indicating if channel has been initialized.
* @type {boolean}
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.initialized_ = false;
/**
* Reconnection iframe created by inner peer.
* @type {Element}
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.reconnectFrame_ = null;
/**
* The string used to prefix all iframe names and IDs.
* @type {string}
*/
goog.net.xpc.IframePollingTransport.IFRAME_PREFIX = 'googlexpc';
/**
* Returns the name/ID of the message frame.
* @return {string} Name of message frame.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.getMsgFrameName_ = function() {
return goog.net.xpc.IframePollingTransport.IFRAME_PREFIX + '_' +
this.channel_.name + '_msg';
};
/**
* Returns the name/ID of the ack frame.
* @return {string} Name of ack frame.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.getAckFrameName_ = function() {
return goog.net.xpc.IframePollingTransport.IFRAME_PREFIX + '_' +
this.channel_.name + '_ack';
};
/**
* Determines whether the channel is still available. The channel is
* unavailable if the transport was disposed or the peer is no longer
* available.
* @return {boolean} Whether the channel is available.
*/
goog.net.xpc.IframePollingTransport.prototype.isChannelAvailable = function() {
return !this.isDisposed() && this.channel_.isPeerAvailable();
};
/**
* Safely retrieves the frames from the peer window. If an error is thrown
* (e.g. the window is closing) an empty frame object is returned.
* @return {!Object.<!Window>} The frames from the peer window.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.getPeerFrames_ = function() {
try {
if (this.isChannelAvailable()) {
return this.channel_.getPeerWindowObject().frames || {};
}
} catch (e) {
// An error may be thrown if the window is closing.
goog.log.fine(goog.net.xpc.logger, 'error retrieving peer frames');
}
return {};
};
/**
* Safely retrieves the peer frame with the specified name.
* @param {string} frameName The name of the peer frame to retrieve.
* @return {Window} The peer frame with the specified name.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.getPeerFrame_ = function(
frameName) {
return this.getPeerFrames_()[frameName];
};
/**
* Connects this transport.
* @override
*/
goog.net.xpc.IframePollingTransport.prototype.connect = function() {
if (!this.isChannelAvailable()) {
// When the channel is unavailable there is no peer to poll so stop trying
// to connect.
return;
}
goog.log.fine(goog.net.xpc.logger, 'transport connect called');
if (!this.initialized_) {
goog.log.fine(goog.net.xpc.logger, 'initializing...');
this.constructSenderFrames_();
this.initialized_ = true;
}
this.checkForeignFramesReady_();
};
/**
* Creates the iframes which are used to send messages (and acknowledgements)
* to the peer. Sender iframes contain a document from a different origin and
* therefore their content can't be accessed.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.constructSenderFrames_ =
function() {
var name = this.getMsgFrameName_();
this.msgIframeElm_ = this.constructSenderFrame_(name);
this.msgWinObj_ = this.getWindow().frames[name];
name = this.getAckFrameName_();
this.ackIframeElm_ = this.constructSenderFrame_(name);
this.ackWinObj_ = this.getWindow().frames[name];
};
/**
* Constructs a sending frame the the given id.
* @param {string} id The id.
* @return {Element} The constructed frame.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.constructSenderFrame_ =
function(id) {
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'constructing sender frame: ' + id);
var ifr = goog.dom.createElement('iframe');
var s = ifr.style;
s.position = 'absolute';
s.top = '-10px'; s.left = '10px'; s.width = '1px'; s.height = '1px';
ifr.id = ifr.name = id;
ifr.src = this.sendUri_ + '#INITIAL';
this.getWindow().document.body.appendChild(ifr);
return ifr;
};
/**
* The protocol for reconnecting is for the inner frame to change channel
* names, and then communicate the new channel name to the outer peer.
* The outer peer looks in a predefined location for the channel name
* upate. It is important to use a completely new channel name, as this
* will ensure that all messaging iframes are not in the bfcache.
* Otherwise, Safari may pollute the history when modifying the location
* of bfcached iframes.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.maybeInnerPeerReconnect_ =
function() {
// Reconnection has been found to not function on some browsers (eg IE7), so
// it's important that the mechanism only be triggered as a last resort. As
// such, we poll a number of times to find the outer iframe before triggering
// it.
if (this.reconnectFrame_ || this.pollsBeforeReconnect_-- > 0) {
return;
}
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'Inner peer reconnect triggered.');
this.channel_.name = goog.net.xpc.getRandomString(10);
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'switching channels: ' + this.channel_.name);
this.deconstructSenderFrames_();
this.initialized_ = false;
// Communicate new channel name to outer peer.
this.reconnectFrame_ = this.constructSenderFrame_(
goog.net.xpc.IframePollingTransport.IFRAME_PREFIX +
'_reconnect_' + this.channel_.name);
};
/**
* Scans inner peer for a reconnect message, which will be used to update
* the outer peer's channel name. If a reconnect message is found, the
* sender frames will be cleaned up to make way for the new sender frames.
* Only called by the outer peer.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.outerPeerReconnect_ = function() {
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'outerPeerReconnect called');
var frames = this.getPeerFrames_();
var length = frames.length;
for (var i = 0; i < length; i++) {
var frameName;
try {
if (frames[i] && frames[i].name) {
frameName = frames[i].name;
}
} catch (e) {
// Do nothing.
}
if (!frameName) {
continue;
}
var message = frameName.split('_');
if (message.length == 3 &&
message[0] == goog.net.xpc.IframePollingTransport.IFRAME_PREFIX &&
message[1] == 'reconnect') {
// This is a legitimate reconnect message from the peer. Start using
// the peer provided channel name, and start a connection over from
// scratch.
this.channel_.name = message[2];
this.deconstructSenderFrames_();
this.initialized_ = false;
break;
}
}
};
/**
* Cleans up the existing sender frames owned by this peer. Only called by
* the outer peer.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.deconstructSenderFrames_ =
function() {
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'deconstructSenderFrames called');
if (this.msgIframeElm_) {
this.msgIframeElm_.parentNode.removeChild(this.msgIframeElm_);
this.msgIframeElm_ = null;
this.msgWinObj_ = null;
}
if (this.ackIframeElm_) {
this.ackIframeElm_.parentNode.removeChild(this.ackIframeElm_);
this.ackIframeElm_ = null;
this.ackWinObj_ = null;
}
};
/**
* Checks if the frames in the peer's page are ready. These contain a
* document from the own domain and are the ones messages are received through.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.checkForeignFramesReady_ =
function() {
// check if the connected iframe ready
if (!(this.isRcvFrameReady_(this.getMsgFrameName_()) &&
this.isRcvFrameReady_(this.getAckFrameName_()))) {
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'foreign frames not (yet) present');
if (this.channel_.getRole() == goog.net.xpc.CrossPageChannelRole.INNER) {
// The outer peer might need a short time to get its frames ready, as
// CrossPageChannel prevents them from getting created until the inner
// peer's frame has thrown its loaded event. This method is a noop for
// the first few times it's called, and then allows the reconnection
// sequence to begin.
this.maybeInnerPeerReconnect_();
} else if (this.channel_.getRole() ==
goog.net.xpc.CrossPageChannelRole.OUTER) {
// The inner peer is either not loaded yet, or the receiving
// frames are simply missing. Since we cannot discern the two cases, we
// should scan for a reconnect message from the inner peer.
this.outerPeerReconnect_();
}
// start a timer to check again
this.getWindow().setTimeout(goog.bind(this.connect, this), 100);
} else {
goog.log.fine(goog.net.xpc.logger, 'foreign frames present');
// Create receivers.
this.msgReceiver_ = new goog.net.xpc.IframePollingTransport.Receiver(
this,
this.getPeerFrame_(this.getMsgFrameName_()),
goog.bind(this.processIncomingMsg, this));
this.ackReceiver_ = new goog.net.xpc.IframePollingTransport.Receiver(
this,
this.getPeerFrame_(this.getAckFrameName_()),
goog.bind(this.processIncomingAck, this));
this.checkLocalFramesPresent_();
}
};
/**
* Checks if the receiving frame is ready.
* @param {string} frameName Which receiving frame to check.
* @return {boolean} Whether the receiving frame is ready.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.isRcvFrameReady_ =
function(frameName) {
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'checking for receive frame: ' + frameName);
/** @preserveTry */
try {
var winObj = this.getPeerFrame_(frameName);
if (!winObj || winObj.location.href.indexOf(this.rcvUri_) != 0) {
return false;
}
} catch (e) {
return false;
}
return true;
};
/**
* Checks if the iframes created in the own document are ready.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.checkLocalFramesPresent_ =
function() {
// Are the sender frames ready?
// These contain a document from the peer's domain, therefore we can only
// check if the frame itself is present.
var frames = this.getPeerFrames_();
if (!(frames[this.getAckFrameName_()] &&
frames[this.getMsgFrameName_()])) {
// start a timer to check again
if (!this.checkLocalFramesPresentCb_) {
this.checkLocalFramesPresentCb_ = goog.bind(
this.checkLocalFramesPresent_, this);
}
this.getWindow().setTimeout(this.checkLocalFramesPresentCb_, 100);
goog.log.fine(goog.net.xpc.logger, 'local frames not (yet) present');
} else {
// Create senders.
this.msgSender_ = new goog.net.xpc.IframePollingTransport.Sender(
this.sendUri_, this.msgWinObj_);
this.ackSender_ = new goog.net.xpc.IframePollingTransport.Sender(
this.sendUri_, this.ackWinObj_);
goog.log.fine(goog.net.xpc.logger, 'local frames ready');
this.getWindow().setTimeout(goog.bind(function() {
this.msgSender_.send(goog.net.xpc.SETUP);
this.sentConnectionSetup_ = true;
this.waitForAck_ = true;
goog.log.fine(goog.net.xpc.logger, 'SETUP sent');
}, this), 100);
}
};
/**
* Check if connection is ready.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.checkIfConnected_ = function() {
if (this.sentConnectionSetupAck_ && this.rcvdConnectionSetupAck_) {
this.channel_.notifyConnected();
if (this.deliveryQueue_) {
goog.log.fine(goog.net.xpc.logger, 'delivering queued messages ' +
'(' + this.deliveryQueue_.length + ')');
for (var i = 0, m; i < this.deliveryQueue_.length; i++) {
m = this.deliveryQueue_[i];
this.channel_.xpcDeliver(m.service, m.payload);
}
delete this.deliveryQueue_;
}
} else {
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'checking if connected: ' +
'ack sent:' + this.sentConnectionSetupAck_ +
', ack rcvd: ' + this.rcvdConnectionSetupAck_);
}
};
/**
* Processes an incoming message.
* @param {string} raw The complete received string.
*/
goog.net.xpc.IframePollingTransport.prototype.processIncomingMsg =
function(raw) {
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'msg received: ' + raw);
if (raw == goog.net.xpc.SETUP) {
if (!this.ackSender_) {
// Got SETUP msg, but we can't send an ack.
return;
}
this.ackSender_.send(goog.net.xpc.SETUP_ACK_);
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST, 'SETUP_ACK sent');
this.sentConnectionSetupAck_ = true;
this.checkIfConnected_();
} else if (this.channel_.isConnected() || this.sentConnectionSetupAck_) {
var pos = raw.indexOf('|');
var head = raw.substring(0, pos);
var frame = raw.substring(pos + 1);
// check if it is a framed message
pos = head.indexOf(',');
if (pos == -1) {
var seq = head;
// send acknowledgement
this.ackSender_.send('ACK:' + seq);
this.deliverPayload_(frame);
} else {
var seq = head.substring(0, pos);
// send acknowledgement
this.ackSender_.send('ACK:' + seq);
var partInfo = head.substring(pos + 1).split('/');
var part0 = parseInt(partInfo[0], 10);
var part1 = parseInt(partInfo[1], 10);
// create an array to accumulate the parts if this is the
// first frame of a message
if (part0 == 1) {
this.parts_ = [];
}
this.parts_.push(frame);
// deliver the message if this was the last frame of a message
if (part0 == part1) {
this.deliverPayload_(this.parts_.join(''));
delete this.parts_;
}
}
} else {
goog.log.warning(goog.net.xpc.logger,
'received msg, but channel is not connected');
}
};
/**
* Process an incoming acknowdedgement.
* @param {string} msgStr The incoming ack string to process.
*/
goog.net.xpc.IframePollingTransport.prototype.processIncomingAck =
function(msgStr) {
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'ack received: ' + msgStr);
if (msgStr == goog.net.xpc.SETUP_ACK_) {
this.waitForAck_ = false;
this.rcvdConnectionSetupAck_ = true;
// send the next frame
this.checkIfConnected_();
} else if (this.channel_.isConnected()) {
if (!this.waitForAck_) {
goog.log.warning(goog.net.xpc.logger, 'got unexpected ack');
return;
}
var seq = parseInt(msgStr.split(':')[1], 10);
if (seq == this.sequence_) {
this.waitForAck_ = false;
this.sendNextFrame_();
} else {
goog.log.warning(goog.net.xpc.logger, 'got ack with wrong sequence');
}
} else {
goog.log.warning(goog.net.xpc.logger,
'received ack, but channel not connected');
}
};
/**
* Sends a frame (message part).
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.sendNextFrame_ = function() {
// do nothing if we are waiting for an acknowledgement or the
// queue is emtpy
if (this.waitForAck_ || !this.sendQueue_.length) {
return;
}
var s = this.sendQueue_.shift();
++this.sequence_;
this.msgSender_.send(this.sequence_ + s);
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'msg sent: ' + this.sequence_ + s);
this.waitForAck_ = true;
};
/**
* Delivers a message.
* @param {string} s The complete message string ("<service_name>:<payload>").
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.deliverPayload_ = function(s) {
// determine the service name and the payload
var pos = s.indexOf(':');
var service = s.substr(0, pos);
var payload = s.substring(pos + 1);
// deliver the message
if (!this.channel_.isConnected()) {
// as valid messages can come in before a SETUP_ACK has
// been received (because subchannels for msgs and acks are independent),
// delay delivery of early messages until after 'connect'-event
(this.deliveryQueue_ || (this.deliveryQueue_ = [])).
push({service: service, payload: payload});
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'queued delivery');
} else {
this.channel_.xpcDeliver(service, payload);
}
};
// ---- send message ----
/**
* Maximal frame length.
* @type {number}
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.MAX_FRAME_LENGTH_ = 3800;
/**
* Sends a message. Splits it in multiple frames if too long (exceeds IE's
* URL-length maximum.
* Wireformat: <seq>[,<frame_no>/<#frames>]|<frame_content>
*
* @param {string} service Name of service this the message has to be delivered.
* @param {string} payload The message content.
* @override
*/
goog.net.xpc.IframePollingTransport.prototype.send =
function(service, payload) {
var frame = service + ':' + payload;
// put in queue
if (!goog.userAgent.IE || payload.length <= this.MAX_FRAME_LENGTH_) {
this.sendQueue_.push('|' + frame);
}
else {
var l = payload.length;
var num = Math.ceil(l / this.MAX_FRAME_LENGTH_); // number of frames
var pos = 0;
var i = 1;
while (pos < l) {
this.sendQueue_.push(',' + i + '/' + num + '|' +
frame.substr(pos, this.MAX_FRAME_LENGTH_));
i++;
pos += this.MAX_FRAME_LENGTH_;
}
}
this.sendNextFrame_();
};
/** @override */
goog.net.xpc.IframePollingTransport.prototype.disposeInternal = function() {
goog.base(this, 'disposeInternal');
var receivers = goog.net.xpc.IframePollingTransport.receivers_;
goog.array.remove(receivers, this.msgReceiver_);
goog.array.remove(receivers, this.ackReceiver_);
this.msgReceiver_ = this.ackReceiver_ = null;
goog.dom.removeNode(this.msgIframeElm_);
goog.dom.removeNode(this.ackIframeElm_);
this.msgIframeElm_ = this.ackIframeElm_ = null;
this.msgWinObj_ = this.ackWinObj_ = null;
};
/**
* Array holding all Receiver-instances.
* @type {Array.<goog.net.xpc.IframePollingTransport.Receiver>}
* @private
*/
goog.net.xpc.IframePollingTransport.receivers_ = [];
/**
* Short polling interval.
* @type {number}
* @private
*/
goog.net.xpc.IframePollingTransport.TIME_POLL_SHORT_ = 10;
/**
* Long polling interval.
* @type {number}
* @private
*/
goog.net.xpc.IframePollingTransport.TIME_POLL_LONG_ = 100;
/**
* Period how long to use TIME_POLL_SHORT_ before raising polling-interval
* to TIME_POLL_LONG_ after an activity.
* @type {number}
* @private
*/
goog.net.xpc.IframePollingTransport.TIME_SHORT_POLL_AFTER_ACTIVITY_ =
1000;
/**
* Polls all receivers.
* @private
*/
goog.net.xpc.IframePollingTransport.receive_ = function() {
var receivers = goog.net.xpc.IframePollingTransport.receivers_;
var receiver;
var rcvd = false;
/** @preserveTry */
try {
for (var i = 0; receiver = receivers[i]; i++) {
rcvd = rcvd || receiver.receive();
}
} catch (e) {
goog.log.info(goog.net.xpc.logger, 'receive_() failed: ' + e);
// Notify the channel that the transport had an error.
receiver.transport_.channel_.notifyTransportError();
// notifyTransportError() closes the channel and disposes the transport.
// If there are no other channels present, this.receivers_ will now be empty
// and there is no need to keep polling.
if (!receivers.length) {
return;
}
}
var now = goog.now();
if (rcvd) {
goog.net.xpc.IframePollingTransport.lastActivity_ = now;
}
// Schedule next check.
var t = now - goog.net.xpc.IframePollingTransport.lastActivity_ <
goog.net.xpc.IframePollingTransport.TIME_SHORT_POLL_AFTER_ACTIVITY_ ?
goog.net.xpc.IframePollingTransport.TIME_POLL_SHORT_ :
goog.net.xpc.IframePollingTransport.TIME_POLL_LONG_;
goog.net.xpc.IframePollingTransport.rcvTimer_ = window.setTimeout(
goog.net.xpc.IframePollingTransport.receiveCb_, t);
};
/**
* Callback that wraps receive_ to be used in timers.
* @type {Function}
* @private
*/
goog.net.xpc.IframePollingTransport.receiveCb_ = goog.bind(
goog.net.xpc.IframePollingTransport.receive_,
goog.net.xpc.IframePollingTransport);
/**
* Starts the polling loop.
* @private
*/
goog.net.xpc.IframePollingTransport.startRcvTimer_ = function() {
goog.log.fine(goog.net.xpc.logger, 'starting receive-timer');
goog.net.xpc.IframePollingTransport.lastActivity_ = goog.now();
if (goog.net.xpc.IframePollingTransport.rcvTimer_) {
window.clearTimeout(goog.net.xpc.IframePollingTransport.rcvTimer_);
}
goog.net.xpc.IframePollingTransport.rcvTimer_ = window.setTimeout(
goog.net.xpc.IframePollingTransport.receiveCb_,
goog.net.xpc.IframePollingTransport.TIME_POLL_SHORT_);
};
/**
* goog.net.xpc.IframePollingTransport.Sender
*
* Utility class to send message-parts to a document from a different origin.
*
* @constructor
* @param {string} url The url the other document will use for polling.
* @param {Object} windowObj The frame used for sending information to.
*/
goog.net.xpc.IframePollingTransport.Sender = function(url, windowObj) {
/**
* The URI used to sending messages.
* @type {string}
* @private
*/
this.sendUri_ = url;
/**
* The window object of the iframe used to send messages.
* The script instantiating the Sender won't have access to
* the content of sendFrame_.
* @type {Object}
* @private
*/
this.sendFrame_ = windowObj;
/**
* Cycle counter (used to make sure that sending two identical messages sent
* in direct succession can be recognized as such by the receiver).
* @type {number}
* @private
*/
this.cycle_ = 0;
};
/**
* Sends a message-part (frame) to the peer.
* The message-part is encoded and put in the fragment identifier
* of the URL used for sending (and belongs to the origin/domain of the peer).
* @param {string} payload The message to send.
*/
goog.net.xpc.IframePollingTransport.Sender.prototype.send = function(payload) {
this.cycle_ = ++this.cycle_ % 2;
var url = this.sendUri_ + '#' + this.cycle_ + encodeURIComponent(payload);
// TODO(user) Find out if try/catch is still needed
/** @preserveTry */
try {
// safari doesn't allow to call location.replace()
if (goog.userAgent.WEBKIT) {
this.sendFrame_.location.href = url;
} else {
this.sendFrame_.location.replace(url);
}
} catch (e) {
goog.log.error(goog.net.xpc.logger, 'sending failed', e);
}
// Restart receiver timer on short polling interval, to support use-cases
// where we need to capture responses quickly.
goog.net.xpc.IframePollingTransport.startRcvTimer_();
};
/**
* goog.net.xpc.IframePollingTransport.Receiver
*
* @constructor
* @param {goog.net.xpc.IframePollingTransport} transport The transport to
* receive from.
* @param {Object} windowObj The window-object to poll for location-changes.
* @param {Function} callback The callback-function to be called when
* location has changed.
*/
goog.net.xpc.IframePollingTransport.Receiver = function(transport,
windowObj,
callback) {
/**
* The transport to receive from.
* @type {goog.net.xpc.IframePollingTransport}
* @private
*/
this.transport_ = transport;
this.rcvFrame_ = windowObj;
this.cb_ = callback;
this.currentLoc_ = this.rcvFrame_.location.href.split('#')[0] + '#INITIAL';
goog.net.xpc.IframePollingTransport.receivers_.push(this);
goog.net.xpc.IframePollingTransport.startRcvTimer_();
};
/**
* Polls the location of the receiver-frame for changes.
* @return {boolean} Whether a change has been detected.
*/
goog.net.xpc.IframePollingTransport.Receiver.prototype.receive = function() {
var loc = this.rcvFrame_.location.href;
if (loc != this.currentLoc_) {
this.currentLoc_ = loc;
var payload = loc.split('#')[1];
if (payload) {
payload = payload.substr(1); // discard first character (cycle)
this.cb_(decodeURIComponent(payload));
}
return true;
} else {
return false;
}
};

View File

@@ -0,0 +1,394 @@
// 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.events');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.Transport');
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}
*/
goog.net.xpc.IframeRelayTransport = function(channel, opt_domHelper) {
goog.base(this, 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');
div.innerHTML = '<iframe onload="this.xpcOnload()"></iframe>';
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.base(this, 'disposeInternal');
if (goog.userAgent.WEBKIT) {
goog.net.xpc.IframeRelayTransport.cleanup_(0);
}
};

View File

@@ -0,0 +1,649 @@
// 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 class which uses native messaging
* facilities for cross domain communication.
*
*/
goog.provide('goog.net.xpc.NativeMessagingTransport');
goog.require('goog.Timer');
goog.require('goog.asserts');
goog.require('goog.async.Deferred');
goog.require('goog.events');
goog.require('goog.events.EventHandler');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.Transport');
/**
* The native messaging transport
*
* Uses document.postMessage() to send messages to other documents.
* Receiving is done by listening on 'message'-events on the document.
*
* @param {goog.net.xpc.CrossPageChannel} channel The channel this
* transport belongs to.
* @param {string} peerHostname The hostname (protocol, domain, and port) of the
* peer.
* @param {goog.dom.DomHelper=} opt_domHelper The dom helper to use for
* finding the correct window/document.
* @param {boolean=} opt_oneSidedHandshake If this is true, only the outer
* transport sends a SETUP message and expects a SETUP_ACK. The inner
* transport goes connected when it receives the SETUP.
* @param {number=} opt_protocolVersion Which version of its setup protocol the
* transport should use. The default is '2'.
* @constructor
* @extends {goog.net.xpc.Transport}
*/
goog.net.xpc.NativeMessagingTransport = function(channel, peerHostname,
opt_domHelper, opt_oneSidedHandshake, opt_protocolVersion) {
goog.base(this, opt_domHelper);
/**
* The channel this transport belongs to.
* @type {goog.net.xpc.CrossPageChannel}
* @private
*/
this.channel_ = channel;
/**
* Which version of the transport's protocol should be used.
* @type {number}
* @private
*/
this.protocolVersion_ = opt_protocolVersion || 2;
goog.asserts.assert(this.protocolVersion_ >= 1);
goog.asserts.assert(this.protocolVersion_ <= 2);
/**
* The hostname of the peer. This parameterizes all calls to postMessage, and
* should contain the precise protocol, domain, and port of the peer window.
* @type {string}
* @private
*/
this.peerHostname_ = peerHostname || '*';
/**
* The event handler.
* @type {!goog.events.EventHandler}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
/**
* Timer for connection reattempts.
* @type {!goog.Timer}
* @private
*/
this.maybeAttemptToConnectTimer_ = new goog.Timer(100, this.getWindow());
/**
* Whether one-sided handshakes are enabled.
* @type {boolean}
* @private
*/
this.oneSidedHandshake_ = !!opt_oneSidedHandshake;
/**
* Fires once we've received our SETUP_ACK message.
* @type {!goog.async.Deferred}
* @private
*/
this.setupAckReceived_ = new goog.async.Deferred();
/**
* Fires once we've sent our SETUP_ACK message.
* @type {!goog.async.Deferred}
* @private
*/
this.setupAckSent_ = new goog.async.Deferred();
/**
* Fires once we're marked connected.
* @type {!goog.async.Deferred}
* @private
*/
this.connected_ = new goog.async.Deferred();
/**
* The unique ID of this side of the connection. Used to determine when a peer
* is reloaded.
* @type {string}
* @private
*/
this.endpointId_ = goog.net.xpc.getRandomString(10);
/**
* The unique ID of the peer. If we get a message from a peer with an ID we
* don't expect, we reset the connection.
* @type {?string}
* @private
*/
this.peerEndpointId_ = null;
// We don't want to mark ourselves connected until we have sent whatever
// message will cause our counterpart in the other frame to also declare
// itself connected, if there is such a message. Otherwise we risk a user
// message being sent in advance of that message, and it being discarded.
if (this.oneSidedHandshake_) {
if (this.channel_.getRole() == goog.net.xpc.CrossPageChannelRole.INNER) {
// One sided handshake, inner frame:
// SETUP_ACK must be received.
this.connected_.awaitDeferred(this.setupAckReceived_);
} else {
// One sided handshake, outer frame:
// SETUP_ACK must be sent.
this.connected_.awaitDeferred(this.setupAckSent_);
}
} else {
// Two sided handshake:
// SETUP_ACK has to have been received, and sent.
this.connected_.awaitDeferred(this.setupAckReceived_);
if (this.protocolVersion_ == 2) {
this.connected_.awaitDeferred(this.setupAckSent_);
}
}
this.connected_.addCallback(this.notifyConnected_, this);
this.connected_.callback(true);
this.eventHandler_.
listen(this.maybeAttemptToConnectTimer_, goog.Timer.TICK,
this.maybeAttemptToConnect_);
goog.log.info(goog.net.xpc.logger, 'NativeMessagingTransport created. ' +
'protocolVersion=' + this.protocolVersion_ + ', oneSidedHandshake=' +
this.oneSidedHandshake_ + ', role=' + this.channel_.getRole());
};
goog.inherits(goog.net.xpc.NativeMessagingTransport, goog.net.xpc.Transport);
/**
* Length of the delay in milliseconds between the channel being connected and
* the connection callback being called, in cases where coverage of timing flaws
* is required.
* @type {number}
* @private
*/
goog.net.xpc.NativeMessagingTransport.CONNECTION_DELAY_MS_ = 200;
/**
* Current determination of peer's protocol version, or null for unknown.
* @type {?number}
* @private
*/
goog.net.xpc.NativeMessagingTransport.prototype.peerProtocolVersion_ = null;
/**
* Flag indicating if this instance of the transport has been initialized.
* @type {boolean}
* @private
*/
goog.net.xpc.NativeMessagingTransport.prototype.initialized_ = false;
/**
* The transport type.
* @type {number}
* @override
*/
goog.net.xpc.NativeMessagingTransport.prototype.transportType =
goog.net.xpc.TransportTypes.NATIVE_MESSAGING;
/**
* The delimiter used for transport service messages.
* @type {string}
* @private
*/
goog.net.xpc.NativeMessagingTransport.MESSAGE_DELIMITER_ = ',';
/**
* Tracks the number of NativeMessagingTransport channels that have been
* initialized but not disposed yet in a map keyed by the UID of the window
* object. This allows for multiple windows to be initiallized and listening
* for messages.
* @type {Object.<number>}
* @private
*/
goog.net.xpc.NativeMessagingTransport.activeCount_ = {};
/**
* Id of a timer user during postMessage sends.
* @type {number}
* @private
*/
goog.net.xpc.NativeMessagingTransport.sendTimerId_ = 0;
/**
* Checks whether the peer transport protocol version could be as indicated.
* @param {number} version The version to check for.
* @return {boolean} Whether the peer transport protocol version is as
* indicated, or null.
* @private
*/
goog.net.xpc.NativeMessagingTransport.prototype.couldPeerVersionBe_ =
function(version) {
return this.peerProtocolVersion_ == null ||
this.peerProtocolVersion_ == version;
};
/**
* Initializes this transport. Registers a listener for 'message'-events
* on the document.
* @param {Window} listenWindow The window to listen to events on.
* @private
*/
goog.net.xpc.NativeMessagingTransport.initialize_ = function(listenWindow) {
var uid = goog.getUid(listenWindow);
var value = goog.net.xpc.NativeMessagingTransport.activeCount_[uid];
if (!goog.isNumber(value)) {
value = 0;
}
if (value == 0) {
// Listen for message-events. These are fired on window in FF3 and on
// document in Opera.
goog.events.listen(
listenWindow.postMessage ? listenWindow : listenWindow.document,
'message',
goog.net.xpc.NativeMessagingTransport.messageReceived_,
false,
goog.net.xpc.NativeMessagingTransport);
}
goog.net.xpc.NativeMessagingTransport.activeCount_[uid] = value + 1;
};
/**
* Processes an incoming message-event.
* @param {goog.events.BrowserEvent} msgEvt The message event.
* @return {boolean} True if message was successfully delivered to a channel.
* @private
*/
goog.net.xpc.NativeMessagingTransport.messageReceived_ = function(msgEvt) {
var data = msgEvt.getBrowserEvent().data;
if (!goog.isString(data)) {
return false;
}
var headDelim = data.indexOf('|');
var serviceDelim = data.indexOf(':');
// make sure we got something reasonable
if (headDelim == -1 || serviceDelim == -1) {
return false;
}
var channelName = data.substring(0, headDelim);
var service = data.substring(headDelim + 1, serviceDelim);
var payload = data.substring(serviceDelim + 1);
goog.log.fine(goog.net.xpc.logger,
'messageReceived: channel=' + channelName +
', service=' + service + ', payload=' + payload);
// Attempt to deliver message to the channel. Keep in mind that it may not
// exist for several reasons, including but not limited to:
// - a malformed message
// - the channel simply has not been created
// - channel was created in a different namespace
// - message was sent to the wrong window
// - channel has become stale (e.g. caching iframes and back clicks)
var channel = goog.net.xpc.channels[channelName];
if (channel) {
channel.xpcDeliver(service, payload, msgEvt.getBrowserEvent().origin);
return true;
}
var transportMessageType =
goog.net.xpc.NativeMessagingTransport.parseTransportPayload_(payload)[0];
// Check if there are any stale channel names that can be updated.
for (var staleChannelName in goog.net.xpc.channels) {
var staleChannel = goog.net.xpc.channels[staleChannelName];
if (staleChannel.getRole() == goog.net.xpc.CrossPageChannelRole.INNER &&
!staleChannel.isConnected() &&
service == goog.net.xpc.TRANSPORT_SERVICE_ &&
(transportMessageType == goog.net.xpc.SETUP ||
transportMessageType == goog.net.xpc.SETUP_NTPV2)) {
// Inner peer received SETUP message but channel names did not match.
// Start using the channel name sent from outer peer. The channel name
// of the inner peer can easily become out of date, as iframe's and their
// JS state get cached in many browsers upon page reload or history
// navigation (particularly Firefox 1.5+). We can trust the outer peer,
// since we only accept postMessage messages from the same hostname that
// originally setup the channel.
goog.log.fine(goog.net.xpc.logger,
'changing channel name to ' + channelName);
staleChannel.name = channelName;
// Remove old stale pointer to channel.
delete goog.net.xpc.channels[staleChannelName];
// Create fresh pointer to channel.
goog.net.xpc.channels[channelName] = staleChannel;
staleChannel.xpcDeliver(service, payload);
return true;
}
}
// Failed to find a channel to deliver this message to, so simply ignore it.
goog.log.info(goog.net.xpc.logger, 'channel name mismatch; message ignored"');
return false;
};
/**
* Handles transport service messages.
* @param {string} payload The message content.
* @override
*/
goog.net.xpc.NativeMessagingTransport.prototype.transportServiceHandler =
function(payload) {
var transportParts =
goog.net.xpc.NativeMessagingTransport.parseTransportPayload_(payload);
var transportMessageType = transportParts[0];
var peerEndpointId = transportParts[1];
switch (transportMessageType) {
case goog.net.xpc.SETUP_ACK_:
this.setPeerProtocolVersion_(1);
if (!this.setupAckReceived_.hasFired()) {
this.setupAckReceived_.callback(true);
}
break;
case goog.net.xpc.SETUP_ACK_NTPV2:
if (this.protocolVersion_ == 2) {
this.setPeerProtocolVersion_(2);
if (!this.setupAckReceived_.hasFired()) {
this.setupAckReceived_.callback(true);
}
}
break;
case goog.net.xpc.SETUP:
this.setPeerProtocolVersion_(1);
this.sendSetupAckMessage_(1);
break;
case goog.net.xpc.SETUP_NTPV2:
if (this.protocolVersion_ == 2) {
var prevPeerProtocolVersion = this.peerProtocolVersion_;
this.setPeerProtocolVersion_(2);
this.sendSetupAckMessage_(2);
if ((prevPeerProtocolVersion == 1 || this.peerEndpointId_ != null) &&
this.peerEndpointId_ != peerEndpointId) {
// Send a new SETUP message since the peer has been replaced.
goog.log.info(goog.net.xpc.logger,
'Sending SETUP and changing peer ID to: ' + peerEndpointId);
this.sendSetupMessage_();
}
this.peerEndpointId_ = peerEndpointId;
}
break;
}
};
/**
* Sends a SETUP transport service message of the correct protocol number for
* our current situation.
* @private
*/
goog.net.xpc.NativeMessagingTransport.prototype.sendSetupMessage_ =
function() {
// 'real' (legacy) v1 transports don't know about there being v2 ones out
// there, and we shouldn't either.
goog.asserts.assert(!(this.protocolVersion_ == 1 &&
this.peerProtocolVersion_ == 2));
if (this.protocolVersion_ == 2 && this.couldPeerVersionBe_(2)) {
var payload = goog.net.xpc.SETUP_NTPV2;
payload += goog.net.xpc.NativeMessagingTransport.MESSAGE_DELIMITER_;
payload += this.endpointId_;
this.send(goog.net.xpc.TRANSPORT_SERVICE_, payload);
}
// For backward compatibility reasons, the V1 SETUP message can be sent by
// both V1 and V2 transports. Once a V2 transport has 'heard' another V2
// transport it starts ignoring V1 messages, so the V2 message must be sent
// first.
if (this.couldPeerVersionBe_(1)) {
this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP);
}
};
/**
* Sends a SETUP_ACK transport service message of the correct protocol number
* for our current situation.
* @param {number} protocolVersion The protocol version of the SETUP message
* which gave rise to this ack message.
* @private
*/
goog.net.xpc.NativeMessagingTransport.prototype.sendSetupAckMessage_ =
function(protocolVersion) {
goog.asserts.assert(this.protocolVersion_ != 1 || protocolVersion != 2,
'Shouldn\'t try to send a v2 setup ack in v1 mode.');
if (this.protocolVersion_ == 2 && this.couldPeerVersionBe_(2) &&
protocolVersion == 2) {
this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP_ACK_NTPV2);
} else if (this.couldPeerVersionBe_(1) && protocolVersion == 1) {
this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP_ACK_);
} else {
return;
}
if (!this.setupAckSent_.hasFired()) {
this.setupAckSent_.callback(true);
}
};
/**
* Attempts to set the peer protocol number. Downgrades from 2 to 1 are not
* permitted.
* @param {number} version The new protocol number.
* @private
*/
goog.net.xpc.NativeMessagingTransport.prototype.setPeerProtocolVersion_ =
function(version) {
if (version > this.peerProtocolVersion_) {
this.peerProtocolVersion_ = version;
}
if (this.peerProtocolVersion_ == 1) {
if (!this.setupAckSent_.hasFired() && !this.oneSidedHandshake_) {
this.setupAckSent_.callback(true);
}
this.peerEndpointId_ = null;
}
};
/**
* Connects this transport.
* @override
*/
goog.net.xpc.NativeMessagingTransport.prototype.connect = function() {
goog.net.xpc.NativeMessagingTransport.initialize_(this.getWindow());
this.initialized_ = true;
this.maybeAttemptToConnect_();
};
/**
* Connects to other peer. In the case of the outer peer, the setup messages are
* likely sent before the inner peer is ready to receive them. Therefore, this
* function will continue trying to send the SETUP message until the inner peer
* responds. In the case of the inner peer, it will occasionally have its
* channel name fall out of sync with the outer peer, particularly during
* soft-reloads and history navigations.
* @private
*/
goog.net.xpc.NativeMessagingTransport.prototype.maybeAttemptToConnect_ =
function() {
// In a one-sided handshake, the outer frame does not send a SETUP message,
// but the inner frame does.
var outerFrame = this.channel_.getRole() ==
goog.net.xpc.CrossPageChannelRole.OUTER;
if ((this.oneSidedHandshake_ && outerFrame) ||
this.channel_.isConnected() ||
this.isDisposed()) {
this.maybeAttemptToConnectTimer_.stop();
return;
}
this.maybeAttemptToConnectTimer_.start();
this.sendSetupMessage_();
};
/**
* Sends a message.
* @param {string} service The name off the service the message is to be
* delivered to.
* @param {string} payload The message content.
* @override
*/
goog.net.xpc.NativeMessagingTransport.prototype.send = function(service,
payload) {
var win = this.channel_.getPeerWindowObject();
if (!win) {
goog.log.fine(goog.net.xpc.logger, 'send(): window not ready');
return;
}
this.send = function(service, payload) {
// In IE8 (and perhaps elsewhere), it seems like postMessage is sometimes
// implemented as a synchronous call. That is, calling it synchronously
// calls whatever listeners it has, and control is not returned to the
// calling thread until those listeners are run. This produces different
// ordering to all other browsers, and breaks this protocol. This timer
// callback is introduced to produce standard behavior across all browsers.
var transport = this;
var channelName = this.channel_.name;
var sendFunctor = function() {
transport.sendTimerId_ = 0;
try {
// postMessage is a method of the window object, except in some
// versions of Opera, where it is a method of the document object. It
// also seems that the appearance of postMessage on the peer window
// object can sometimes be delayed.
var obj = win.postMessage ? win : win.document;
if (!obj.postMessage) {
goog.log.warning(goog.net.xpc.logger,
'Peer window had no postMessage function.');
return;
}
obj.postMessage(channelName + '|' + service + ':' + payload,
transport.peerHostname_);
goog.log.fine(goog.net.xpc.logger, 'send(): service=' + service +
' payload=' + payload + ' to hostname=' + transport.peerHostname_);
} catch (error) {
// There is some evidence (not totally convincing) that postMessage can
// be missing or throw errors during a narrow timing window during
// startup. This protects against that.
goog.log.warning(goog.net.xpc.logger,
'Error performing postMessage, ignoring.', error);
}
};
this.sendTimerId_ = goog.Timer.callOnce(sendFunctor, 0);
};
this.send(service, payload);
};
/**
* Notify the channel that this transport is connected. If either transport is
* protocol v1, a short delay is required to paper over timing vulnerabilities
* in that protocol version.
* @private
*/
goog.net.xpc.NativeMessagingTransport.prototype.notifyConnected_ =
function() {
var delay = (this.protocolVersion_ == 1 || this.peerProtocolVersion_ == 1) ?
goog.net.xpc.NativeMessagingTransport.CONNECTION_DELAY_MS_ : undefined;
this.channel_.notifyConnected(delay);
};
/** @override */
goog.net.xpc.NativeMessagingTransport.prototype.disposeInternal = function() {
if (this.initialized_) {
var listenWindow = this.getWindow();
var uid = goog.getUid(listenWindow);
var value = goog.net.xpc.NativeMessagingTransport.activeCount_[uid];
goog.net.xpc.NativeMessagingTransport.activeCount_[uid] = value - 1;
if (value == 1) {
goog.events.unlisten(
listenWindow.postMessage ? listenWindow : listenWindow.document,
'message',
goog.net.xpc.NativeMessagingTransport.messageReceived_,
false,
goog.net.xpc.NativeMessagingTransport);
}
}
if (this.sendTimerId_) {
goog.Timer.clear(this.sendTimerId_);
this.sendTimerId_ = 0;
}
goog.dispose(this.eventHandler_);
delete this.eventHandler_;
goog.dispose(this.maybeAttemptToConnectTimer_);
delete this.maybeAttemptToConnectTimer_;
this.setupAckReceived_.cancel();
delete this.setupAckReceived_;
this.setupAckSent_.cancel();
delete this.setupAckSent_;
this.connected_.cancel();
delete this.connected_;
// Cleaning up this.send as it is an instance method, created in
// goog.net.xpc.NativeMessagingTransport.prototype.send and has a closure over
// this.channel_.peerWindowObject_.
delete this.send;
goog.base(this, 'disposeInternal');
};
/**
* Parse a transport service payload message. For v1, it is simply expected to
* be 'SETUP' or 'SETUP_ACK'. For v2, an example setup message is
* 'SETUP_NTPV2,abc123', where the second part is the endpoint id. The v2 setup
* ack message is simply 'SETUP_ACK_NTPV2'.
* @param {string} payload The payload.
* @return {!Array.<?string>} An array with the message type as the first member
* and the endpoint id as the second, if one was sent, or null otherwise.
* @private
*/
goog.net.xpc.NativeMessagingTransport.parseTransportPayload_ =
function(payload) {
var transportParts = /** @type {!Array.<?string>} */ (payload.split(
goog.net.xpc.NativeMessagingTransport.MESSAGE_DELIMITER_));
transportParts[1] = transportParts[1] || null;
return transportParts;
};

View File

@@ -0,0 +1,479 @@
// Copyright 2008 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 NIX (Native IE XDC) method transport for
* cross-domain communication. It exploits the fact that Internet Explorer
* allows a window that is the parent of an iframe to set said iframe window's
* opener property to an object. This object can be a function that in turn
* can be used to send a message despite same-origin constraints. Note that
* this function, if a pure JavaScript object, opens up the possibilitiy of
* gaining a hold of the context of the other window and in turn, attacking
* it. This implementation therefore wraps the JavaScript objects used inside
* a VBScript class. Since VBScript objects are passed in JavaScript as a COM
* wrapper (like DOM objects), they are thus opaque to JavaScript
* (except for the interface they expose). This therefore provides a safe
* method of transport.
*
*
* Initially based on FrameElementTransport which shares some similarities
* to this method.
*/
goog.provide('goog.net.xpc.NixTransport');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.Transport');
goog.require('goog.reflect');
/**
* NIX method transport.
*
* NOTE(user): NIX method tested in all IE versions starting from 6.0.
*
* @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}
*/
goog.net.xpc.NixTransport = function(channel, opt_domHelper) {
goog.base(this, opt_domHelper);
/**
* The channel this transport belongs to.
* @type {goog.net.xpc.CrossPageChannel}
* @private
*/
this.channel_ = channel;
/**
* The authorization token, if any, used by this transport.
* @type {?string}
* @private
*/
this.authToken_ = channel[goog.net.xpc.CfgFields.AUTH_TOKEN] || '';
/**
* The authorization token, if any, that must be sent by the other party
* for setup to occur.
* @type {?string}
* @private
*/
this.remoteAuthToken_ =
channel[goog.net.xpc.CfgFields.REMOTE_AUTH_TOKEN] || '';
// Conduct the setup work for NIX in general, if need be.
goog.net.xpc.NixTransport.conductGlobalSetup_(this.getWindow());
// Setup aliases so that VBScript can call these methods
// on the transport class, even if they are renamed during
// compression.
this[goog.net.xpc.NixTransport.NIX_HANDLE_MESSAGE] = this.handleMessage_;
this[goog.net.xpc.NixTransport.NIX_CREATE_CHANNEL] = this.createChannel_;
};
goog.inherits(goog.net.xpc.NixTransport, goog.net.xpc.Transport);
// Consts for NIX. VBScript doesn't allow items to start with _ for some
// reason, so we need to make these names quite unique, as they will go into
// the global namespace.
/**
* Global name of the Wrapper VBScript class.
* Note that this class will be stored in the *global*
* namespace (i.e. window in browsers).
* @type {string}
*/
goog.net.xpc.NixTransport.NIX_WRAPPER = 'GCXPC____NIXVBS_wrapper';
/**
* Global name of the GetWrapper VBScript function. This
* constant is used by JavaScript to call this function.
* Note that this function will be stored in the *global*
* namespace (i.e. window in browsers).
* @type {string}
*/
goog.net.xpc.NixTransport.NIX_GET_WRAPPER = 'GCXPC____NIXVBS_get_wrapper';
/**
* The name of the handle message method used by the wrapper class
* when calling the transport.
* @type {string}
*/
goog.net.xpc.NixTransport.NIX_HANDLE_MESSAGE = 'GCXPC____NIXJS_handle_message';
/**
* The name of the create channel method used by the wrapper class
* when calling the transport.
* @type {string}
*/
goog.net.xpc.NixTransport.NIX_CREATE_CHANNEL = 'GCXPC____NIXJS_create_channel';
/**
* A "unique" identifier that is stored in the wrapper
* class so that the wrapper can be distinguished from
* other objects easily.
* @type {string}
*/
goog.net.xpc.NixTransport.NIX_ID_FIELD = 'GCXPC____NIXVBS_container';
/**
* Determines if the installed version of IE supports accessing window.opener
* after it has been set to a non-Window/null value. NIX relies on this being
* possible.
* @return {boolean} Whether window.opener behavior is compatible with NIX.
*/
goog.net.xpc.NixTransport.isNixSupported = function() {
var isSupported = false;
try {
var oldOpener = window.opener;
// The compiler complains (as it should!) if we set window.opener to
// something other than a window or null.
window.opener = /** @type {Window} */ ({});
isSupported = goog.reflect.canAccessProperty(window, 'opener');
window.opener = oldOpener;
} catch (e) { }
return isSupported;
};
/**
* Conducts the global setup work for the NIX transport method.
* This function creates and then injects into the page the
* VBScript code necessary to create the NIX wrapper class.
* Note that this method can be called multiple times, as
* it internally checks whether the work is necessary before
* proceeding.
* @param {Window} listenWindow The window containing the affected page.
* @private
*/
goog.net.xpc.NixTransport.conductGlobalSetup_ = function(listenWindow) {
if (listenWindow['nix_setup_complete']) {
return;
}
// Inject the VBScript code needed.
var vbscript =
// We create a class to act as a wrapper for
// a Javascript call, to prevent a break in of
// the context.
'Class ' + goog.net.xpc.NixTransport.NIX_WRAPPER + '\n ' +
// An internal member for keeping track of the
// transport for which this wrapper exists.
'Private m_Transport\n' +
// An internal member for keeping track of the
// auth token associated with the context that
// created this wrapper. Used for validation
// purposes.
'Private m_Auth\n' +
// Method for internally setting the value
// of the m_Transport property. We have the
// isEmpty check to prevent the transport
// from being overridden with an illicit
// object by a malicious party.
'Public Sub SetTransport(transport)\n' +
'If isEmpty(m_Transport) Then\n' +
'Set m_Transport = transport\n' +
'End If\n' +
'End Sub\n' +
// Method for internally setting the value
// of the m_Auth property. We have the
// isEmpty check to prevent the transport
// from being overridden with an illicit
// object by a malicious party.
'Public Sub SetAuth(auth)\n' +
'If isEmpty(m_Auth) Then\n' +
'm_Auth = auth\n' +
'End If\n' +
'End Sub\n' +
// Returns the auth token to the gadget, so it can
// confirm a match before initiating the connection
'Public Function GetAuthToken()\n ' +
'GetAuthToken = m_Auth\n' +
'End Function\n' +
// A wrapper method which causes a
// message to be sent to the other context.
'Public Sub SendMessage(service, payload)\n ' +
'Call m_Transport.' +
goog.net.xpc.NixTransport.NIX_HANDLE_MESSAGE + '(service, payload)\n' +
'End Sub\n' +
// Method for setting up the inner->outer
// channel.
'Public Sub CreateChannel(channel)\n ' +
'Call m_Transport.' +
goog.net.xpc.NixTransport.NIX_CREATE_CHANNEL + '(channel)\n' +
'End Sub\n' +
// An empty field with a unique identifier to
// prevent the code from confusing this wrapper
// with a run-of-the-mill value found in window.opener.
'Public Sub ' + goog.net.xpc.NixTransport.NIX_ID_FIELD + '()\n ' +
'End Sub\n' +
'End Class\n ' +
// Function to get a reference to the wrapper.
'Function ' +
goog.net.xpc.NixTransport.NIX_GET_WRAPPER + '(transport, auth)\n' +
'Dim wrap\n' +
'Set wrap = New ' + goog.net.xpc.NixTransport.NIX_WRAPPER + '\n' +
'wrap.SetTransport transport\n' +
'wrap.SetAuth auth\n' +
'Set ' + goog.net.xpc.NixTransport.NIX_GET_WRAPPER + ' = wrap\n' +
'End Function';
try {
listenWindow.execScript(vbscript, 'vbscript');
listenWindow['nix_setup_complete'] = true;
}
catch (e) {
goog.log.error(goog.net.xpc.logger,
'exception caught while attempting global setup: ' + e);
}
};
/**
* The transport type.
* @type {number}
* @protected
* @override
*/
goog.net.xpc.NixTransport.prototype.transportType =
goog.net.xpc.TransportTypes.NIX;
/**
* Keeps track of whether the local setup has completed (i.e.
* the initial work towards setting the channel up has been
* completed for this end).
* @type {boolean}
* @private
*/
goog.net.xpc.NixTransport.prototype.localSetupCompleted_ = false;
/**
* The NIX channel used to talk to the other page. This
* object is in fact a reference to a VBScript class
* (see above) and as such, is in fact a COM wrapper.
* When using this object, make sure to not access methods
* without calling them, otherwise a COM error will be thrown.
* @type {Object}
* @private
*/
goog.net.xpc.NixTransport.prototype.nixChannel_ = null;
/**
* Connect this transport.
* @override
*/
goog.net.xpc.NixTransport.prototype.connect = function() {
if (this.channel_.getRole() == goog.net.xpc.CrossPageChannelRole.OUTER) {
this.attemptOuterSetup_();
} else {
this.attemptInnerSetup_();
}
};
/**
* Attempts to setup the channel from the perspective
* of the outer (read: container) page. This method
* will attempt to create a NIX wrapper for this transport
* and place it into the "opener" property of the inner
* page's window object. If it fails, it will continue
* to loop until it does so.
*
* @private
*/
goog.net.xpc.NixTransport.prototype.attemptOuterSetup_ = function() {
if (this.localSetupCompleted_) {
return;
}
// Get shortcut to iframe-element that contains the inner
// page.
var innerFrame = this.channel_.getIframeElement();
try {
// Attempt to place the NIX wrapper object into the inner
// frame's opener property.
var theWindow = this.getWindow();
var getWrapper = theWindow[goog.net.xpc.NixTransport.NIX_GET_WRAPPER];
innerFrame.contentWindow.opener = getWrapper(this, this.authToken_);
this.localSetupCompleted_ = true;
}
catch (e) {
goog.log.error(goog.net.xpc.logger,
'exception caught while attempting setup: ' + e);
}
// If the retry is necessary, reattempt this setup.
if (!this.localSetupCompleted_) {
this.getWindow().setTimeout(goog.bind(this.attemptOuterSetup_, this), 100);
}
};
/**
* Attempts to setup the channel from the perspective
* of the inner (read: iframe) page. This method
* will attempt to *read* the opener object from the
* page's opener property. If it succeeds, this object
* is saved into nixChannel_ and the channel is confirmed
* with the container by calling CreateChannel with an instance
* of a wrapper for *this* page. Note that if this method
* fails, it will continue to loop until it succeeds.
*
* @private
*/
goog.net.xpc.NixTransport.prototype.attemptInnerSetup_ = function() {
if (this.localSetupCompleted_) {
return;
}
try {
var opener = this.getWindow().opener;
// Ensure that the object contained inside the opener
// property is in fact a NIX wrapper.
if (opener && goog.net.xpc.NixTransport.NIX_ID_FIELD in opener) {
this.nixChannel_ = opener;
// Ensure that the NIX channel given to use is valid.
var remoteAuthToken = this.nixChannel_['GetAuthToken']();
if (remoteAuthToken != this.remoteAuthToken_) {
goog.log.error(goog.net.xpc.logger,
'Invalid auth token from other party');
return;
}
// Complete the construction of the channel by sending our own
// wrapper to the container via the channel they gave us.
var theWindow = this.getWindow();
var getWrapper = theWindow[goog.net.xpc.NixTransport.NIX_GET_WRAPPER];
this.nixChannel_['CreateChannel'](getWrapper(this, this.authToken_));
this.localSetupCompleted_ = true;
// Notify channel that the transport is ready.
this.channel_.notifyConnected();
}
}
catch (e) {
goog.log.error(goog.net.xpc.logger,
'exception caught while attempting setup: ' + e);
return;
}
// If the retry is necessary, reattempt this setup.
if (!this.localSetupCompleted_) {
this.getWindow().setTimeout(goog.bind(this.attemptInnerSetup_, this), 100);
}
};
/**
* Internal method called by the inner page, via the
* NIX wrapper, to complete the setup of the channel.
*
* @param {Object} channel The NIX wrapper of the
* inner page.
* @private
*/
goog.net.xpc.NixTransport.prototype.createChannel_ = function(channel) {
// Verify that the channel is in fact a NIX wrapper.
if (typeof channel != 'unknown' ||
!(goog.net.xpc.NixTransport.NIX_ID_FIELD in channel)) {
goog.log.error(goog.net.xpc.logger,
'Invalid NIX channel given to createChannel_');
}
this.nixChannel_ = channel;
// Ensure that the NIX channel given to use is valid.
var remoteAuthToken = this.nixChannel_['GetAuthToken']();
if (remoteAuthToken != this.remoteAuthToken_) {
goog.log.error(goog.net.xpc.logger, 'Invalid auth token from other party');
return;
}
// Indicate to the CrossPageChannel that the channel is setup
// and ready to use.
this.channel_.notifyConnected();
};
/**
* Internal method called by the other page, via the NIX wrapper,
* to deliver a message.
* @param {string} serviceName The name of the service the message is to be
* delivered to.
* @param {string} payload The message to process.
* @private
*/
goog.net.xpc.NixTransport.prototype.handleMessage_ =
function(serviceName, payload) {
/** @this {goog.net.xpc.NixTransport} */
var deliveryHandler = function() {
this.channel_.xpcDeliver(serviceName, payload);
};
this.getWindow().setTimeout(goog.bind(deliveryHandler, this), 1);
};
/**
* Sends a message.
* @param {string} service The name of the service the message is to be
* delivered to.
* @param {string} payload The message content.
* @override
*/
goog.net.xpc.NixTransport.prototype.send = function(service, payload) {
// Verify that the NIX channel we have is valid.
if (typeof(this.nixChannel_) !== 'unknown') {
goog.log.error(goog.net.xpc.logger, 'NIX channel not connected');
}
// Send the message via the NIX wrapper object.
this.nixChannel_['SendMessage'](service, payload);
};
/** @override */
goog.net.xpc.NixTransport.prototype.disposeInternal = function() {
goog.base(this, 'disposeInternal');
this.nixChannel_ = null;
};

View File

@@ -0,0 +1,72 @@
// 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 Standalone script to be included in the relay-document
* used by goog.net.xpc.IframeRelayTransport. This script will decode the
* fragment identifier, determine the target window object and deliver
* the data to it.
*
*/
goog.provide('goog.net.xpc.relay');
(function() {
// Decode the fragement identifier.
// location.href is expected to be structured as follows:
// <url>#<channel_name>[,<iframe_id>]|<data>
// Get the fragment identifier.
var raw = window.location.hash;
if (!raw) {
return;
}
if (raw.charAt(0) == '#') {
raw = raw.substring(1);
}
var pos = raw.indexOf('|');
var head = raw.substring(0, pos).split(',');
var channelName = head[0];
var iframeId = head.length == 2 ? head[1] : null;
var frame = raw.substring(pos + 1);
// Find the window object of the peer.
//
// The general structure of the frames looks like this:
// - peer1
// - relay2
// - peer2
// - relay1
//
// We are either relay1 or relay2.
var win;
if (iframeId) {
// We are relay2 and need to deliver the data to peer2.
win = window.parent.frames[iframeId];
} else {
// We are relay1 and need to deliver the data to peer1.
win = window.parent.parent;
}
// Deliver the data.
try {
win['xpcRelay'](channelName, frame);
} catch (e) {
// Nothing useful can be done here.
// It would be great to inform the sender the delivery of this message
// failed, but this is not possible because we are already in the receiver's
// domain at this point.
}
})();

View File

@@ -0,0 +1,105 @@
// 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 base class for transports.
*
*/
goog.provide('goog.net.xpc.Transport');
goog.require('goog.Disposable');
goog.require('goog.dom');
goog.require('goog.net.xpc');
/**
* The base class for transports.
* @param {goog.dom.DomHelper=} opt_domHelper The dom helper to use for
* finding the window objects.
* @constructor
* @extends {goog.Disposable};
*/
goog.net.xpc.Transport = function(opt_domHelper) {
goog.Disposable.call(this);
/**
* The dom helper to use for finding the window objects to reference.
* @type {goog.dom.DomHelper}
* @private
*/
this.domHelper_ = opt_domHelper || goog.dom.getDomHelper();
};
goog.inherits(goog.net.xpc.Transport, goog.Disposable);
/**
* The transport type.
* @type {number}
* @protected
*/
goog.net.xpc.Transport.prototype.transportType = 0;
/**
* @return {number} The transport type identifier.
*/
goog.net.xpc.Transport.prototype.getType = function() {
return this.transportType;
};
/**
* Returns the window associated with this transport instance.
* @return {Window} The window to use.
*/
goog.net.xpc.Transport.prototype.getWindow = function() {
return this.domHelper_.getWindow();
};
/**
* Return the transport name.
* @return {string} the transport name.
*/
goog.net.xpc.Transport.prototype.getName = function() {
return goog.net.xpc.TransportNames[this.transportType] || '';
};
/**
* Handles transport service messages (internal signalling).
* @param {string} payload The message content.
*/
goog.net.xpc.Transport.prototype.transportServiceHandler = goog.abstractMethod;
/**
* Connects this transport.
* The transport implementation is expected to call
* CrossPageChannel.prototype.notifyConnected when the channel is ready
* to be used.
*/
goog.net.xpc.Transport.prototype.connect = goog.abstractMethod;
/**
* Sends a message.
* @param {string} service The name off the service the message is to be
* delivered to.
* @param {string} payload The message content.
*/
goog.net.xpc.Transport.prototype.send = goog.abstractMethod;

View File

@@ -0,0 +1,293 @@
// 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 Provides the namesspace for client-side communication
* between pages originating from different domains (it works also
* with pages from the same domain, but doing that is kinda
* pointless).
*
* The only publicly visible class is goog.net.xpc.CrossPageChannel.
*
* Note: The preferred name for the main class would have been
* CrossDomainChannel. But as there already is a class named like
* that (which serves a different purpose) in the maps codebase,
* CrossPageChannel was chosen to avoid confusion.
*
* CrossPageChannel abstracts the underlying transport mechanism to
* provide a common interface in all browsers.
*
*/
/*
TODO(user)
- resolve fastback issues in Safari (IframeRelayTransport)
*/
/**
* Namespace for CrossPageChannel
*/
goog.provide('goog.net.xpc');
goog.provide('goog.net.xpc.CfgFields');
goog.provide('goog.net.xpc.ChannelStates');
goog.provide('goog.net.xpc.TransportNames');
goog.provide('goog.net.xpc.TransportTypes');
goog.provide('goog.net.xpc.UriCfgFields');
goog.require('goog.log');
/**
* Enum used to identify transport types.
* @enum {number}
*/
goog.net.xpc.TransportTypes = {
NATIVE_MESSAGING: 1,
FRAME_ELEMENT_METHOD: 2,
IFRAME_RELAY: 3,
IFRAME_POLLING: 4,
FLASH: 5,
NIX: 6
};
/**
* Enum containing transport names. These need to correspond to the
* transport class names for createTransport_() to work.
* @type {Object}
*/
goog.net.xpc.TransportNames = {
'1': 'NativeMessagingTransport',
'2': 'FrameElementMethodTransport',
'3': 'IframeRelayTransport',
'4': 'IframePollingTransport',
'5': 'FlashTransport',
'6': 'NixTransport'
};
// TODO(user): Add auth token support to other methods.
/**
* Field names used on configuration object.
* @type {Object}
*/
goog.net.xpc.CfgFields = {
/**
* Channel name identifier.
* Both peers have to be initialized with
* the same channel name. If not present, a channel name is
* generated (which then has to transferred to the peer somehow).
*/
CHANNEL_NAME: 'cn',
/**
* Authorization token. If set, NIX will use this authorization token
* to validate the setup.
*/
AUTH_TOKEN: 'at',
/**
* Remote party's authorization token. If set, NIX will validate this
* authorization token against that sent by the other party.
*/
REMOTE_AUTH_TOKEN: 'rat',
/**
* The URI of the peer page.
*/
PEER_URI: 'pu',
/**
* Ifame-ID identifier.
* The id of the iframe element the peer-document lives in.
*/
IFRAME_ID: 'ifrid',
/**
* Transport type identifier.
* The transport type to use. Possible values are entries from
* goog.net.xpc.TransportTypes. If not present, the transport is
* determined automatically based on the useragent's capabilities.
*/
TRANSPORT: 'tp',
/**
* Local relay URI identifier (IframeRelayTransport-specific).
* The URI (can't contain a fragment identifier) used by the peer to
* relay data through.
*/
LOCAL_RELAY_URI: 'lru',
/**
* Peer relay URI identifier (IframeRelayTransport-specific).
* The URI (can't contain a fragment identifier) used to relay data
* to the peer.
*/
PEER_RELAY_URI: 'pru',
/**
* Local poll URI identifier (IframePollingTransport-specific).
* The URI (can't contain a fragment identifier)which is polled
* to receive data from the peer.
*/
LOCAL_POLL_URI: 'lpu',
/**
* Local poll URI identifier (IframePollingTransport-specific).
* The URI (can't contain a fragment identifier) used to send data
* to the peer.
*/
PEER_POLL_URI: 'ppu',
/**
* The hostname of the peer window, including protocol, domain, and port
* (if specified). Used for security sensitive applications that make
* use of NativeMessagingTransport (i.e. most applications).
*/
PEER_HOSTNAME: 'ph',
/**
* Usually both frames using a connection initially send a SETUP message to
* each other, and each responds with a SETUP_ACK. A frame marks itself
* connected when it receives that SETUP_ACK. If this parameter is true
* however, the channel it is passed to will not send a SETUP, but rather will
* wait for one from its peer and mark itself connected when that arrives.
* Peer iframes created using such a channel will send SETUP however, and will
* wait for SETUP_ACK before marking themselves connected. The goal is to
* cope with a situation where the availability of the URL for the peer frame
* cannot be relied on, eg when the application is offline. Without this
* setting, the primary frame will attempt to send its SETUP message every
* 100ms, forever. This floods the javascript console with uncatchable
* security warnings, and fruitlessly burns CPU. There is one scenario this
* mode will not support, and that is reconnection by the outer frame, ie the
* creation of a new channel object to connect to a peer iframe which was
* already communicating with a previous channel object of the same name. If
* that behavior is needed, this mode should not be used. Reconnection by
* inner frames is supported in this mode however.
*/
ONE_SIDED_HANDSHAKE: 'osh',
/**
* The frame role (inner or outer). Used to explicitly indicate the role for
* each peer whenever the role cannot be reliably determined (e.g. the two
* peer windows are not parent/child frames). If unspecified, the role will
* be dynamically determined, assuming a parent/child frame setup.
*/
ROLE: 'role',
/**
* Which version of the native transport startup protocol should be used, the
* default being '2'. Version 1 had various timing vulnerabilities, which
* had to be compensated for by introducing delays, and is deprecated. V1
* and V2 are broadly compatible, although the more robust timing and lack
* of delays is not gained unless both sides are using V2. The only
* unsupported case of cross-protocol interoperation is where a connection
* starts out with V2 at both ends, and one of the ends reconnects as a V1.
* All other initial startup and reconnection scenarios are supported.
*/
NATIVE_TRANSPORT_PROTOCOL_VERSION: 'nativeProtocolVersion'
};
/**
* Config properties that need to be URL sanitized.
* @type {Array}.<string>
*/
goog.net.xpc.UriCfgFields = [
goog.net.xpc.CfgFields.PEER_URI,
goog.net.xpc.CfgFields.LOCAL_RELAY_URI,
goog.net.xpc.CfgFields.PEER_RELAY_URI,
goog.net.xpc.CfgFields.LOCAL_POLL_URI,
goog.net.xpc.CfgFields.PEER_POLL_URI
];
/**
* @enum {number}
*/
goog.net.xpc.ChannelStates = {
NOT_CONNECTED: 1,
CONNECTED: 2,
CLOSED: 3
};
/**
* The name of the transport service (used for internal signalling).
* @type {string}
* @suppress {underscore}
*/
goog.net.xpc.TRANSPORT_SERVICE_ = 'tp';
/**
* Transport signaling message: setup.
* @type {string}
*/
goog.net.xpc.SETUP = 'SETUP';
/**
* Transport signaling message: setup for native transport protocol v2.
* @type {string}
*/
goog.net.xpc.SETUP_NTPV2 = 'SETUP_NTPV2';
/**
* Transport signaling message: setup acknowledgement.
* @type {string}
* @suppress {underscore}
*/
goog.net.xpc.SETUP_ACK_ = 'SETUP_ACK';
/**
* Transport signaling message: setup acknowledgement.
* @type {string}
*/
goog.net.xpc.SETUP_ACK_NTPV2 = 'SETUP_ACK_NTPV2';
/**
* Object holding active channels.
* Package private. Do not call from outside goog.net.xpc.
*
* @type {Object.<string, goog.net.xpc.CrossPageChannel>}
*/
goog.net.xpc.channels = {};
/**
* Returns a random string.
* @param {number} length How many characters the string shall contain.
* @param {string=} opt_characters The characters used.
* @return {string} The random string.
*/
goog.net.xpc.getRandomString = function(length, opt_characters) {
var chars = opt_characters || goog.net.xpc.randomStringCharacters_;
var charsLength = chars.length;
var s = '';
while (length-- > 0) {
s += chars.charAt(Math.floor(Math.random() * charsLength));
}
return s;
};
/**
* The default characters used for random string generation.
* @type {string}
* @private
*/
goog.net.xpc.randomStringCharacters_ =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
/**
* The logger.
* @type {goog.log.Logger}
*/
goog.net.xpc.logger = goog.log.getLogger('goog.net.xpc');