crosspagechannel.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851
  1. // Copyright 2007 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Provides the class CrossPageChannel, the main class in
  16. * goog.net.xpc.
  17. *
  18. * @see ../../demos/xpc/index.html
  19. */
  20. goog.provide('goog.net.xpc.CrossPageChannel');
  21. goog.require('goog.Uri');
  22. goog.require('goog.async.Deferred');
  23. goog.require('goog.async.Delay');
  24. goog.require('goog.dispose');
  25. goog.require('goog.dom');
  26. goog.require('goog.dom.TagName');
  27. goog.require('goog.events');
  28. goog.require('goog.events.EventHandler');
  29. goog.require('goog.events.EventType');
  30. goog.require('goog.json');
  31. goog.require('goog.log');
  32. goog.require('goog.messaging.AbstractChannel');
  33. goog.require('goog.net.xpc');
  34. goog.require('goog.net.xpc.CfgFields');
  35. goog.require('goog.net.xpc.ChannelStates');
  36. goog.require('goog.net.xpc.CrossPageChannelRole');
  37. goog.require('goog.net.xpc.DirectTransport');
  38. goog.require('goog.net.xpc.FrameElementMethodTransport');
  39. goog.require('goog.net.xpc.IframePollingTransport');
  40. goog.require('goog.net.xpc.IframeRelayTransport');
  41. goog.require('goog.net.xpc.NativeMessagingTransport');
  42. goog.require('goog.net.xpc.NixTransport');
  43. goog.require('goog.net.xpc.TransportTypes');
  44. goog.require('goog.net.xpc.UriCfgFields');
  45. goog.require('goog.string');
  46. goog.require('goog.uri.utils');
  47. goog.require('goog.userAgent');
  48. /**
  49. * A communication channel between two documents from different domains.
  50. * Provides asynchronous messaging.
  51. *
  52. * @param {Object} cfg Channel configuration object.
  53. * @param {goog.dom.DomHelper=} opt_domHelper The optional dom helper to
  54. * use for looking up elements in the dom.
  55. * @constructor
  56. * @extends {goog.messaging.AbstractChannel}
  57. */
  58. goog.net.xpc.CrossPageChannel = function(cfg, opt_domHelper) {
  59. goog.net.xpc.CrossPageChannel.base(this, 'constructor');
  60. for (var i = 0, uriField; uriField = goog.net.xpc.UriCfgFields[i]; i++) {
  61. if (uriField in cfg && !/^https?:\/\//.test(cfg[uriField])) {
  62. throw Error('URI ' + cfg[uriField] + ' is invalid for field ' + uriField);
  63. }
  64. }
  65. /**
  66. * The configuration for this channel.
  67. * @type {Object}
  68. * @private
  69. */
  70. this.cfg_ = cfg;
  71. /**
  72. * The name of the channel. Please use
  73. * <code>updateChannelNameAndCatalog</code> to change this from the transports
  74. * vs changing the property directly.
  75. * @type {string}
  76. */
  77. this.name = this.cfg_[goog.net.xpc.CfgFields.CHANNEL_NAME] ||
  78. goog.net.xpc.getRandomString(10);
  79. /**
  80. * The dom helper to use for accessing the dom.
  81. * @type {goog.dom.DomHelper}
  82. * @private
  83. */
  84. this.domHelper_ = opt_domHelper || goog.dom.getDomHelper();
  85. /**
  86. * Collects deferred function calls which will be made once the connection
  87. * has been fully set up.
  88. * @type {!Array<function()>}
  89. * @private
  90. */
  91. this.deferredDeliveries_ = [];
  92. /**
  93. * An event handler used to listen for load events on peer iframes.
  94. * @type {!goog.events.EventHandler<!goog.net.xpc.CrossPageChannel>}
  95. * @private
  96. */
  97. this.peerLoadHandler_ = new goog.events.EventHandler(this);
  98. // If LOCAL_POLL_URI or PEER_POLL_URI is not available, try using
  99. // robots.txt from that host.
  100. cfg[goog.net.xpc.CfgFields.LOCAL_POLL_URI] =
  101. cfg[goog.net.xpc.CfgFields.LOCAL_POLL_URI] ||
  102. goog.uri.utils.getHost(this.domHelper_.getWindow().location.href) +
  103. '/robots.txt';
  104. // PEER_URI is sometimes undefined in tests.
  105. cfg[goog.net.xpc.CfgFields.PEER_POLL_URI] =
  106. cfg[goog.net.xpc.CfgFields.PEER_POLL_URI] ||
  107. goog.uri.utils.getHost(cfg[goog.net.xpc.CfgFields.PEER_URI] || '') +
  108. '/robots.txt';
  109. goog.net.xpc.channels[this.name] = this;
  110. if (!goog.events.getListener(
  111. window, goog.events.EventType.UNLOAD,
  112. goog.net.xpc.CrossPageChannel.disposeAll_)) {
  113. // Set listener to dispose all registered channels on page unload.
  114. goog.events.listenOnce(
  115. window, goog.events.EventType.UNLOAD,
  116. goog.net.xpc.CrossPageChannel.disposeAll_);
  117. }
  118. goog.log.info(goog.net.xpc.logger, 'CrossPageChannel created: ' + this.name);
  119. };
  120. goog.inherits(goog.net.xpc.CrossPageChannel, goog.messaging.AbstractChannel);
  121. /**
  122. * Regexp for escaping service names.
  123. * @type {RegExp}
  124. * @private
  125. */
  126. goog.net.xpc.CrossPageChannel.TRANSPORT_SERVICE_ESCAPE_RE_ =
  127. new RegExp('^%*' + goog.net.xpc.TRANSPORT_SERVICE_ + '$');
  128. /**
  129. * Regexp for unescaping service names.
  130. * @type {RegExp}
  131. * @private
  132. */
  133. goog.net.xpc.CrossPageChannel.TRANSPORT_SERVICE_UNESCAPE_RE_ =
  134. new RegExp('^%+' + goog.net.xpc.TRANSPORT_SERVICE_ + '$');
  135. /**
  136. * A delay between the transport reporting as connected and the calling of the
  137. * connection callback. Sometimes used to paper over timing vulnerabilities.
  138. * @type {goog.async.Delay}
  139. * @private
  140. */
  141. goog.net.xpc.CrossPageChannel.prototype.connectionDelay_ = null;
  142. /**
  143. * A deferred which is set to non-null while a peer iframe is being created
  144. * but has not yet thrown its load event, and which fires when that load event
  145. * arrives.
  146. * @type {goog.async.Deferred}
  147. * @private
  148. */
  149. goog.net.xpc.CrossPageChannel.prototype.peerWindowDeferred_ = null;
  150. /**
  151. * The transport.
  152. * @type {goog.net.xpc.Transport?}
  153. * @private
  154. */
  155. goog.net.xpc.CrossPageChannel.prototype.transport_ = null;
  156. /**
  157. * The channel state.
  158. * @type {number}
  159. * @private
  160. */
  161. goog.net.xpc.CrossPageChannel.prototype.state_ =
  162. goog.net.xpc.ChannelStates.NOT_CONNECTED;
  163. /**
  164. * @override
  165. * @return {boolean} Whether the channel is connected.
  166. */
  167. goog.net.xpc.CrossPageChannel.prototype.isConnected = function() {
  168. return this.state_ == goog.net.xpc.ChannelStates.CONNECTED;
  169. };
  170. /**
  171. * Reference to the window-object of the peer page.
  172. * @type {Object}
  173. * @private
  174. */
  175. goog.net.xpc.CrossPageChannel.prototype.peerWindowObject_ = null;
  176. /**
  177. * Reference to the iframe-element.
  178. * @type {?HTMLIFrameElement}
  179. * @private
  180. */
  181. goog.net.xpc.CrossPageChannel.prototype.iframeElement_ = null;
  182. /**
  183. * Returns the configuration object for this channel.
  184. * Package private. Do not call from outside goog.net.xpc.
  185. *
  186. * @return {Object} The configuration object for this channel.
  187. */
  188. goog.net.xpc.CrossPageChannel.prototype.getConfig = function() {
  189. return this.cfg_;
  190. };
  191. /**
  192. * Returns a reference to the iframe-element.
  193. * Package private. Do not call from outside goog.net.xpc.
  194. *
  195. * @return {?HTMLIFrameElement} A reference to the iframe-element.
  196. */
  197. goog.net.xpc.CrossPageChannel.prototype.getIframeElement = function() {
  198. return this.iframeElement_;
  199. };
  200. /**
  201. * Sets the window object the foreign document resides in.
  202. *
  203. * @param {Object} peerWindowObject The window object of the peer.
  204. */
  205. goog.net.xpc.CrossPageChannel.prototype.setPeerWindowObject = function(
  206. peerWindowObject) {
  207. this.peerWindowObject_ = peerWindowObject;
  208. };
  209. /**
  210. * Returns the window object the foreign document resides in.
  211. *
  212. * @return {Object} The window object of the peer.
  213. * @package
  214. */
  215. goog.net.xpc.CrossPageChannel.prototype.getPeerWindowObject = function() {
  216. return this.peerWindowObject_;
  217. };
  218. /**
  219. * Determines whether the peer window is available (e.g. not closed).
  220. *
  221. * @return {boolean} Whether the peer window is available.
  222. * @package
  223. */
  224. goog.net.xpc.CrossPageChannel.prototype.isPeerAvailable = function() {
  225. // NOTE(user): This check is not reliable in IE, where a document in an
  226. // iframe does not get unloaded when removing the iframe element from the DOM.
  227. // TODO(user): Find something that works in IE as well.
  228. // NOTE(user): "!this.peerWindowObject_.closed" evaluates to 'false' in IE9
  229. // sometimes even though typeof(this.peerWindowObject_.closed) is boolean and
  230. // this.peerWindowObject_.closed evaluates to 'false'. Casting it to a Boolean
  231. // results in sane evaluation. When this happens, it's in the inner iframe
  232. // when querying its parent's 'closed' status. Note that this is a different
  233. // case than mibuerge@'s note above.
  234. try {
  235. return !!this.peerWindowObject_ && !this.peerWindowObject_.closed;
  236. } catch (e) {
  237. // If the window is closing, an error may be thrown.
  238. return false;
  239. }
  240. };
  241. /**
  242. * Determine which transport type to use for this channel / useragent.
  243. * @return {goog.net.xpc.TransportTypes|undefined} The best transport type.
  244. * @private
  245. */
  246. goog.net.xpc.CrossPageChannel.prototype.determineTransportType_ = function() {
  247. var transportType;
  248. if (goog.isFunction(document.postMessage) ||
  249. goog.isFunction(window.postMessage) ||
  250. // IE8 supports window.postMessage, but
  251. // typeof window.postMessage returns "object"
  252. (goog.userAgent.IE && window.postMessage)) {
  253. transportType = goog.net.xpc.TransportTypes.NATIVE_MESSAGING;
  254. } else if (goog.userAgent.GECKO) {
  255. transportType = goog.net.xpc.TransportTypes.FRAME_ELEMENT_METHOD;
  256. } else if (
  257. goog.userAgent.IE && this.cfg_[goog.net.xpc.CfgFields.PEER_RELAY_URI]) {
  258. transportType = goog.net.xpc.TransportTypes.IFRAME_RELAY;
  259. } else if (goog.userAgent.IE && goog.net.xpc.NixTransport.isNixSupported()) {
  260. transportType = goog.net.xpc.TransportTypes.NIX;
  261. } else {
  262. transportType = goog.net.xpc.TransportTypes.IFRAME_POLLING;
  263. }
  264. return transportType;
  265. };
  266. /**
  267. * Creates the transport for this channel. Chooses from the available
  268. * transport based on the user agent and the configuration.
  269. * @private
  270. */
  271. goog.net.xpc.CrossPageChannel.prototype.createTransport_ = function() {
  272. // return, if the transport has already been created
  273. if (this.transport_) {
  274. return;
  275. }
  276. // TODO(user): Use goog.scope.
  277. var CfgFields = goog.net.xpc.CfgFields;
  278. if (!this.cfg_[CfgFields.TRANSPORT]) {
  279. this.cfg_[CfgFields.TRANSPORT] = this.determineTransportType_();
  280. }
  281. switch (this.cfg_[CfgFields.TRANSPORT]) {
  282. case goog.net.xpc.TransportTypes.NATIVE_MESSAGING:
  283. var protocolVersion =
  284. this.cfg_[CfgFields.NATIVE_TRANSPORT_PROTOCOL_VERSION] || 2;
  285. this.transport_ = new goog.net.xpc.NativeMessagingTransport(
  286. this, this.cfg_[CfgFields.PEER_HOSTNAME], this.domHelper_,
  287. !!this.cfg_[CfgFields.ONE_SIDED_HANDSHAKE], protocolVersion);
  288. break;
  289. case goog.net.xpc.TransportTypes.NIX:
  290. this.transport_ = new goog.net.xpc.NixTransport(this, this.domHelper_);
  291. break;
  292. case goog.net.xpc.TransportTypes.FRAME_ELEMENT_METHOD:
  293. this.transport_ =
  294. new goog.net.xpc.FrameElementMethodTransport(this, this.domHelper_);
  295. break;
  296. case goog.net.xpc.TransportTypes.IFRAME_RELAY:
  297. this.transport_ =
  298. new goog.net.xpc.IframeRelayTransport(this, this.domHelper_);
  299. break;
  300. case goog.net.xpc.TransportTypes.IFRAME_POLLING:
  301. this.transport_ =
  302. new goog.net.xpc.IframePollingTransport(this, this.domHelper_);
  303. break;
  304. case goog.net.xpc.TransportTypes.DIRECT:
  305. if (this.peerWindowObject_ &&
  306. goog.net.xpc.DirectTransport.isSupported(
  307. /** @type {!Window} */ (this.peerWindowObject_))) {
  308. this.transport_ =
  309. new goog.net.xpc.DirectTransport(this, this.domHelper_);
  310. } else {
  311. goog.log.info(
  312. goog.net.xpc.logger,
  313. 'DirectTransport not supported for this window, peer window in' +
  314. ' different security context or not set yet.');
  315. }
  316. break;
  317. }
  318. if (this.transport_) {
  319. goog.log.info(
  320. goog.net.xpc.logger, 'Transport created: ' + this.transport_.getName());
  321. } else {
  322. throw Error('CrossPageChannel: No suitable transport found!');
  323. }
  324. };
  325. /**
  326. * Returns the transport type in use for this channel.
  327. * @return {number} Transport-type identifier.
  328. */
  329. goog.net.xpc.CrossPageChannel.prototype.getTransportType = function() {
  330. return this.transport_.getType();
  331. };
  332. /**
  333. * Returns the tranport name in use for this channel.
  334. * @return {string} The transport name.
  335. */
  336. goog.net.xpc.CrossPageChannel.prototype.getTransportName = function() {
  337. return this.transport_.getName();
  338. };
  339. /**
  340. * @return {!Object} Configuration-object to be used by the peer to
  341. * initialize the channel.
  342. */
  343. goog.net.xpc.CrossPageChannel.prototype.getPeerConfiguration = function() {
  344. var peerCfg = {};
  345. peerCfg[goog.net.xpc.CfgFields.CHANNEL_NAME] = this.name;
  346. peerCfg[goog.net.xpc.CfgFields.TRANSPORT] =
  347. this.cfg_[goog.net.xpc.CfgFields.TRANSPORT];
  348. peerCfg[goog.net.xpc.CfgFields.ONE_SIDED_HANDSHAKE] =
  349. this.cfg_[goog.net.xpc.CfgFields.ONE_SIDED_HANDSHAKE];
  350. if (this.cfg_[goog.net.xpc.CfgFields.LOCAL_RELAY_URI]) {
  351. peerCfg[goog.net.xpc.CfgFields.PEER_RELAY_URI] =
  352. this.cfg_[goog.net.xpc.CfgFields.LOCAL_RELAY_URI];
  353. }
  354. if (this.cfg_[goog.net.xpc.CfgFields.LOCAL_POLL_URI]) {
  355. peerCfg[goog.net.xpc.CfgFields.PEER_POLL_URI] =
  356. this.cfg_[goog.net.xpc.CfgFields.LOCAL_POLL_URI];
  357. }
  358. if (this.cfg_[goog.net.xpc.CfgFields.PEER_POLL_URI]) {
  359. peerCfg[goog.net.xpc.CfgFields.LOCAL_POLL_URI] =
  360. this.cfg_[goog.net.xpc.CfgFields.PEER_POLL_URI];
  361. }
  362. var role = this.cfg_[goog.net.xpc.CfgFields.ROLE];
  363. if (role) {
  364. peerCfg[goog.net.xpc.CfgFields.ROLE] =
  365. role == goog.net.xpc.CrossPageChannelRole.INNER ?
  366. goog.net.xpc.CrossPageChannelRole.OUTER :
  367. goog.net.xpc.CrossPageChannelRole.INNER;
  368. }
  369. return peerCfg;
  370. };
  371. /**
  372. * Creates the iframe containing the peer page in a specified parent element.
  373. * This method does not connect the channel, connect() still has to be called
  374. * separately.
  375. *
  376. * @param {!Element} parentElm The container element the iframe is appended to.
  377. * @param {Function=} opt_configureIframeCb If present, this function gets
  378. * called with the iframe element as parameter to allow setting properties
  379. * on it before it gets added to the DOM. If absent, the iframe's width and
  380. * height are set to '100%'.
  381. * @param {boolean=} opt_addCfgParam Whether to add the peer configuration as
  382. * URL parameter (default: true).
  383. * @return {!HTMLIFrameElement} The iframe element.
  384. */
  385. goog.net.xpc.CrossPageChannel.prototype.createPeerIframe = function(
  386. parentElm, opt_configureIframeCb, opt_addCfgParam) {
  387. goog.log.info(goog.net.xpc.logger, 'createPeerIframe()');
  388. var iframeId = this.cfg_[goog.net.xpc.CfgFields.IFRAME_ID];
  389. if (!iframeId) {
  390. // Create a randomized ID for the iframe element to avoid
  391. // bfcache-related issues.
  392. iframeId = this.cfg_[goog.net.xpc.CfgFields.IFRAME_ID] =
  393. 'xpcpeer' + goog.net.xpc.getRandomString(4);
  394. }
  395. // TODO(user) Opera creates a history-entry when creating an iframe
  396. // programmatically as follows. Find a way which avoids this.
  397. var iframeElm =
  398. goog.dom.getDomHelper(parentElm).createElement(goog.dom.TagName.IFRAME);
  399. iframeElm.id = iframeElm.name = iframeId;
  400. if (opt_configureIframeCb) {
  401. opt_configureIframeCb(iframeElm);
  402. } else {
  403. iframeElm.style.width = iframeElm.style.height = '100%';
  404. }
  405. this.cleanUpIncompleteConnection_();
  406. this.peerWindowDeferred_ = new goog.async.Deferred(undefined, this);
  407. var peerUri = this.getPeerUri(opt_addCfgParam);
  408. this.peerLoadHandler_.listenOnceWithScope(
  409. iframeElm, 'load', this.peerWindowDeferred_.callback, false,
  410. this.peerWindowDeferred_);
  411. if (goog.userAgent.GECKO || goog.userAgent.WEBKIT) {
  412. // Appending the iframe in a timeout to avoid a weird fastback issue, which
  413. // is present in Safari and Gecko.
  414. window.setTimeout(goog.bind(function() {
  415. parentElm.appendChild(iframeElm);
  416. iframeElm.src = peerUri.toString();
  417. goog.log.info(
  418. goog.net.xpc.logger, 'peer iframe created (' + iframeId + ')');
  419. }, this), 1);
  420. } else {
  421. iframeElm.src = peerUri.toString();
  422. parentElm.appendChild(iframeElm);
  423. goog.log.info(
  424. goog.net.xpc.logger, 'peer iframe created (' + iframeId + ')');
  425. }
  426. return /** @type {!HTMLIFrameElement} */ (iframeElm);
  427. };
  428. /**
  429. * Clean up after any incomplete attempt to establish and connect to a peer
  430. * iframe.
  431. * @private
  432. */
  433. goog.net.xpc.CrossPageChannel.prototype.cleanUpIncompleteConnection_ =
  434. function() {
  435. if (this.peerWindowDeferred_) {
  436. this.peerWindowDeferred_.cancel();
  437. this.peerWindowDeferred_ = null;
  438. }
  439. this.deferredDeliveries_.length = 0;
  440. this.peerLoadHandler_.removeAll();
  441. };
  442. /**
  443. * Returns the peer URI, with an optional URL parameter for configuring the peer
  444. * window.
  445. *
  446. * @param {boolean=} opt_addCfgParam Whether to add the peer configuration as
  447. * URL parameter (default: true).
  448. * @return {!goog.Uri} The peer URI.
  449. */
  450. goog.net.xpc.CrossPageChannel.prototype.getPeerUri = function(opt_addCfgParam) {
  451. var peerUri = this.cfg_[goog.net.xpc.CfgFields.PEER_URI];
  452. if (goog.isString(peerUri)) {
  453. peerUri = this.cfg_[goog.net.xpc.CfgFields.PEER_URI] =
  454. new goog.Uri(peerUri);
  455. }
  456. // Add the channel configuration used by the peer as URL parameter.
  457. if (opt_addCfgParam !== false) {
  458. peerUri.setParameterValue(
  459. 'xpc', goog.json.serialize(this.getPeerConfiguration()));
  460. }
  461. return peerUri;
  462. };
  463. /**
  464. * Initiates connecting the channel. When this method is called, all the
  465. * information needed to connect the channel has to be available.
  466. *
  467. * @override
  468. * @param {Function=} opt_connectCb The function to be called when the
  469. * channel has been connected and is ready to be used.
  470. */
  471. goog.net.xpc.CrossPageChannel.prototype.connect = function(opt_connectCb) {
  472. this.connectCb_ = opt_connectCb || goog.nullFunction;
  473. // If this channel was previously closed, transition back to the NOT_CONNECTED
  474. // state to ensure that the connection can proceed (xpcDeliver blocks
  475. // transport messages while the connection state is CLOSED).
  476. if (this.state_ == goog.net.xpc.ChannelStates.CLOSED) {
  477. this.state_ = goog.net.xpc.ChannelStates.NOT_CONNECTED;
  478. }
  479. // If we know of a peer window whose creation has been requested but is not
  480. // complete, peerWindowDeferred_ will be non-null, and we should block on it.
  481. if (this.peerWindowDeferred_) {
  482. this.peerWindowDeferred_.addCallback(this.continueConnection_);
  483. } else {
  484. this.continueConnection_();
  485. }
  486. };
  487. /**
  488. * Continues the connection process once we're as sure as we can be that the
  489. * peer iframe has been created.
  490. * @private
  491. */
  492. goog.net.xpc.CrossPageChannel.prototype.continueConnection_ = function() {
  493. goog.log.info(goog.net.xpc.logger, 'continueConnection_()');
  494. this.peerWindowDeferred_ = null;
  495. if (this.cfg_[goog.net.xpc.CfgFields.IFRAME_ID]) {
  496. this.iframeElement_ = /** @type {?HTMLIFrameElement} */ (
  497. this.domHelper_.getElement(
  498. this.cfg_[goog.net.xpc.CfgFields.IFRAME_ID]));
  499. }
  500. if (this.iframeElement_) {
  501. var winObj = this.iframeElement_.contentWindow;
  502. // accessing the window using contentWindow doesn't work in safari
  503. if (!winObj) {
  504. winObj = window.frames[this.cfg_[goog.net.xpc.CfgFields.IFRAME_ID]];
  505. }
  506. this.setPeerWindowObject(winObj);
  507. }
  508. // if the peer window object has not been set at this point, we assume
  509. // being in an iframe and the channel is meant to be to the containing page
  510. if (!this.peerWindowObject_) {
  511. // throw an error if we are in the top window (== not in an iframe)
  512. if (window == window.top) {
  513. throw Error(
  514. "CrossPageChannel: Can't connect, peer window-object not set.");
  515. } else {
  516. this.setPeerWindowObject(window.parent);
  517. }
  518. }
  519. this.createTransport_();
  520. this.transport_.connect();
  521. // Now we run any deferred deliveries collected while connection was deferred.
  522. while (this.deferredDeliveries_.length > 0) {
  523. this.deferredDeliveries_.shift()();
  524. }
  525. };
  526. /**
  527. * Closes the channel.
  528. */
  529. goog.net.xpc.CrossPageChannel.prototype.close = function() {
  530. this.cleanUpIncompleteConnection_();
  531. this.state_ = goog.net.xpc.ChannelStates.CLOSED;
  532. goog.dispose(this.transport_);
  533. this.transport_ = null;
  534. this.connectCb_ = null;
  535. goog.dispose(this.connectionDelay_);
  536. this.connectionDelay_ = null;
  537. goog.log.info(goog.net.xpc.logger, 'Channel "' + this.name + '" closed');
  538. };
  539. /**
  540. * Package-private.
  541. * Called by the transport when the channel is connected.
  542. * @param {number=} opt_delay Delay this number of milliseconds before calling
  543. * the connection callback. Usage is discouraged, but can be used to paper
  544. * over timing vulnerabilities when there is no alternative.
  545. */
  546. goog.net.xpc.CrossPageChannel.prototype.notifyConnected = function(opt_delay) {
  547. if (this.isConnected() ||
  548. (this.connectionDelay_ && this.connectionDelay_.isActive())) {
  549. return;
  550. }
  551. this.state_ = goog.net.xpc.ChannelStates.CONNECTED;
  552. goog.log.info(goog.net.xpc.logger, 'Channel "' + this.name + '" connected');
  553. goog.dispose(this.connectionDelay_);
  554. if (goog.isDef(opt_delay)) {
  555. this.connectionDelay_ = new goog.async.Delay(this.connectCb_, opt_delay);
  556. this.connectionDelay_.start();
  557. } else {
  558. this.connectionDelay_ = null;
  559. this.connectCb_();
  560. }
  561. };
  562. /**
  563. * Called by the transport in case of an unrecoverable failure.
  564. * Package private. Do not call from outside goog.net.xpc.
  565. */
  566. goog.net.xpc.CrossPageChannel.prototype.notifyTransportError = function() {
  567. goog.log.info(goog.net.xpc.logger, 'Transport Error');
  568. this.close();
  569. };
  570. /** @override */
  571. goog.net.xpc.CrossPageChannel.prototype.send = function(serviceName, payload) {
  572. if (!this.isConnected()) {
  573. goog.log.error(goog.net.xpc.logger, 'Can\'t send. Channel not connected.');
  574. return;
  575. }
  576. // Check if the peer is still around.
  577. if (!this.isPeerAvailable()) {
  578. goog.log.error(goog.net.xpc.logger, 'Peer has disappeared.');
  579. this.close();
  580. return;
  581. }
  582. if (goog.isObject(payload)) {
  583. payload = goog.json.serialize(payload);
  584. }
  585. // Partially URL-encode the service name because some characters (: and |) are
  586. // used as delimiters for some transports, and we want to allow those
  587. // characters in service names.
  588. this.transport_.send(this.escapeServiceName_(serviceName), payload);
  589. };
  590. /**
  591. * Delivers messages to the appropriate service-handler. Named xpcDeliver to
  592. * avoid name conflict with {@code deliver} function in superclass
  593. * goog.messaging.AbstractChannel.
  594. *
  595. * @param {string} serviceName The name of the port.
  596. * @param {string} payload The payload.
  597. * @param {string=} opt_origin An optional origin for the message, where the
  598. * underlying transport makes that available. If this is specified, and
  599. * the PEER_HOSTNAME parameter was provided, they must match or the message
  600. * will be rejected.
  601. * @package
  602. */
  603. goog.net.xpc.CrossPageChannel.prototype.xpcDeliver = function(
  604. serviceName, payload, opt_origin) {
  605. // This check covers the very rare (but producable) case where the inner frame
  606. // becomes ready and sends its setup message while the outer frame is
  607. // deferring its connect method waiting for the inner frame to be ready. The
  608. // resulting deferral ensures the message will not be processed until the
  609. // channel is fully configured.
  610. if (this.peerWindowDeferred_) {
  611. this.deferredDeliveries_.push(
  612. goog.bind(this.xpcDeliver, this, serviceName, payload, opt_origin));
  613. return;
  614. }
  615. // Check whether the origin of the message is as expected.
  616. if (!this.isMessageOriginAcceptable(opt_origin)) {
  617. goog.log.warning(
  618. goog.net.xpc.logger, 'Message received from unapproved origin "' +
  619. opt_origin + '" - rejected.');
  620. return;
  621. }
  622. // If there is another channel still open, the native transport's global
  623. // postMessage listener will still be active. This will mean that messages
  624. // being sent to the now-closed channel will still be received and delivered,
  625. // such as transport service traffic from its previous correspondent in the
  626. // other frame. Ensure these messages don't cause exceptions.
  627. // Example: http://b/12419303
  628. if (this.isDisposed() || this.state_ == goog.net.xpc.ChannelStates.CLOSED) {
  629. goog.log.warning(
  630. goog.net.xpc.logger, 'CrossPageChannel::xpcDeliver(): Channel closed.');
  631. } else if (!serviceName || serviceName == goog.net.xpc.TRANSPORT_SERVICE_) {
  632. this.transport_.transportServiceHandler(payload);
  633. } else {
  634. // only deliver messages if connected
  635. if (this.isConnected()) {
  636. this.deliver(this.unescapeServiceName_(serviceName), payload);
  637. } else {
  638. goog.log.info(
  639. goog.net.xpc.logger,
  640. 'CrossPageChannel::xpcDeliver(): Not connected.');
  641. }
  642. }
  643. };
  644. /**
  645. * Escape the user-provided service name for sending across the channel. This
  646. * URL-encodes certain special characters so they don't conflict with delimiters
  647. * used by some of the transports, and adds a special prefix if the name
  648. * conflicts with the reserved transport service name.
  649. *
  650. * This is the opposite of {@link #unescapeServiceName_}.
  651. *
  652. * @param {string} name The name of the service to escape.
  653. * @return {string} The escaped service name.
  654. * @private
  655. */
  656. goog.net.xpc.CrossPageChannel.prototype.escapeServiceName_ = function(name) {
  657. if (goog.net.xpc.CrossPageChannel.TRANSPORT_SERVICE_ESCAPE_RE_.test(name)) {
  658. name = '%' + name;
  659. }
  660. return name.replace(/[%:|]/g, encodeURIComponent);
  661. };
  662. /**
  663. * Unescape the escaped service name that was sent across the channel. This is
  664. * the opposite of {@link #escapeServiceName_}.
  665. *
  666. * @param {string} name The name of the service to unescape.
  667. * @return {string} The unescaped service name.
  668. * @private
  669. */
  670. goog.net.xpc.CrossPageChannel.prototype.unescapeServiceName_ = function(name) {
  671. name = name.replace(/%[0-9a-f]{2}/gi, decodeURIComponent);
  672. if (goog.net.xpc.CrossPageChannel.TRANSPORT_SERVICE_UNESCAPE_RE_.test(name)) {
  673. return name.substring(1);
  674. } else {
  675. return name;
  676. }
  677. };
  678. /**
  679. * Returns the role of this channel (either inner or outer).
  680. * @return {number} The role of this channel.
  681. */
  682. goog.net.xpc.CrossPageChannel.prototype.getRole = function() {
  683. var role = this.cfg_[goog.net.xpc.CfgFields.ROLE];
  684. if (goog.isNumber(role)) {
  685. return role;
  686. } else {
  687. return window.parent == this.peerWindowObject_ ?
  688. goog.net.xpc.CrossPageChannelRole.INNER :
  689. goog.net.xpc.CrossPageChannelRole.OUTER;
  690. }
  691. };
  692. /**
  693. * Sets the channel name. Note, this doesn't establish a unique channel to
  694. * communicate on.
  695. * @param {string} name The new channel name.
  696. */
  697. goog.net.xpc.CrossPageChannel.prototype.updateChannelNameAndCatalog = function(
  698. name) {
  699. goog.log.fine(goog.net.xpc.logger, 'changing channel name to ' + name);
  700. delete goog.net.xpc.channels[this.name];
  701. this.name = name;
  702. goog.net.xpc.channels[name] = this;
  703. };
  704. /**
  705. * Returns whether an incoming message with the given origin is acceptable.
  706. * If an incoming request comes with a specified (non-empty) origin, and the
  707. * PEER_HOSTNAME config parameter has also been provided, the two must match,
  708. * or the message is unacceptable.
  709. * @param {string=} opt_origin The origin associated with the incoming message.
  710. * @return {boolean} Whether the message is acceptable.
  711. * @package
  712. */
  713. goog.net.xpc.CrossPageChannel.prototype.isMessageOriginAcceptable = function(
  714. opt_origin) {
  715. var peerHostname = this.cfg_[goog.net.xpc.CfgFields.PEER_HOSTNAME];
  716. return goog.string.isEmptyOrWhitespace(goog.string.makeSafe(opt_origin)) ||
  717. goog.string.isEmptyOrWhitespace(goog.string.makeSafe(peerHostname)) ||
  718. opt_origin == this.cfg_[goog.net.xpc.CfgFields.PEER_HOSTNAME];
  719. };
  720. /** @override */
  721. goog.net.xpc.CrossPageChannel.prototype.disposeInternal = function() {
  722. this.close();
  723. this.peerWindowObject_ = null;
  724. this.iframeElement_ = null;
  725. delete goog.net.xpc.channels[this.name];
  726. goog.dispose(this.peerLoadHandler_);
  727. delete this.peerLoadHandler_;
  728. goog.net.xpc.CrossPageChannel.base(this, 'disposeInternal');
  729. };
  730. /**
  731. * Disposes all channels.
  732. * @private
  733. */
  734. goog.net.xpc.CrossPageChannel.disposeAll_ = function() {
  735. for (var name in goog.net.xpc.channels) {
  736. goog.dispose(goog.net.xpc.channels[name]);
  737. }
  738. };