/**
* @license
* Copyright The Closure Library Authors.
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Provides an implementation of a transport that can call methods
* directly on a frame. Useful if you want to use XPC for crossdomain messaging
* (using another transport), or same domain messaging (using this transport).
*/
goog.provide('goog.net.xpc.DirectTransport');
goog.require('goog.Timer');
goog.require('goog.async.Deferred');
goog.require('goog.events.EventHandler');
goog.require('goog.log');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CfgFields');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.Transport');
goog.require('goog.net.xpc.TransportTypes');
goog.require('goog.object');
goog.requireType('goog.dom.DomHelper');
goog.requireType('goog.net.xpc.CrossPageChannel');
goog.scope(function() {
'use strict';
const CfgFields = goog.net.xpc.CfgFields;
const CrossPageChannelRole = goog.net.xpc.CrossPageChannelRole;
const Deferred = goog.async.Deferred;
const EventHandler = goog.events.EventHandler;
const Timer = goog.Timer;
const Transport = goog.net.xpc.Transport;
/**
* A direct window to window method transport.
*
* If the windows are in the same security context, this transport calls
* directly into the other window without using any additional mechanism. This
* is mainly used in scenarios where you want to optionally use a cross domain
* transport in cross security context situations, or optionally use a direct
* transport in same security context situations.
*
* Note: Global properties are exported by using this transport. One to
* communicate with the other window by, currently crosswindowmessaging.channel,
* and by using goog.getUid on window, currently closure_uid_[0-9]+.
*
* @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/document. If omitted, uses the current
* document.
* @constructor
* @extends {Transport}
*/
goog.net.xpc.DirectTransport = function(channel, opt_domHelper) {
'use strict';
goog.net.xpc.DirectTransport.base(this, 'constructor', opt_domHelper);
/**
* The channel this transport belongs to.
* @private {!goog.net.xpc.CrossPageChannel}
*/
this.channel_ = channel;
/** @private {!EventHandler<!goog.net.xpc.DirectTransport>} */
this.eventHandler_ = new EventHandler(this);
this.registerDisposable(this.eventHandler_);
/**
* Timer for connection reattempts.
* @private {!Timer}
*/
this.maybeAttemptToConnectTimer_ = new Timer(
DirectTransport.CONNECTION_ATTEMPT_INTERVAL_MS_, this.getWindow());
this.registerDisposable(this.maybeAttemptToConnectTimer_);
/**
* Fires once we've received our SETUP_ACK message.
* @private {!Deferred}
*/
this.setupAckReceived_ = new Deferred();
/**
* Fires once we've sent our SETUP_ACK message.
* @private {!Deferred}
*/
this.setupAckSent_ = new Deferred();
/**
* Fires once we're marked connected.
* @private {!Deferred}
*/
this.connected_ = new Deferred();
/**
* The unique ID of this side of the connection. Used to determine when a peer
* is reloaded.
* @private {string}
*/
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.
* @private {?string}
*/
this.peerEndpointId_ = null;
/**
* The map of sending messages.
* @private {Object}
*/
this.asyncSendsMap_ = {};
/**
* The original channel name.
* @private {string}
*/
this.originalChannelName_ = this.channel_.name;
// We reconfigure the channel name to include the role so that we can
// communicate in the same window between the different roles on the
// same channel.
this.channel_.updateChannelNameAndCatalog(
DirectTransport.getRoledChannelName_(
this.channel_.name, this.channel_.getRole()));
/**
* Flag indicating if this instance of the transport has been initialized.
* @private {boolean}
*/
this.initialized_ = false;
// 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.
// Two sided handshake:
// SETUP_ACK has to have been received, and sent.
this.connected_.awaitDeferred(this.setupAckReceived_);
this.connected_.awaitDeferred(this.setupAckSent_);
this.connected_.addCallback(this.notifyConnected_, this);
this.connected_.callback(true);
this.eventHandler_.listen(
this.maybeAttemptToConnectTimer_, Timer.TICK,
this.maybeAttemptToConnect_);
goog.log.info(
goog.net.xpc.logger,
'DirectTransport created. role=' + this.channel_.getRole());
};
goog.inherits(goog.net.xpc.DirectTransport, Transport);
const DirectTransport = goog.net.xpc.DirectTransport;
/**
* @private {number}
* @const
*/
DirectTransport.CONNECTION_ATTEMPT_INTERVAL_MS_ = 100;
/**
* The delay to notify the xpc of a successful connection. This is used
* to allow both parties to be connected if one party's connection callback
* invokes an immediate send.
* @private {number}
* @const
*/
DirectTransport.CONNECTION_DELAY_INTERVAL_MS_ = 0;
/**
* @param {!Window} peerWindow The peer window to check if DirectTranport is
* supported on.
* @return {boolean} Whether this transport is supported.
*/
DirectTransport.isSupported = function(peerWindow) {
'use strict';
try {
return window.document.domain == peerWindow.document.domain;
} catch (e) {
return false;
}
};
/**
* Tracks the number of DirectTransport 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.
* @private {!Object<number>}
*/
DirectTransport.activeCount_ = {};
/**
* Path of global message proxy.
* @private {string}
* @const
*/
// TODO(user): Make this configurable using the CfgFields.
DirectTransport.GLOBAL_TRANPORT_PATH_ = 'crosswindowmessaging.channel';
/**
* The delimiter used for transport service messages.
* @private {string}
* @const
*/
DirectTransport.MESSAGE_DELIMITER_ = ',';
/**
* Initializes this transport. Registers a method for 'message'-events in the
* global scope.
* @param {!Window} listenWindow The window to listen to events on.
* @private
*/
DirectTransport.initialize_ = function(listenWindow) {
'use strict';
const uid = goog.getUid(listenWindow);
const value = DirectTransport.activeCount_[uid] || 0;
if (value == 0) {
// Set up a handler on the window to proxy messages to class.
const globalProxy = goog.getObjectByName(
DirectTransport.GLOBAL_TRANPORT_PATH_, listenWindow);
if (globalProxy == null) {
goog.exportSymbol(
DirectTransport.GLOBAL_TRANPORT_PATH_,
DirectTransport.messageReceivedHandler_, listenWindow);
}
}
DirectTransport.activeCount_[uid]++;
};
/**
* @param {string} channelName The channel name.
* @param {string|number} role The role.
* @return {string} The formatted channel name including role.
* @private
*/
DirectTransport.getRoledChannelName_ = function(channelName, role) {
'use strict';
return channelName + '_' + role;
};
/**
* @param {!Object} literal The literal unrenamed message.
* @return {boolean} Whether the message was successfully delivered to a
* channel.
* @private
*/
DirectTransport.messageReceivedHandler_ = function(literal) {
'use strict';
const msg = DirectTransport.Message_.fromLiteral(literal);
const channelName = msg.channelName;
const service = msg.service;
const payload = msg.payload;
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)
const allChannels = goog.module.get('goog.net.xpc.CrossPageChannel').channels;
const channel = allChannels[channelName];
if (channel) {
channel.xpcDeliver(service, payload);
return true;
}
const transportMessageType =
DirectTransport.parseTransportPayload_(payload)[0];
// Check if there are any stale channel names that can be updated.
for (let staleChannelName in allChannels) {
const staleChannel = allChannels[staleChannelName];
if (staleChannel.getRole() == CrossPageChannelRole.INNER &&
!staleChannel.isConnected() &&
service == goog.net.xpc.TRANSPORT_SERVICE &&
transportMessageType == goog.net.xpc.SETUP) {
// 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+).
staleChannel.updateChannelNameAndCatalog(channelName);
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;
};
/**
* The transport type.
* @type {number}
* @override
*/
DirectTransport.prototype.transportType = goog.net.xpc.TransportTypes.DIRECT;
/**
* Handles transport service messages.
* @param {string} payload The message content.
* @override
*/
DirectTransport.prototype.transportServiceHandler = function(payload) {
'use strict';
const transportParts = DirectTransport.parseTransportPayload_(payload);
const transportMessageType = transportParts[0];
const peerEndpointId = transportParts[1];
switch (transportMessageType) {
case goog.net.xpc.SETUP_ACK:
if (!this.setupAckReceived_.hasFired()) {
this.setupAckReceived_.callback(true);
}
break;
case goog.net.xpc.SETUP:
this.sendSetupAckMessage_();
if ((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.
* @private
*/
DirectTransport.prototype.sendSetupMessage_ = function() {
'use strict';
// Although we could send real objects, since some other transports are
// limited to strings we also keep this requirement.
let payload = goog.net.xpc.SETUP;
payload += DirectTransport.MESSAGE_DELIMITER_;
payload += this.endpointId_;
this.send(goog.net.xpc.TRANSPORT_SERVICE, payload);
};
/**
* Sends a SETUP_ACK transport service message.
* @private
*/
DirectTransport.prototype.sendSetupAckMessage_ = function() {
'use strict';
this.send(goog.net.xpc.TRANSPORT_SERVICE, goog.net.xpc.SETUP_ACK);
if (!this.setupAckSent_.hasFired()) {
this.setupAckSent_.callback(true);
}
};
/** @override */
DirectTransport.prototype.connect = function() {
'use strict';
const win = this.getWindow();
if (win) {
DirectTransport.initialize_(win);
this.initialized_ = true;
this.maybeAttemptToConnect_();
} else {
goog.log.fine(goog.net.xpc.logger, 'connect(): no window to initialize.');
}
};
/**
* 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
*/
DirectTransport.prototype.maybeAttemptToConnect_ = function() {
'use strict';
if (this.channel_.isConnected()) {
this.maybeAttemptToConnectTimer_.stop();
return;
}
this.maybeAttemptToConnectTimer_.start();
this.sendSetupMessage_();
};
/**
* Prepares to send a message.
* @param {string} service The name of the service the message is to be
* delivered to.
* @param {string} payload The message content.
* @override
*/
DirectTransport.prototype.send = function(service, payload) {
'use strict';
if (!this.channel_.getPeerWindowObject()) {
goog.log.fine(goog.net.xpc.logger, 'send(): window not ready');
return;
}
const channelName = DirectTransport.getRoledChannelName_(
this.originalChannelName_, this.getPeerRole_());
const message = new DirectTransport.Message_(channelName, service, payload);
if (this.channel_.getConfig()[CfgFields.DIRECT_TRANSPORT_SYNC_MODE]) {
this.executeScheduledSend_(message);
} else {
// Note: goog.async.nextTick doesn't support cancelling or disposal so
// leaving as 0ms timer, though this may have performance implications.
this.asyncSendsMap_[goog.getUid(message)] =
Timer.callOnce(goog.bind(this.executeScheduledSend_, this, message), 0);
}
};
/**
* Sends the message.
* @param {!DirectTransport.Message_} message The message to send.
* @private
*/
DirectTransport.prototype.executeScheduledSend_ = function(message) {
'use strict';
const messageId = goog.getUid(message);
if (this.asyncSendsMap_[messageId]) {
delete this.asyncSendsMap_[messageId];
}
let peerProxy;
try {
peerProxy = goog.getObjectByName(
DirectTransport.GLOBAL_TRANPORT_PATH_,
this.channel_.getPeerWindowObject());
} catch (error) {
goog.log.warning(
goog.net.xpc.logger, 'Can\'t access other window, ignoring.', error);
return;
}
if (peerProxy === null) {
goog.log.warning(
goog.net.xpc.logger, 'Peer window had no global function.');
return;
}
try {
peerProxy(message.toLiteral());
goog.log.info(
goog.net.xpc.logger, 'send(): channelName=' + message.channelName +
' service=' + message.service + ' payload=' + message.payload);
} catch (error) {
goog.log.warning(
goog.net.xpc.logger, 'Error performing call, ignoring.', error);
}
};
/**
* @return {goog.net.xpc.CrossPageChannelRole} The role of peer channel (either
* inner or outer).
* @private
*/
DirectTransport.prototype.getPeerRole_ = function() {
'use strict';
const role = this.channel_.getRole();
return role == goog.net.xpc.CrossPageChannelRole.OUTER ?
goog.net.xpc.CrossPageChannelRole.INNER :
goog.net.xpc.CrossPageChannelRole.OUTER;
};
/**
* Notifies the channel that this transport is connected.
* @private
*/
DirectTransport.prototype.notifyConnected_ = function() {
'use strict';
// Add a delay as the connection callback will break if this transport is
// synchronous and the callback invokes send() immediately.
this.channel_.notifyConnected(
this.channel_.getConfig()[CfgFields.DIRECT_TRANSPORT_SYNC_MODE] ?
DirectTransport.CONNECTION_DELAY_INTERVAL_MS_ :
0);
};
/** @override */
DirectTransport.prototype.disposeInternal = function() {
'use strict';
if (this.initialized_) {
const listenWindow = this.getWindow();
const uid = goog.getUid(listenWindow);
const value = --DirectTransport.activeCount_[uid];
if (value == 1) {
goog.exportSymbol(
DirectTransport.GLOBAL_TRANPORT_PATH_, null, listenWindow);
}
}
if (this.asyncSendsMap_) {
goog.object.forEach(this.asyncSendsMap_, function(timerId) {
'use strict';
Timer.clear(timerId);
});
this.asyncSendsMap_ = null;
}
// Deferred's aren't disposables.
if (this.setupAckReceived_) {
this.setupAckReceived_.cancel();
delete this.setupAckReceived_;
}
if (this.setupAckSent_) {
this.setupAckSent_.cancel();
delete this.setupAckSent_;
}
if (this.connected_) {
this.connected_.cancel();
delete this.connected_;
}
DirectTransport.base(this, 'disposeInternal');
};
/**
* Parses a transport service payload message.
* @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
*/
DirectTransport.parseTransportPayload_ = function(payload) {
'use strict';
const transportParts = /** @type {!Array<?string>} */ (
payload.split(DirectTransport.MESSAGE_DELIMITER_));
transportParts[1] = transportParts[1] || null; // Usually endpointId.
return transportParts;
};
/**
* Message container that gets passed back and forth between windows.
* @param {string} channelName The channel name to tranport messages on.
* @param {string} service The service to send the payload to.
* @param {string} payload The payload to send.
* @constructor
* @struct
* @private
*/
DirectTransport.Message_ = function(channelName, service, payload) {
'use strict';
/**
* The name of the channel.
* @type {string}
*/
this.channelName = channelName;
/**
* The service on the channel.
* @type {string}
*/
this.service = service;
/**
* The payload.
* @type {string}
*/
this.payload = payload;
};
/**
* Converts a message to a literal object.
* @return {!Object} The message as a literal object.
*/
DirectTransport.Message_.prototype.toLiteral = function() {
'use strict';
return {
'channelName': this.channelName,
'service': this.service,
'payload': this.payload,
};
};
/**
* Creates a Message_ from a literal object.
* @param {!Object} literal The literal to convert to Message.
* @return {!DirectTransport.Message_} The Message.
*/
DirectTransport.Message_.fromLiteral = function(literal) {
'use strict';
return new DirectTransport.Message_(
literal['channelName'], literal['service'], literal['payload']);
};
}); // goog.scope