/**
* @license
* Copyright The Closure Library Authors.
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @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.Uri');
goog.require('goog.async.Deferred');
goog.require('goog.async.Delay');
goog.require('goog.dispose');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.dom.safe');
goog.require('goog.events');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventType');
goog.require('goog.html.legacyconversions');
goog.require('goog.json');
goog.require('goog.log');
goog.require('goog.messaging.AbstractChannel');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CfgFields');
goog.require('goog.net.xpc.ChannelStates');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.DirectTransport');
goog.require('goog.net.xpc.NativeMessagingTransport');
goog.require('goog.net.xpc.TransportTypes');
goog.require('goog.net.xpc.UriCfgFields');
goog.require('goog.string');
goog.require('goog.uri.utils');
goog.require('goog.userAgent');
goog.requireType('goog.net.xpc.Transport');
/**
* 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}
* @deprecated Prefer goog.messaging.MessageChannel and friends.
*/
goog.net.xpc.CrossPageChannel = function(cfg, opt_domHelper) {
'use strict';
goog.net.xpc.CrossPageChannel.base(this, 'constructor');
for (let i = 0, uriField; uriField = goog.net.xpc.UriCfgFields[i]; i++) {
if (uriField in cfg && !/^https?:\/\//.test(cfg[uriField])) {
throw new 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. Please use
* <code>updateChannelNameAndCatalog</code> to change this from the transports
* vs changing the property directly.
* @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<!goog.net.xpc.CrossPageChannel>}
* @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.CrossPageChannel.channels[this.name] = this;
if (!goog.events.getListener(
window, goog.events.EventType.UNLOAD,
goog.net.xpc.CrossPageChannel.disposeAll_)) {
// Set listener to dispose all registered channels on page unload.
goog.events.listenOnce(
window, goog.events.EventType.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() {
'use strict';
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 {?HTMLIFrameElement}
* @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() {
'use strict';
return this.cfg_;
};
/**
* Returns a reference to the iframe-element.
* Package private. Do not call from outside goog.net.xpc.
*
* @return {?HTMLIFrameElement} A reference to the iframe-element.
*/
goog.net.xpc.CrossPageChannel.prototype.getIframeElement = function() {
'use strict';
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) {
'use strict';
this.peerWindowObject_ = peerWindowObject;
};
/**
* Returns the window object the foreign document resides in.
*
* @return {Object} The window object of the peer.
* @package
*/
goog.net.xpc.CrossPageChannel.prototype.getPeerWindowObject = function() {
'use strict';
return this.peerWindowObject_;
};
/**
* Determines whether the peer window is available (e.g. not closed).
*
* @return {boolean} Whether the peer window is available.
* @package
*/
goog.net.xpc.CrossPageChannel.prototype.isPeerAvailable = function() {
'use strict';
// 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_ && !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} The best transport type.
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.determineTransportType_ = function() {
'use strict';
let transportType;
if (typeof document.postMessage === 'function' ||
typeof window.postMessage === 'function' ||
// IE8 supports window.postMessage, but
// typeof window.postMessage returns "object"
(goog.userAgent.IE && window.postMessage)) {
transportType = goog.net.xpc.TransportTypes.NATIVE_MESSAGING;
} else {
transportType = goog.net.xpc.TransportTypes.UNDEFINED;
}
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() {
'use strict';
// return, if the transport has already been created
if (this.transport_) {
return;
}
// TODO(user): Use goog.scope.
const CfgFields = goog.net.xpc.CfgFields;
if (!this.cfg_[CfgFields.TRANSPORT]) {
this.cfg_[CfgFields.TRANSPORT] = this.determineTransportType_();
}
// If TRANSPORT cfg is a function, we assume it's a constructor to a
// Transport implementation. Allows fine-grained dependency control over
// what Transport impls are brought in.
if (typeof this.cfg_[CfgFields.TRANSPORT] === 'function') {
this.transport_ = /** @type {!goog.net.xpc.Transport} */ (
new this.cfg_[CfgFields.TRANSPORT](this, this.domHelper_));
} else {
switch (this.cfg_[CfgFields.TRANSPORT]) {
case goog.net.xpc.TransportTypes.NATIVE_MESSAGING:
const protocolVersion =
this.cfg_[CfgFields.NATIVE_TRANSPORT_PROTOCOL_VERSION] || 2;
this.transport_ = new goog.net.xpc.NativeMessagingTransport(
this, this.cfg_[CfgFields.PEER_HOSTNAME], this.domHelper_,
!!this.cfg_[CfgFields.ONE_SIDED_HANDSHAKE], protocolVersion);
break;
case goog.net.xpc.TransportTypes.DIRECT:
if (this.peerWindowObject_ &&
goog.net.xpc.DirectTransport.isSupported(
/** @type {!Window} */ (this.peerWindowObject_))) {
this.transport_ =
new goog.net.xpc.DirectTransport(this, this.domHelper_);
} else {
goog.log.info(
goog.net.xpc.logger,
'DirectTransport not supported for this window, peer window in' +
' different security context or not set yet.');
}
break;
}
}
if (this.transport_) {
goog.log.info(
goog.net.xpc.logger, 'Transport created: ' + this.transport_.getName());
} else {
throw new Error(
'CrossPageChannel: No suitable transport found! You may ' +
'try injecting a Transport constructor directly via the channel ' +
'config object.');
}
};
/**
* Returns the transport type in use for this channel.
* @return {number} Transport-type identifier.
*/
goog.net.xpc.CrossPageChannel.prototype.getTransportType = function() {
'use strict';
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() {
'use strict';
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() {
'use strict';
const 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];
}
const 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) {
'use strict';
goog.log.info(goog.net.xpc.logger, 'createPeerIframe()');
let 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.
const iframeElm =
goog.dom.getDomHelper(parentElm).createElement(goog.dom.TagName.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);
const peerUri = this.getPeerUri(opt_addCfgParam);
this.peerLoadHandler_.listenOnceWithScope(
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() {
'use strict';
parentElm.appendChild(iframeElm);
goog.dom.safe.setIframeSrc(
iframeElm,
goog.html.legacyconversions.trustedResourceUrlFromString(
peerUri.toString()));
goog.log.info(
goog.net.xpc.logger, 'peer iframe created (' + iframeId + ')');
}, this), 1);
} else {
goog.dom.safe.setIframeSrc(
iframeElm,
goog.html.legacyconversions.trustedResourceUrlFromString(
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() {
'use strict';
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) {
'use strict';
let peerUri = this.cfg_[goog.net.xpc.CfgFields.PEER_URI];
if (typeof peerUri === 'string') {
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) {
'use strict';
this.connectCb_ = opt_connectCb || goog.nullFunction;
// If this channel was previously closed, transition back to the NOT_CONNECTED
// state to ensure that the connection can proceed (xpcDeliver blocks
// transport messages while the connection state is CLOSED).
if (this.state_ == goog.net.xpc.ChannelStates.CLOSED) {
this.state_ = goog.net.xpc.ChannelStates.NOT_CONNECTED;
}
// 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() {
'use strict';
goog.log.info(goog.net.xpc.logger, 'continueConnection_()');
this.peerWindowDeferred_ = null;
if (this.cfg_[goog.net.xpc.CfgFields.IFRAME_ID]) {
this.iframeElement_ = /** @type {?HTMLIFrameElement} */ (
this.domHelper_.getElement(
this.cfg_[goog.net.xpc.CfgFields.IFRAME_ID]));
}
if (this.iframeElement_) {
let 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 new 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() {
'use strict';
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) {
'use strict';
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 !== undefined) {
this.connectionDelay_ = new goog.async.Delay(this.connectCb_, opt_delay);
this.connectionDelay_.start();
} else {
this.connectionDelay_ = null;
this.connectCb_();
}
};
/**
* 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() {
'use strict';
goog.log.info(goog.net.xpc.logger, 'Transport Error');
this.close();
};
/** @override */
goog.net.xpc.CrossPageChannel.prototype.send = function(serviceName, payload) {
'use strict';
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 `deliver` function in superclass
* goog.messaging.AbstractChannel.
*
* @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.
* @package
*/
goog.net.xpc.CrossPageChannel.prototype.xpcDeliver = function(
serviceName, payload, opt_origin) {
'use strict';
// 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 there is another channel still open, the native transport's global
// postMessage listener will still be active. This will mean that messages
// being sent to the now-closed channel will still be received and delivered,
// such as transport service traffic from its previous correspondent in the
// other frame. Ensure these messages don't cause exceptions.
// Example: http://b/12419303
if (this.isDisposed() || this.state_ == goog.net.xpc.ChannelStates.CLOSED) {
goog.log.warning(
goog.net.xpc.logger, 'CrossPageChannel::xpcDeliver(): Channel closed.');
} 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) {
'use strict';
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) {
'use strict';
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() {
'use strict';
const role = this.cfg_[goog.net.xpc.CfgFields.ROLE];
if (typeof role === 'number') {
return role;
} else {
return window.parent == this.peerWindowObject_ ?
goog.net.xpc.CrossPageChannelRole.INNER :
goog.net.xpc.CrossPageChannelRole.OUTER;
}
};
/**
* Sets the channel name. Note, this doesn't establish a unique channel to
* communicate on.
* @param {string} name The new channel name.
*/
goog.net.xpc.CrossPageChannel.prototype.updateChannelNameAndCatalog = function(
name) {
'use strict';
goog.log.fine(goog.net.xpc.logger, 'changing channel name to ' + name);
delete goog.net.xpc.CrossPageChannel.channels[this.name];
this.name = name;
goog.net.xpc.CrossPageChannel.channels[name] = this;
};
/**
* 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.
* @package
*/
goog.net.xpc.CrossPageChannel.prototype.isMessageOriginAcceptable = function(
opt_origin) {
'use strict';
const peerHostname = this.cfg_[goog.net.xpc.CfgFields.PEER_HOSTNAME];
return goog.string.isEmptyOrWhitespace(goog.string.makeSafe(opt_origin)) ||
goog.string.isEmptyOrWhitespace(goog.string.makeSafe(peerHostname)) ||
opt_origin == this.cfg_[goog.net.xpc.CfgFields.PEER_HOSTNAME];
};
/** @override */
goog.net.xpc.CrossPageChannel.prototype.disposeInternal = function() {
'use strict';
this.close();
this.peerWindowObject_ = null;
this.iframeElement_ = null;
delete goog.net.xpc.CrossPageChannel.channels[this.name];
goog.dispose(this.peerLoadHandler_);
delete this.peerLoadHandler_;
goog.net.xpc.CrossPageChannel.base(this, 'disposeInternal');
};
/**
* Disposes all channels.
* @private
*/
goog.net.xpc.CrossPageChannel.disposeAll_ = function() {
'use strict';
for (let name in goog.net.xpc.CrossPageChannel.channels) {
goog.dispose(goog.net.xpc.CrossPageChannel.channels[name]);
}
};
/**
* Object holding active channels.
*
* @package {!Object<string, !goog.net.xpc.CrossPageChannel>}
*/
goog.net.xpc.CrossPageChannel.channels = {};