You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

183 lines
5.4 KiB

4 years ago
  1. 'use strict';
  2. var utils = require('../../utils/iframe')
  3. , random = require('../../utils/random')
  4. , browser = require('../../utils/browser')
  5. , urlUtils = require('../../utils/url')
  6. , inherits = require('inherits')
  7. , EventEmitter = require('events').EventEmitter
  8. ;
  9. var debug = function() {};
  10. if (process.env.NODE_ENV !== 'production') {
  11. debug = require('debug')('sockjs-client:receiver:jsonp');
  12. }
  13. function JsonpReceiver(url) {
  14. debug(url);
  15. var self = this;
  16. EventEmitter.call(this);
  17. utils.polluteGlobalNamespace();
  18. this.id = 'a' + random.string(6);
  19. var urlWithId = urlUtils.addQuery(url, 'c=' + encodeURIComponent(utils.WPrefix + '.' + this.id));
  20. global[utils.WPrefix][this.id] = this._callback.bind(this);
  21. this._createScript(urlWithId);
  22. // Fallback mostly for Konqueror - stupid timer, 35 seconds shall be plenty.
  23. this.timeoutId = setTimeout(function() {
  24. debug('timeout');
  25. self._abort(new Error('JSONP script loaded abnormally (timeout)'));
  26. }, JsonpReceiver.timeout);
  27. }
  28. inherits(JsonpReceiver, EventEmitter);
  29. JsonpReceiver.prototype.abort = function() {
  30. debug('abort');
  31. if (global[utils.WPrefix][this.id]) {
  32. var err = new Error('JSONP user aborted read');
  33. err.code = 1000;
  34. this._abort(err);
  35. }
  36. };
  37. JsonpReceiver.timeout = 35000;
  38. JsonpReceiver.scriptErrorTimeout = 1000;
  39. JsonpReceiver.prototype._callback = function(data) {
  40. debug('_callback', data);
  41. this._cleanup();
  42. if (this.aborting) {
  43. return;
  44. }
  45. if (data) {
  46. debug('message', data);
  47. this.emit('message', data);
  48. }
  49. this.emit('close', null, 'network');
  50. this.removeAllListeners();
  51. };
  52. JsonpReceiver.prototype._abort = function(err) {
  53. debug('_abort', err);
  54. this._cleanup();
  55. this.aborting = true;
  56. this.emit('close', err.code, err.message);
  57. this.removeAllListeners();
  58. };
  59. JsonpReceiver.prototype._cleanup = function() {
  60. debug('_cleanup');
  61. clearTimeout(this.timeoutId);
  62. if (this.script2) {
  63. this.script2.parentNode.removeChild(this.script2);
  64. this.script2 = null;
  65. }
  66. if (this.script) {
  67. var script = this.script;
  68. // Unfortunately, you can't really abort script loading of
  69. // the script.
  70. script.parentNode.removeChild(script);
  71. script.onreadystatechange = script.onerror =
  72. script.onload = script.onclick = null;
  73. this.script = null;
  74. }
  75. delete global[utils.WPrefix][this.id];
  76. };
  77. JsonpReceiver.prototype._scriptError = function() {
  78. debug('_scriptError');
  79. var self = this;
  80. if (this.errorTimer) {
  81. return;
  82. }
  83. this.errorTimer = setTimeout(function() {
  84. if (!self.loadedOkay) {
  85. self._abort(new Error('JSONP script loaded abnormally (onerror)'));
  86. }
  87. }, JsonpReceiver.scriptErrorTimeout);
  88. };
  89. JsonpReceiver.prototype._createScript = function(url) {
  90. debug('_createScript', url);
  91. var self = this;
  92. var script = this.script = global.document.createElement('script');
  93. var script2; // Opera synchronous load trick.
  94. script.id = 'a' + random.string(8);
  95. script.src = url;
  96. script.type = 'text/javascript';
  97. script.charset = 'UTF-8';
  98. script.onerror = this._scriptError.bind(this);
  99. script.onload = function() {
  100. debug('onload');
  101. self._abort(new Error('JSONP script loaded abnormally (onload)'));
  102. };
  103. // IE9 fires 'error' event after onreadystatechange or before, in random order.
  104. // Use loadedOkay to determine if actually errored
  105. script.onreadystatechange = function() {
  106. debug('onreadystatechange', script.readyState);
  107. if (/loaded|closed/.test(script.readyState)) {
  108. if (script && script.htmlFor && script.onclick) {
  109. self.loadedOkay = true;
  110. try {
  111. // In IE, actually execute the script.
  112. script.onclick();
  113. } catch (x) {
  114. // intentionally empty
  115. }
  116. }
  117. if (script) {
  118. self._abort(new Error('JSONP script loaded abnormally (onreadystatechange)'));
  119. }
  120. }
  121. };
  122. // IE: event/htmlFor/onclick trick.
  123. // One can't rely on proper order for onreadystatechange. In order to
  124. // make sure, set a 'htmlFor' and 'event' properties, so that
  125. // script code will be installed as 'onclick' handler for the
  126. // script object. Later, onreadystatechange, manually execute this
  127. // code. FF and Chrome doesn't work with 'event' and 'htmlFor'
  128. // set. For reference see:
  129. // http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html
  130. // Also, read on that about script ordering:
  131. // http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order
  132. if (typeof script.async === 'undefined' && global.document.attachEvent) {
  133. // According to mozilla docs, in recent browsers script.async defaults
  134. // to 'true', so we may use it to detect a good browser:
  135. // https://developer.mozilla.org/en/HTML/Element/script
  136. if (!browser.isOpera()) {
  137. // Naively assume we're in IE
  138. try {
  139. script.htmlFor = script.id;
  140. script.event = 'onclick';
  141. } catch (x) {
  142. // intentionally empty
  143. }
  144. script.async = true;
  145. } else {
  146. // Opera, second sync script hack
  147. script2 = this.script2 = global.document.createElement('script');
  148. script2.text = "try{var a = document.getElementById('" + script.id + "'); if(a)a.onerror();}catch(x){};";
  149. script.async = script2.async = false;
  150. }
  151. }
  152. if (typeof script.async !== 'undefined') {
  153. script.async = true;
  154. }
  155. var head = global.document.getElementsByTagName('head')[0];
  156. head.insertBefore(script, head.firstChild);
  157. if (script2) {
  158. head.insertBefore(script2, head.firstChild);
  159. }
  160. };
  161. module.exports = JsonpReceiver;