1// Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
2// License: New BSD License
3// Reference: http://dev.w3.org/html5/websockets/
4// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol
5
6(function() {
7
8  if (window.WebSocket) return;
9
10  var console = window.console;
11  if (!console || !console.log || !console.error) {
12    console = {log: function(){ }, error: function(){ }};
13  }
14
15  if (!swfobject.hasFlashPlayerVersion("10.0.0")) {
16    console.error("Flash Player >= 10.0.0 is required.");
17    return;
18  }
19  if (location.protocol == "file:") {
20    console.error(
21      "WARNING: web-socket-js doesn't work in file:///... URL " +
22      "unless you set Flash Security Settings properly. " +
23      "Open the page via Web server i.e. http://...");
24  }
25
26  /**
27   * This class represents a faux web socket.
28   * @param {string} url
29   * @param {string} protocol
30   * @param {string} proxyHost
31   * @param {int} proxyPort
32   * @param {string} headers
33   */
34  WebSocket = function(url, protocol, proxyHost, proxyPort, headers) {
35    var self = this;
36    self.__id = WebSocket.__nextId++;
37    WebSocket.__instances[self.__id] = self;
38    self.readyState = WebSocket.CONNECTING;
39    self.bufferedAmount = 0;
40    self.__events = {};
41    // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc.
42    // Otherwise, when onopen fires immediately, onopen is called before it is set.
43    setTimeout(function() {
44      WebSocket.__addTask(function() {
45        WebSocket.__flash.create(
46            self.__id, url, protocol, proxyHost || null, proxyPort || 0, headers || null);
47      });
48    }, 0);
49  };
50
51  /**
52   * Send data to the web socket.
53   * @param {string} data  The data to send to the socket.
54   * @return {boolean}  True for success, false for failure.
55   */
56  WebSocket.prototype.send = function(data) {
57    if (this.readyState == WebSocket.CONNECTING) {
58      throw "INVALID_STATE_ERR: Web Socket connection has not been established";
59    }
60    // We use encodeURIComponent() here, because FABridge doesn't work if
61    // the argument includes some characters. We don't use escape() here
62    // because of this:
63    // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions
64    // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't
65    // preserve all Unicode characters either e.g. "\uffff" in Firefox.
66    // Note by wtritch: Hopefully this will not be necessary using ExternalInterface.  Will require
67    // additional testing.
68    var result = WebSocket.__flash.send(this.__id, encodeURIComponent(data));
69    if (result < 0) { // success
70      return true;
71    } else {
72      this.bufferedAmount += result;
73      return false;
74    }
75  };
76
77  /**
78   * Close this web socket gracefully.
79   */
80  WebSocket.prototype.close = function() {
81    if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) {
82      return;
83    }
84    this.readyState = WebSocket.CLOSING;
85    WebSocket.__flash.close(this.__id);
86  };
87
88  /**
89   * Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>}
90   *
91   * @param {string} type
92   * @param {function} listener
93   * @param {boolean} useCapture
94   * @return void
95   */
96  WebSocket.prototype.addEventListener = function(type, listener, useCapture) {
97    if (!(type in this.__events)) {
98      this.__events[type] = [];
99    }
100    this.__events[type].push(listener);
101  };
102
103  /**
104   * Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>}
105   *
106   * @param {string} type
107   * @param {function} listener
108   * @param {boolean} useCapture
109   * @return void
110   */
111  WebSocket.prototype.removeEventListener = function(type, listener, useCapture) {
112    if (!(type in this.__events)) return;
113    var events = this.__events[type];
114    for (var i = events.length - 1; i >= 0; --i) {
115      if (events[i] === listener) {
116        events.splice(i, 1);
117        break;
118      }
119    }
120  };
121
122  /**
123   * Implementation of {@link <a href="http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration">DOM 2 EventTarget Interface</a>}
124   *
125   * @param {Event} event
126   * @return void
127   */
128  WebSocket.prototype.dispatchEvent = function(event) {
129    var events = this.__events[event.type] || [];
130    for (var i = 0; i < events.length; ++i) {
131      events[i](event);
132    }
133    var handler = this["on" + event.type];
134    if (handler) handler(event);
135  };
136
137  /**
138   * Handles an event from Flash.
139   * @param {Object} flashEvent
140   */
141  WebSocket.prototype.__handleEvent = function(flashEvent) {
142    if ("readyState" in flashEvent) {
143      this.readyState = flashEvent.readyState;
144    }
145
146    var jsEvent;
147    if (flashEvent.type == "open" || flashEvent.type == "error") {
148      jsEvent = this.__createSimpleEvent(flashEvent.type);
149    } else if (flashEvent.type == "close") {
150      // TODO implement jsEvent.wasClean
151      jsEvent = this.__createSimpleEvent("close");
152    } else if (flashEvent.type == "message") {
153      var data = decodeURIComponent(flashEvent.message);
154      jsEvent = this.__createMessageEvent("message", data);
155    } else {
156      throw "unknown event type: " + flashEvent.type;
157    }
158
159    this.dispatchEvent(jsEvent);
160  };
161
162  WebSocket.prototype.__createSimpleEvent = function(type) {
163    if (document.createEvent && window.Event) {
164      var event = document.createEvent("Event");
165      event.initEvent(type, false, false);
166      return event;
167    } else {
168      return {type: type, bubbles: false, cancelable: false};
169    }
170  };
171
172  WebSocket.prototype.__createMessageEvent = function(type, data) {
173    if (document.createEvent && window.MessageEvent && !window.opera) {
174      var event = document.createEvent("MessageEvent");
175      event.initMessageEvent("message", false, false, data, null, null, window, null);
176      return event;
177    } else {
178      // IE and Opera, the latter one truncates the data parameter after any 0x00 bytes.
179      return {type: type, data: data, bubbles: false, cancelable: false};
180    }
181  };
182
183  /**
184   * Define the WebSocket readyState enumeration.
185   */
186  WebSocket.CONNECTING = 0;
187  WebSocket.OPEN = 1;
188  WebSocket.CLOSING = 2;
189  WebSocket.CLOSED = 3;
190
191  WebSocket.__flash = null;
192  WebSocket.__instances = {};
193  WebSocket.__tasks = [];
194  WebSocket.__nextId = 0;
195
196  /**
197   * Load a new flash security policy file.
198   * @param {string} url
199   */
200  WebSocket.loadFlashPolicyFile = function(url){
201    WebSocket.__addTask(function() {
202      WebSocket.__flash.loadManualPolicyFile(url);
203    });
204  };
205
206  /**
207   * Loads WebSocketMain.swf and creates WebSocketMain object in Flash.
208   */
209  WebSocket.__initialize = function() {
210    if (WebSocket.__flash) return;
211
212    if (WebSocket.__swfLocation) {
213      // For backword compatibility.
214      window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation;
215    }
216    if (!window.WEB_SOCKET_SWF_LOCATION) {
217      console.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf");
218      return;
219    }
220    var container = document.createElement("div");
221    container.id = "webSocketContainer";
222    // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents
223    // Flash from loading at least in IE. So we move it out of the screen at (-100, -100).
224    // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash
225    // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is
226    // the best we can do as far as we know now.
227    container.style.position = "absolute";
228    if (WebSocket.__isFlashLite()) {
229      container.style.left = "0px";
230      container.style.top = "0px";
231    } else {
232      container.style.left = "-100px";
233      container.style.top = "-100px";
234    }
235    var holder = document.createElement("div");
236    holder.id = "webSocketFlash";
237    container.appendChild(holder);
238    document.body.appendChild(container);
239    // See this article for hasPriority:
240    // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html
241    swfobject.embedSWF(
242      WEB_SOCKET_SWF_LOCATION,
243      "webSocketFlash",
244      "1" /* width */,
245      "1" /* height */,
246      "10.0.0" /* SWF version */,
247      null,
248      null,
249      {hasPriority: true, swliveconnect : true, allowScriptAccess: "always"},
250      null,
251      function(e) {
252        if (!e.success) {
253          console.error("[WebSocket] swfobject.embedSWF failed");
254        }
255      });
256  };
257
258  /**
259   * Called by Flash to notify JS that it's fully loaded and ready
260   * for communication.
261   */
262  WebSocket.__onFlashInitialized = function() {
263    // We need to set a timeout here to avoid round-trip calls
264    // to flash during the initialization process.
265    setTimeout(function() {
266      WebSocket.__flash = document.getElementById("webSocketFlash");
267      WebSocket.__flash.setCallerUrl(location.href);
268      WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG);
269      for (var i = 0; i < WebSocket.__tasks.length; ++i) {
270        WebSocket.__tasks[i]();
271      }
272      WebSocket.__tasks = [];
273    }, 0);
274  };
275
276  /**
277   * Called by Flash to notify WebSockets events are fired.
278   */
279  WebSocket.__onFlashEvent = function() {
280    setTimeout(function() {
281      try {
282        // Gets events using receiveEvents() instead of getting it from event object
283        // of Flash event. This is to make sure to keep message order.
284        // It seems sometimes Flash events don't arrive in the same order as they are sent.
285        var events = WebSocket.__flash.receiveEvents();
286        for (var i = 0; i < events.length; ++i) {
287          WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]);
288        }
289      } catch (e) {
290        console.error(e);
291      }
292    }, 0);
293    return true;
294  };
295
296  // Called by Flash.
297  WebSocket.__log = function(message) {
298    console.log(decodeURIComponent(message));
299  };
300
301  // Called by Flash.
302  WebSocket.__error = function(message) {
303    console.error(decodeURIComponent(message));
304  };
305
306  WebSocket.__addTask = function(task) {
307    if (WebSocket.__flash) {
308      task();
309    } else {
310      WebSocket.__tasks.push(task);
311    }
312  };
313
314  /**
315   * Test if the browser is running flash lite.
316   * @return {boolean} True if flash lite is running, false otherwise.
317   */
318  WebSocket.__isFlashLite = function() {
319    if (!window.navigator || !window.navigator.mimeTypes) {
320      return false;
321    }
322    var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"];
323    if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) {
324      return false;
325    }
326    return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false;
327  };
328
329  if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) {
330    if (window.addEventListener) {
331      window.addEventListener("load", function(){
332        WebSocket.__initialize();
333      }, false);
334    } else {
335      window.attachEvent("onload", function(){
336        WebSocket.__initialize();
337      });
338    }
339  }
340
341})();
342