1/** 2The MIT License (MIT) 3 4Copyright (c) 2010-2014 Wandenberg Peixoto <[email protected]>, Rogério Carvalho Schneider <[email protected]> 5 6This file is part of Nginx Push Stream Module. 7 8Permission is hereby granted, free of charge, to any person obtaining a copy 9of this software and associated documentation files (the "Software"), to deal 10in the Software without restriction, including without limitation the rights 11to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12copies of the Software, and to permit persons to whom the Software is 13furnished to do so, subject to the following conditions: 14 15The above copyright notice and this permission notice shall be included in all 16copies or substantial portions of the Software. 17 18THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24SOFTWARE. 25 26pushstream.js 27 28Created: Nov 01, 2011 29Authors: Wandenberg Peixoto <[email protected]>, Rogério Carvalho Schneider <[email protected]> 30 */ 31(function (window, document, undefined) { 32 "use strict"; 33 34 /* prevent duplicate declaration */ 35 if (window.PushStream) { return; } 36 37 var Utils = {}; 38 39 var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; 40 var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 41 42 var valueToTwoDigits = function (value) { 43 return ((value < 10) ? '0' : '') + value; 44 }; 45 46 Utils.dateToUTCString = function (date) { 47 var time = valueToTwoDigits(date.getUTCHours()) + ':' + valueToTwoDigits(date.getUTCMinutes()) + ':' + valueToTwoDigits(date.getUTCSeconds()); 48 return days[date.getUTCDay()] + ', ' + valueToTwoDigits(date.getUTCDate()) + ' ' + months[date.getUTCMonth()] + ' ' + date.getUTCFullYear() + ' ' + time + ' GMT'; 49 }; 50 51 var extend = function () { 52 var object = arguments[0] || {}; 53 for (var i = 0; i < arguments.length; i++) { 54 var settings = arguments[i]; 55 for (var attr in settings) { 56 if (!settings.hasOwnProperty || settings.hasOwnProperty(attr)) { 57 object[attr] = settings[attr]; 58 } 59 } 60 } 61 return object; 62 }; 63 64 var validChars = /^[\],:{}\s]*$/, 65 validEscape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, 66 validTokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, 67 validBraces = /(?:^|:|,)(?:\s*\[)+/g; 68 69 var trim = function(value) { 70 return value.replace(/^\s*/, "").replace(/\s*$/, ""); 71 }; 72 73 Utils.parseJSON = function(data) { 74 if (!data || !isString(data)) { 75 return null; 76 } 77 78 // Make sure leading/trailing whitespace is removed (IE can't handle it) 79 data = trim(data); 80 81 // Attempt to parse using the native JSON parser first 82 if (window.JSON && window.JSON.parse) { 83 try { 84 return window.JSON.parse( data ); 85 } catch(e) { 86 throw "Invalid JSON: " + data; 87 } 88 } 89 90 // Make sure the incoming data is actual JSON 91 // Logic borrowed from http://json.org/json2.js 92 if (validChars.test(data.replace(validEscape, "@").replace( validTokens, "]").replace( validBraces, "")) ) { 93 return (new Function("return " + data))(); 94 } 95 96 throw "Invalid JSON: " + data; 97 }; 98 99 var getControlParams = function(pushstream) { 100 var data = {}; 101 data[pushstream.tagArgument] = ""; 102 data[pushstream.timeArgument] = ""; 103 data[pushstream.eventIdArgument] = ""; 104 if (pushstream.messagesControlByArgument) { 105 data[pushstream.tagArgument] = Number(pushstream._etag); 106 if (pushstream._lastModified) { 107 data[pushstream.timeArgument] = pushstream._lastModified; 108 } else if (pushstream._lastEventId) { 109 data[pushstream.eventIdArgument] = pushstream._lastEventId; 110 } 111 } 112 return data; 113 }; 114 115 var getTime = function() { 116 return (new Date()).getTime(); 117 }; 118 119 var currentTimestampParam = function() { 120 return { "_" : getTime() }; 121 }; 122 123 var objectToUrlParams = function(settings) { 124 var params = settings;
125 if (typeof(settings) === 'object') { 126 params = ''; 127 for (var attr in settings) { 128 if (!settings.hasOwnProperty || settings.hasOwnProperty(attr)) { 129 params += '&' + attr + '=' + escapeText(settings[attr]); 130 } 131 } 132 params = params.substring(1); 133 } 134 135 return params || ''; 136 }; 137 138 var addParamsToUrl = function(url, params) { 139 return url + ((url.indexOf('?') < 0) ? '?' : '&') + objectToUrlParams(params); 140 }; 141 142 var isArray = Array.isArray || function(obj) { 143 return Object.prototype.toString.call(obj) === '[object Array]'; 144 }; 145 146 var isString = function(obj) { 147 return Object.prototype.toString.call(obj) === '[object String]'; 148 }; 149 150 var isDate = function(obj) { 151 return Object.prototype.toString.call(obj) === '[object Date]'; 152 }; 153 154 var Log4js = { 155 logger: null, 156 debug : function() { if (PushStream.LOG_LEVEL === 'debug') { Log4js._log.apply(Log4js._log, arguments); }}, 157 info : function() { if ((PushStream.LOG_LEVEL === 'info') || (PushStream.LOG_LEVEL === 'debug')) { Log4js._log.apply(Log4js._log, arguments); }}, 158 error : function() { Log4js._log.apply(Log4js._log, arguments); }, 159 _initLogger : function() { 160 var console = window.console; 161 if (console && console.log) { 162 if (console.log.apply) { 163 Log4js.logger = console.log; 164 } else if ((typeof console.log === "object") && Function.prototype.bind) { 165 Log4js.logger = Function.prototype.bind.call(console.log, console); 166 } else if ((typeof console.log === "object") && Function.prototype.call) { 167 Log4js.logger = function() { 168 Function.prototype.call.call(console.log, console, Array.prototype.slice.call(arguments)); 169 }; 170 } 171 } 172 }, 173 _log : function() { 174 if (!Log4js.logger) { 175 Log4js._initLogger(); 176 } 177 178 if (Log4js.logger) { 179 try { 180 Log4js.logger.apply(window.console, arguments); 181 } catch(e) { 182 Log4js._initLogger(); 183 if (Log4js.logger) { 184 Log4js.logger.apply(window.console, arguments); 185 } 186 } 187 } 188 189 var logElement = document.getElementById(PushStream.LOG_OUTPUT_ELEMENT_ID); 190 if (logElement) { 191 var str = ''; 192 for (var i = 0; i < arguments.length; i++) { 193 str += arguments[i] + " "; 194 } 195 logElement.innerHTML += str + '\n'; 196 197 var lines = logElement.innerHTML.split('\n'); 198 if (lines.length > 100) { 199 logElement.innerHTML = lines.slice(-100).join('\n'); 200 } 201 } 202 } 203 }; 204 205 var Ajax = { 206 _getXHRObject : function(crossDomain) { 207 var xhr = false; 208 if (crossDomain) { 209 try { xhr = new window.XDomainRequest(); } catch (e) { } 210 if (xhr) { 211 return xhr; 212 } 213 } 214 215 try { xhr = new window.XMLHttpRequest(); } 216 catch (e1) { 217 try { xhr = new window.ActiveXObject("Msxml2.XMLHTTP"); } 218 catch (e2) { 219 try { xhr = new window.ActiveXObject("Microsoft.XMLHTTP"); } 220 catch (e3) { 221 xhr = false; 222 } 223 } 224 } 225 return xhr; 226 }, 227 228 _send : function(settings, post) { 229 settings = settings || {}; 230 settings.timeout = settings.timeout || 30000; 231 var xhr = Ajax._getXHRObject(settings.crossDomain); 232 if (!xhr||!settings.url) { return; } 233 234 Ajax.clear(settings); 235 236 settings.xhr = xhr; 237 238 if (window.XDomainRequest && (xhr instanceof window.XDomainRequest)) { 239 xhr.onload = function () { 240 if (settings.afterReceive) { settings.afterReceive(xhr); } 241 if (settings.success) { settings.success(xhr.responseText); } 242 }; 243 244 xhr.onerror = xhr.ontimeout = function () { 245 if (settings.afterReceive) { settings.afterReceive(xhr); } 246 if (settings.error) { settings.error(xhr.status); } 247 }; 248 } else { 249 xhr.onreadystatechange = function () { 250 if (xhr.readyState === 4) { 251 Ajax.clear(settings); 252 if (settings.afterReceive) { settings.afterReceive(xhr); } 253 if(xhr.status === 200) { 254 if (settings.success) { settings.success(xhr.responseText); } 255 } else { 256 if (settings.error) { settings.error(xhr.status); } 257 } 258 } 259 }; 260 } 261 262 if (settings.beforeOpen) { settings.beforeOpen(xhr); } 263 264 var params = {}; 265 var body = null; 266 var method = "GET"; 267 if (post) { 268 body = objectToUrlParams(settings.data); 269 method = "POST"; 270 } else { 271 params = settings.data || {}; 272 } 273 274 xhr.open(method, addParamsToUrl(settings.url, extend({}, params, currentTimestampParam())), true); 275 276 if (settings.beforeSend) { settings.beforeSend(xhr); } 277 278 var onerror = function() { 279 Ajax.clear(settings); 280 try { xhr.abort(); } catch (e) { /* ignore error on closing */ } 281 settings.error(304); 282 }; 283 284 if (post) { 285 if (xhr.setRequestHeader) { 286 xhr.setRequestHeader("Accept", "application/json"); 287 xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 288 } 289 } else { 290 settings.timeoutId = window.setTimeout(onerror, settings.timeout + 2000); 291 } 292 293 xhr.send(body); 294 return xhr; 295 }, 296 297 _clear_xhr : function(xhr) { 298 if (xhr) { 299 xhr.onreadystatechange = null; 300 } 301 }, 302 303 _clear_script : function(script) { 304 // Handling memory leak in IE, removing and dereference the script 305 if (script) { 306 script.onerror = script.onload = script.onreadystatechange = null; 307 if (script.parentNode) { script.parentNode.removeChild(script); } 308 } 309 }, 310 311 _clear_timeout : function(settings) { 312 settings.timeoutId = clearTimer(settings.timeoutId); 313 }, 314 315 _clear_jsonp : function(settings) { 316 var callbackFunctionName = settings.data.callback; 317 if (callbackFunctionName) { 318 window[callbackFunctionName] = function() { window[callbackFunctionName] = null; }; 319 } 320 }, 321 322 clear : function(settings) { 323 Ajax._clear_timeout(settings); 324 Ajax._clear_jsonp(settings); 325 Ajax._clear_script(document.getElementById(settings.scriptId)); 326 Ajax._clear_xhr(settings.xhr); 327 }, 328 329 jsonp : function(settings) { 330 settings.timeout = settings.timeout || 30000; 331 Ajax.clear(settings); 332 333 var head = document.head || document.getElementsByTagName("head")[0]; 334 var script = document.createElement("script"); 335 var startTime = getTime(); 336 337 var onerror = function() { 338 Ajax.clear(settings); 339 var endTime = getTime(); 340 settings.error(((endTime - startTime) > settings.timeout/2) ? 304 : 403); 341 }; 342 343 var onload = function() { 344 Ajax.clear(settings); 345 settings.load(); 346 }; 347 348 var onsuccess = function() { 349 settings.afterSuccess = true; 350 settings.success.apply(null, arguments); 351 }; 352 353 script.onerror = onerror; 354 script.onload = script.onreadystatechange = function() { 355 if (!script.readyState || /loaded|complete/.test(script.readyState)) { 356 if (settings.afterSuccess) { 357 onload(); 358 } else {
359 onerror(); 360 } 361 } 362 }; 363 364 if (settings.beforeOpen) { settings.beforeOpen({}); } 365 if (settings.beforeSend) { settings.beforeSend({}); } 366 367 settings.timeoutId = window.setTimeout(onerror, settings.timeout + 2000); 368 settings.scriptId = settings.scriptId || getTime(); 369 settings.afterSuccess = null; 370 371 settings.data.callback = settings.scriptId + "_onmessage_" + getTime(); 372 window[settings.data.callback] = onsuccess; 373 374 script.setAttribute("src", addParamsToUrl(settings.url, extend({}, settings.data, currentTimestampParam()))); 375 script.setAttribute("async", "async"); 376 script.setAttribute("id", settings.scriptId); 377 378 // Use insertBefore instead of appendChild to circumvent an IE6 bug. 379 head.insertBefore(script, head.firstChild); 380 return settings; 381 }, 382 383 load : function(settings) { 384 return Ajax._send(settings, false); 385 }, 386 387 post : function(settings) { 388 return Ajax._send(settings, true); 389 } 390 }; 391 392 var escapeText = function(text) { 393 return (text) ? window.encodeURIComponent(text) : ''; 394 }; 395 396 var unescapeText = function(text) { 397 return (text) ? window.decodeURIComponent(text) : ''; 398 }; 399 400 Utils.parseMessage = function(messageText, keys) { 401 var msg = messageText; 402 if (isString(messageText)) { 403 msg = Utils.parseJSON(messageText); 404 } 405 406 var message = { 407 id : msg[keys.jsonIdKey], 408 channel: msg[keys.jsonChannelKey], 409 text : isString(msg[keys.jsonTextKey]) ? unescapeText(msg[keys.jsonTextKey]) : msg[keys.jsonTextKey], 410 tag : msg[keys.jsonTagKey], 411 time : msg[keys.jsonTimeKey], 412 eventid: msg[keys.jsonEventIdKey] || "" 413 }; 414 415 return message; 416 }; 417 418 var getBacktrack = function(options) { 419 return (options.backtrack) ? ".b" + Number(options.backtrack) : ""; 420 }; 421 422 var getChannelsPath = function(channels, withBacktrack) { 423 var path = ''; 424 for (var channelName in channels) { 425 if (!channels.hasOwnProperty || channels.hasOwnProperty(channelName)) { 426 path += "/" + channelName + (withBacktrack ? getBacktrack(channels[channelName]) : ""); 427 } 428 } 429 return path; 430 }; 431 432 var getSubscriberUrl = function(pushstream, prefix, extraParams, withBacktrack) { 433 var websocket = pushstream.wrapper.type === WebSocketWrapper.TYPE; 434 var useSSL = pushstream.useSSL; 435 var port = normalizePort(useSSL, pushstream.port); 436 var url = (websocket) ? ((useSSL) ? "wss://" : "ws://") : ((useSSL) ? "https://" : "http://"); 437 url += pushstream.host; 438 url += (port ? (":" + port) : ""); 439 url += prefix; 440 441 var channels = getChannelsPath(pushstream.channels, withBacktrack); 442 if (pushstream.channelsByArgument) { 443 var channelParam = {}; 444 channelParam[pushstream.channelsArgument] = channels.substring(1); 445 extraParams = extend({}, extraParams, channelParam); 446 } else { 447 url += channels; 448 } 449 return addParamsToUrl(url, extraParams); 450 }; 451 452 var getPublisherUrl = function(pushstream) { 453 var port = normalizePort(pushstream.useSSL, pushstream.port); 454 var url = (pushstream.useSSL) ? "https://" : "http://"; 455 url += pushstream.host; 456 url += (port ? (":" + port) : ""); 457 url += pushstream.urlPrefixPublisher; 458 url += "?id=" + getChannelsPath(pushstream.channels, false); 459 460 return url; 461 }; 462 463 Utils.extract_xss_domain = function(domain) { 464 // if domain is an ip address return it, else return ate least the last two parts of it 465 if (domain.match(/^(\d{1,3}\.){3}\d{1,3}$/)) { 466 return domain; 467 } 468 469 var domainParts = domain.split('.'); 470 // if the domain ends with 3 chars or 2 chars preceded by more than 4 chars, 471 // we can keep only 2 parts, else we have to keep at least 3 (
471or all domain name) 472 var keepNumber = Math.max(domainParts.length - 1, (domain.match(/(\w{4,}\.\w{2}|\.\w{3,})$/) ? 2 : 3)); 473 474 return domainParts.slice(-1 * keepNumber).join('.'); 475 }; 476 477 var normalizePort = function (useSSL, port) { 478 port = Number(port || (useSSL ? 443 : 80)); 479 return ((!useSSL && port === 80) || (useSSL && port === 443)) ? "" : port; 480 }; 481 482 Utils.isCrossDomainUrl = function(url) { 483 if (!url) { 484 return false; 485 } 486 487 var parser = document.createElement('a'); 488 parser.href = url; 489 490 var srcPort = normalizePort(window.location.protocol === "https:", window.location.port); 491 var dstPort = normalizePort(parser.protocol === "https:", parser.port); 492 493 return (window.location.protocol !== parser.protocol) || 494 (window.location.hostname !== parser.hostname) || 495 (srcPort !== dstPort); 496 }; 497 498 var linker = function(method, instance) { 499 return function() { 500 return method.apply(instance, arguments); 501 }; 502 }; 503 504 var clearTimer = function(timer) { 505 if (timer) { 506 window.clearTimeout(timer); 507 } 508 return null; 509 }; 510 511 /* common callbacks */ 512 var onmessageCallback = function(event) { 513 Log4js.info("[" + this.type + "] message received", arguments); 514 var message = Utils.parseMessage(event.data, this.pushstream); 515 if (message.tag) { this.pushstream._etag = message.tag; } 516 if (message.time) { this.pushstream._lastModified = message.time; } 517 if (message.eventid) { this.pushstream._lastEventId = message.eventid; } 518 this.pushstream._onmessage(message.text, message.id, message.channel, message.eventid, true, message.time); 519 }; 520 521 var onopenCallback = function() { 522 this.pushstream._onopen(); 523 Log4js.info("[" + this.type + "] connection opened"); 524 }; 525 526 var onerrorCallback = function(event) { 527 Log4js.info("[" + this.type + "] error (disconnected by server):", event); 528 if ((this.pushstream.readyState === PushStream.OPEN) && 529 (this.type === EventSourceWrapper.TYPE) && 530 (event.type === 'error') && 531 (this.connection.readyState === window.EventSource.CONNECTING)) { 532 // EventSource already has a reconnection function using the last-event-id header 533 return; 534 } 535 this._closeCurrentConnection(); 536 this.pushstream._onerror({type: ((event && ((event.type === "load") || ((event.type === "close") && (event.code === 1006)))) || (this.pushstream.readyState === PushStream.CONNECTING)) ? "load" : "timeout"}); 537 }; 538 539 /* wrappers */ 540 541 var WebSocketWrapper = function(pushstream) { 542 if (!window.WebSocket && !window.MozWebSocket) { throw "WebSocket not supported"; } 543 this.type = WebSocketWrapper.TYPE; 544 this.pushstream = pushstream; 545 this.connection = null; 546 }; 547 548 WebSocketWrapper.TYPE = "WebSocket"; 549 550 WebSocketWrapper.prototype = { 551 connect: function() { 552 this._closeCurrentConnection(); 553 var params = extend({}, this.pushstream.extraParams(), currentTimestampParam(), getControlParams(this.pushstream)); 554 var url = getSubscriberUrl(this.pushstream, this.pushstream.urlPrefixWebsocket, params, !this.pushstream._useControlArguments()); 555 this.connection = (window.WebSocket) ? new window.WebSocket(url) : new window.MozWebSocket(url); 556 this.connection.onerror = linker(onerrorCallback, this); 557 this.connection.onclose = linker(onerrorCallback, this); 558 this.connection.onopen = linker(onopenCallback, this); 559 this.connection.onmessage = linker(onmessageCallback, this); 560 Log4js.info("[WebSocket] connecting to:", url); 561 }, 562 563 disconnect: function() { 564 if (this.connection) { 565 Log4js.debug("[WebSocket] closing connection to:", this.connection.url); 566 this.connection.onclose = null; 567 this._closeCurrentConnection(); 568 this.pushstream._onclose(); 569 } 570 }, 571 572 _closeCurrentConnection: function() { 573 if (this.connection) { 574 try { this.connection.close(); } catch (e) { /* ignore error on closing */ } 575 this.connection = null; 576 } 577 }, 578 579 sendMessage: function(message) { 580 if (this.connection) { this.connection.send(message); } 581 } 582 }; 583 584 var EventSourceWrapper = function(pushstream) { 585 if (!window.EventSource) { throw "EventSource not supported"; } 586 this.type = EventSourceWrapper.TYPE; 587 this.pushstream = pushstream; 588 this.connection = null; 589 }; 590 591 EventSourceWrapper.TYPE = "EventSource"; 592 593 EventSourceWrapper.prototype = { 594 connect: function() { 595 this._closeCurrentConnection(); 596 var params = extend({}, this.pushstream.extraParams(), currentTimestampParam(), getControlParams(this.pushstream)); 597 var url = getSubscriberUrl(this.pushstream, this.pushstream.urlPrefixEventsource, params, !this.pushstream._useControlArguments()); 598 this.connection = new window.EventSource(url); 599 this.connection.onerror = linker(onerrorCallback, this); 600 this.connection.onopen = linker(onopenCallback, this); 601 this.connection.onmessage = linker(onmessageCallback, this); 602 Log4js.info("[EventSource] connecting to:", url); 603 }, 604 605 disconnect: function() { 606 if (this.connection) { 607 Log4js.debug("[EventSource] closing connection to:", this.connection.url); 608 this.connection.onclose = null; 609 this._closeCurrentConnection(); 610 this.pushstream._onclose(); 611 } 612 }, 613 614 _closeCurrentConnection: function() { 615 if (this.connection) { 616 try { this.connection.close(); } catch (e) { /* ignore error on closing */ } 617 this.connection = null; 618 } 619 } 620 }; 621 622 var StreamWrapper = function(pushstream) { 623 this.type = StreamWrapper.TYPE; 624 this.pushstream = pushstream; 625 this.connection = null; 626 this.url = null; 627 this.frameloadtimer = null; 628 this.pingtimer = null; 629 this.iframeId = "PushStreamManager_" + pushstream.id; 630 }; 631 632 StreamWrapper.TYPE = "Stream"; 633 634 StreamWrapper.prototype = { 635 connect: function() { 636 this._closeCurrentConnection(); 637 var domain = Utils.extract_xss_domain(this.pushstream.host);
638 try { 639 document.domain = domain; 640 } catch(e) { 641 Log4js.error("[Stream] (warning) problem setting document.domain = " + domain + " (OBS: IE8 does not support set IP numbers as domain)"); 642 } 643 var params = extend({}, this.pushstream.extraParams(), currentTimestampParam(), {"streamid": this.pushstream.id}, getControlParams(this.pushstream)); 644 this.url = getSubscriberUrl(this.pushstream, this.pushstream.urlPrefixStream, params, !this.pushstream._useControlArguments()); 645 Log4js.debug("[Stream] connecting to:", this.url); 646 this.loadFrame(this.url); 647 }, 648 649 disconnect: function() { 650 if (this.connection) { 651 Log4js.debug("[Stream] closing connection to:", this.url); 652 this._closeCurrentConnection(); 653 this.pushstream._onclose(); 654 } 655 }, 656 657 _clear_iframe: function() { 658 var oldIframe = document.getElementById(this.iframeId); 659 if (oldIframe) { 660 oldIframe.onload = null; 661 oldIframe.src = "about:blank"; 662 if (oldIframe.parentNode) { oldIframe.parentNode.removeChild(oldIframe); } 663 } 664 }, 665 666 _closeCurrentConnection: function() { 667 this._clear_iframe(); 668 if (this.connection) { 669 this.pingtimer = clearTimer(this.pingtimer); 670 this.frameloadtimer = clearTimer(this.frameloadtimer); 671 this.connection = null; 672 this.transferDoc = null; 673 if (typeof window.CollectGarbage === 'function') { window.CollectGarbage(); } 674 } 675 }, 676 677 loadFrame: function(url) { 678 this._clear_iframe(); 679 680 var ifr = null; 681 if ("ActiveXObject" in window) { 682 var transferDoc = new window.ActiveXObject("htmlfile"); 683 transferDoc.open(); 684 transferDoc.write("\x3C" + "html" + "\x3E\x3C" + "script" + "\x3E" + "document.domain='" + document.domain + "';\x3C" + "/script" + "\x3E"); 685 transferDoc.write("\x3C" + "body" + "\x3E\x3C" + "iframe id='" + this.iframeId + "' src='" + url + "'\x3E\x3C" + "/iframe" + "\x3E\x3C" + "/body" + "\x3E\x3C" + "/html" + "\x3E"); 686 transferDoc.parentWindow.PushStream = PushStream; 687 transferDoc.close(); 688 ifr = transferDoc.getElementById(this.iframeId); 689 this.transferDoc = transferDoc; 690 } else { 691 ifr = document.createElement("IFRAME"); 692 ifr.style.width = "1px"; 693 ifr.style.height = "1px"; 694 ifr.style.border = "none"; 695 ifr.style.position = "absolute"; 696 ifr.style.top = "-10px"; 697 ifr.style.marginTop = "-10px"; 698 ifr.style.zIndex = "-20"; 699 ifr.PushStream = PushStream; 700 document.body.appendChild(ifr); 701 ifr.setAttribute("src", url); 702 ifr.setAttribute("id", this.iframeId); 703 } 704 705 ifr.onload = linker(onerrorCallback, this); 706 this.connection = ifr; 707 this.frameloadtimer = window.setTimeout(linker(onerrorCallback, this), this.pushstream.timeout); 708 }, 709 710 register: function(iframeWindow) { 711 this.frameloadtimer = clearTimer(this.frameloadtimer); 712 iframeWindow.p = linker(this.process, this); 713 this.connection.onload = linker(this._onframeloaded, this); 714 this.pushstream._onopen(); 715 this.setPingTimer(); 716 Log4js.info("[Stream] frame registered"); 717 }, 718 719 process: function(id, channel, text, eventid, time, tag) { 720 this.pingtimer = clearTimer(this.pingtimer); 721 Log4js.info("[Stream] message received", arguments); 722 if (id !== -1) { 723 if (tag) { this.pushstream._etag = tag; } 724 if (time) { this.pushstream._lastModified = time; } 725 if (eventid) { this.pushstream._lastEventId = eventid; } 726 } 727 this.pushstream._onmessage(unescapeText(text), id, channel, eventid || "", true, time); 728 this.setPingTimer(); 729 }, 730 731 _onframeloaded: function() { 732 Log4js.info("[Stream] frame loaded (disconnected by server)"); 733 this.pushstream._onerror({type: "timeout"}); 734 this.connection.onload = null; 735 this.disconnect(); 736 }, 737 738 setPingTimer: function() { 739 if (this.pingtimer) { clearTimer(this.pingtimer); } 740 this.pingtimer = window.setTimeout(linker(onerrorCallback, this), this.pushstream.pingtimeout); 741 } 742 }; 743 744 var LongPollingWrapper = function(pushstream) { 745 this.type = LongPollingWrapper.TYPE; 746 this.pushstream = pushstream; 747 this.connection = null; 748 this.opentimer = null; 749 this.messagesQueue = []; 750 this._linkedInternalListen = linker(this._internalListen, this); 751 this.xhrSettings = { 752 timeout: this.pushstream.timeout, 753 data: {}, 754 url: null, 755 success: linker(this.onmessage, this), 756 error: linker(this.onerror, this), 757 load: linker(this.onload, this), 758 beforeSend: linker(this.beforeSend, this), 759 afterReceive: linker(this.afterReceive, this) 760 }; 761 }; 762 763 LongPollingWrapper.TYPE = "LongPolling"; 764 765 LongPollingWrapper.prototype = { 766 connect: function() { 767 this.messagesQueue = []; 768 this._closeCurrentConnection(); 769 this.urlWithBacktrack = getSubscriberUrl(this.pushstream, this.pushstream.urlPrefixLongpolling, {}, true); 770 this.urlWithoutBacktrack = getSubscriberUrl(this.pushstream, this.pushstream.urlPrefixLongpolling, {}, false); 771 this.xhrSettings.url = this.urlWithBacktrack; 772 this.useJSONP = this.pushstream._crossDomain || this.pushstream.useJSONP; 773 this.xhrSettings.scriptId = "PushStreamManager_" + this.pushstream.id; 774 if (this.useJSONP) { 775 this.pushstream.messagesControlByArgument = true; 776 } 777 this._listen(); 778 this.opentimer = window.setTimeout(linker(onopenCallback, this), 150); 779 Log4js.info("[LongPolling] connecting to:", this.xhrSettings.url); 780 }, 781 782 _listen: function() { 783 if (this._internalListenTimeout) { clearTimer(this._internalListenTimeout); } 784 this._internalListenTimeout = window.setTimeout(this._linkedInternalListen, 100); 785 }, 786 787 _internalListen: function() { 788 if (this.pushstream._keepConnected) { 789 this.xhrSettings.url = this.pushstream._useControlArguments() ? this.urlWithoutBacktrack : this.urlWithBacktrack; 790 this.xhrSettings.data = extend({}, this.pushstream.extraParams(), this.xhrSettings.data, getControlParams(this.pushstream)); 791 if (this.useJSONP) { 792 this.connection = Ajax.jsonp(this.xhrSettings); 793 } else if (!this.connection) { 794 this.connection = Ajax.load(this.xhrSettings); 795 } 796 } 797 }, 798 799 disconnect: function() { 800 if (this.connection) { 801 Log4js.debug("[LongPolling] closing connection to:", this.xhrSettings.url); 802 this._closeCurrentConnection(); 803 this.pushstream._onclose(); 804 } 805 }, 806 807 _closeCurrentConnection: function() { 808 this.opentimer = clearTimer(this.opentimer); 809 if (this.connection) { 810 try { this.connection.abort(); } catch (e) { 811 try { Ajax.clear(this.connection); } catch (e1) { /* ignore error on closing */ } 812 } 813 this.connection = null; 814 this.xhrSettings.url = null; 815 } 816 }, 817 818 beforeSend: function(xhr) { 819 if (!this.pushstream.messagesControlByArgument) { 820 xhr.setRequestHeader("If-None-Match", this.pushstream._etag); 821 xhr.setRequestHeader("If-Modified-Since", this.pushstream._lastModified); 822 } 823 }, 824 825 afterReceive: function(xhr) { 826 if (!this.pushstream.messagesControlByArgument) { 827 this.pushstream._etag = xhr.getResponseHeader('Etag'); 828 this.pushstream._lastModified = xhr.getResponseHeader('Last-Modified'); 829 } 830 this.connection = null; 831 }, 832 833 onerror: function(status) { 834 this._closeCurrentConnection(); 835 if (this.pushstream._keepConnected) { /* abort(), called by disconnect(), call this callback, but should be ignored */ 836 if (status === 304) { 837 this._listen(); 838 } else { 839 Log4js.info("[LongPolling] error (disconnected by server):", status);
840 this.pushstream._onerror({type: ((status === 403) || (this.pushstream.readyState === PushStream.CONNECTING)) ? "load" : "timeout"}); 841 } 842 } 843 }, 844 845 onload: function() { 846 this._listen(); 847 }, 848 849 onmessage: function(responseText) { 850 if (this._internalListenTimeout) { clearTimer(this._internalListenTimeout); } 851 Log4js.info("[LongPolling] message received", responseText); 852 var lastMessage = null; 853 var messages = isArray(responseText) ? responseText : responseText.replace(/\}\{/g, "}\r\n{").split("\r\n"); 854 for (var i = 0; i < messages.length; i++) { 855 if (messages[i]) { 856 lastMessage = Utils.parseMessage(messages[i], this.pushstream); 857 this.messagesQueue.push(lastMessage); 858 if (this.pushstream.messagesControlByArgument && lastMessage.time) { 859 this.pushstream._etag = lastMessage.tag; 860 this.pushstream._lastModified = lastMessage.time; 861 } 862 } 863 } 864 865 this._listen(); 866 867 while (this.messagesQueue.length > 0) { 868 var message = this.messagesQueue.shift(); 869 this.pushstream._onmessage(message.text, message.id, message.channel, message.eventid, (this.messagesQueue.length === 0), message.time); 870 } 871 } 872 }; 873 874 /* mains class */ 875 876 var PushStreamManager = []; 877 878 var PushStream = function(settings) { 879 settings = settings || {}; 880 881 this.id = PushStreamManager.push(this) - 1; 882 883 this.useSSL = settings.useSSL || false; 884 this.host = settings.host || window.location.hostname; 885 this.port = Number(settings.port || (this.useSSL ? 443 : 80)); 886 887 this.timeout = settings.timeout || 30000; 888 this.pingtimeout = settings.pingtimeout || 30000; 889 this.reconnectOnTimeoutInterval = settings.reconnectOnTimeoutInterval || 3000; 890 this.reconnectOnChannelUnavailableInterval = settings.reconnectOnChannelUnavailableInterval || 60000; 891 this.autoReconnect = (settings.autoReconnect !== false); 892 893 this.lastEventId = settings.lastEventId || null; 894 this.messagesPublishedAfter = settings.messagesPublishedAfter; 895 this.messagesControlByArgument = settings.messagesControlByArgument || false; 896 this.tagArgument = settings.tagArgument || 'tag'; 897 this.timeArgument = settings.timeArgument || 'time'; 898 this.eventIdArgument = settings.eventIdArgument || 'eventid'; 899 this.useJSONP = settings.useJSONP || false; 900 901 this._reconnecttimer = null; 902 this._etag = 0; 903 this._lastModified = null; 904 this._lastEventId = null; 905 906 this.urlPrefixPublisher = settings.urlPrefixPublisher || '/pub'; 907 this.urlPrefixStream = settings.urlPrefixStream || '/sub'; 908 this.urlPrefixEventsource = settings.urlPrefixEventsource || '/ev'; 909 this.urlPrefixLongpolling = settings.urlPrefixLongpolling || '/lp'; 910 this.urlPrefixWebsocket = settings.urlPrefixWebsocket || '/ws'; 911 912 this.jsonIdKey = settings.jsonIdKey || 'id'; 913 this.jsonChannelKey = settings.jsonChannelKey || 'channel'; 914 this.jsonTextKey = settings.jsonTextKey || 'text'; 915 this.jsonTagKey = settings.jsonTagKey || 'tag'; 916 this.jsonTimeKey = settings.jsonTimeKey || 'time'; 917 this.jsonEventIdKey = settings.jsonEventIdKey || 'eventid'; 918 919 this.modes = (settings.modes || 'eventsource|websocket|stream|longpolling').split('|'); 920 this.wrappers = []; 921 this.wrapper = null; 922 923 this.onchanneldeleted = settings.onchanneldeleted || null; 924 this.onmessage = settings.onmessage || null; 925 this.onerror = settings.onerror || null; 926 this.onstatuschange = settings.onstatuschange || null; 927 this.extraParams = settings.extraParams || function() { return {}; }; 928 929 this.channels = {}; 930 this.channelsCount = 0; 931 this.channelsByArgument = settings.channelsByArgument || false; 932 this.channelsArgument = settings.channelsArgument || 'channels'; 933 934 this._crossDomain = Utils.isCrossDomainUrl(getPublisherUrl(this)); 935 936 for (var i = 0; i < this.modes.length; i++) { 937 try { 938 var wrapper = null; 939 switch (this.modes[i]) { 940 case "websocket" : wrapper = new WebSocketWrapper(this); break; 941 case "eventsource": wrapper = new EventSourceWrapper(this); break; 942 case "longpolling": wrapper = new LongPollingWrapper(this); break; 943 case "stream" : wrapper = new StreamWrapper(this); break; 944 } 945 this.wrappers[this.wrappers.length] = wrapper; 946 } catch(e) { Log4js.info(e); } 947 } 948 949 this.readyState = 0; 950 }; 951 952 /* constants */ 953 PushStream.LOG_LEVEL = 'error'; /* debug, info, error */
954 PushStream.LOG_OUTPUT_ELEMENT_ID = 'Log4jsLogOutput'; 955 956 /* status codes */ 957 PushStream.CLOSED = 0; 958 PushStream.CONNECTING = 1; 959 PushStream.OPEN = 2; 960 961 /* main code */ 962 PushStream.prototype = { 963 addChannel: function(channel, options) { 964 if (escapeText(channel) !== channel) { 965 throw "Invalid channel name! Channel has to be a set of [a-zA-Z0-9]"; 966 } 967 Log4js.debug("entering addChannel"); 968 if (typeof(this.channels[channel]) !== "undefined") { throw "Cannot add channel " + channel + ": already subscribed"; } 969 options = options || {}; 970 Log4js.info("adding channel", channel, options); 971 this.channels[channel] = options; 972 this.channelsCount++; 973 if (this.readyState !== PushStream.CLOSED) { this.connect(); } 974 Log4js.debug("leaving addChannel"); 975 }, 976 977 removeChannel: function(channel) { 978 if (this.channels[channel]) { 979 Log4js.info("removing channel", channel); 980 delete this.channels[channel]; 981 this.channelsCount--; 982 } 983 }, 984 985 removeAllChannels: function() { 986 Log4js.info("removing all channels"); 987 this.channels = {}; 988 this.channelsCount = 0; 989 }, 990 991 _setState: function(state) { 992 if (this.readyState !== state) { 993 Log4js.info("status changed", state); 994 this.readyState = state; 995 if (this.onstatuschange) { 996 this.onstatuschange(this.readyState); 997 } 998 } 999 }, 1000 1001 connect: function() { 1002 Log4js.debug("entering connect"); 1003 if (!this.host) { throw "PushStream host not specified"; } 1004 if (isNaN(this.port)) { throw "PushStream port not specified"; } 1005 if (!this.channelsCount) { throw "No channels specified"; } 1006 if (this.wrappers.length === 0) { throw "No available support for this browser"; } 1007 1008 this._keepConnected = true; 1009 this._lastUsedMode = 0; 1010 this._connect(); 1011 1012 Log4js.debug("leaving connect"); 1013 }, 1014 1015 disconnect: function() { 1016 Log4js.debug("entering disconnect"); 1017 this._keepConnected = false; 1018 this._disconnect(); 1019 this._setState(PushStream.CLOSED); 1020 Log4js.info("disconnected"); 1021 Log4js.debug("leaving disconnect"); 1022 }, 1023 1024 _useControlArguments :function() { 1025 return this.messagesControlByArgument && ((this._lastModified !== null) || (this._lastEventId !== null)); 1026 }, 1027 1028 _connect: function() { 1029 if (this._lastEventId === null) { 1030 this._lastEventId = this.lastEventId; 1031 } 1032 if (this._lastModified === null) { 1033 var date = this.messagesPublishedAfter; 1034 if (!isDate(date)) { 1035 var messagesPublishedAfter = Number(this.messagesPublishedAfter); 1036 if (messagesPublishedAfter > 0) { 1037 date = new Date(); 1038 date.setTime(date.getTime() - (messagesPublishedAfter * 1000)); 1039 } else if (messagesPublishedAfter < 0) { 1040 date = new Date(0); 1041 } 1042 } 1043 1044 if (isDate(date)) { 1045 this._lastModified = Utils.dateToUTCString(date); 1046 } 1047 } 1048 1049 this._disconnect(); 1050 this._setState(PushStream.CONNECTING); 1051 this.wrapper = this.wrappers[this._lastUsedMode++ % this.wrappers.length]; 1052 1053 try { 1054 this.wrapper.connect(); 1055 } catch (e) { 1056 //each wrapper has a cleanup routine at disconnect method 1057 if (this.wrapper) { 1058 this.wrapper.disconnect(); 1059 } 1060 } 1061 }, 1062 1063 _disconnect: function() { 1064 this._reconnecttimer = clearTimer(this._reconnecttimer); 1065 if (this.wrapper) { 1066 this.wrapper.disconnect(); 1067 } 1068 }, 1069 1070 _onopen: function() { 1071 this._reconnecttimer = clearTimer(this._reconnecttimer); 1072 this._setState(PushStream.OPEN); 1073 if (this._lastUsedMode > 0) { 1074 this._lastUsedMode--; //use same mode on next connection 1075 } 1076 }, 1077 1078 _onclose: function() { 1079 this._reconnecttimer = clearTimer(this._reconnecttimer); 1080 this._setState(PushStream.CLOSED); 1081 this._reconnect(this.reconnectOnTimeoutInterval); 1082 }, 1083 1084 _onmessage: function(text, id, channel, eventid, isLastMessageFromBatch, time) { 1085 Log4js.debug("message", text, id, channel, eventid, isLastMessageFromBatch, time); 1086 if (id === -2) { 1087 if (this.onchanneldeleted) { this.onchanneldeleted(channel); } 1088 } else if (id > 0) { 1089 if (this.onmessage) { this.onmessage(text, id, channel, eventid, isLastMessageFromBatch, time); } 1090 } 1091 }, 1092 1093 _onerror: function(error) { 1094 this._setState(PushStream.CLOSED); 1095 this._reconnect((error.type === "timeout") ? this.reconnectOnTimeoutInterval : this.reconnectOnChannelUnavailableInterval);
1096 if (this.onerror) { this.onerror(error); } 1097 }, 1098 1099 _reconnect: function(timeout) { 1100 if (this.autoReconnect && this._keepConnected && !this._reconnecttimer && (this.readyState !== PushStream.CONNECTING)) { 1101 Log4js.info("trying to reconnect in", timeout); 1102 this._reconnecttimer = window.setTimeout(linker(this._connect, this), timeout); 1103 } 1104 }, 1105 1106 sendMessage: function(message, successCallback, errorCallback) { 1107 message = escapeText(message); 1108 if (this.wrapper.type === WebSocketWrapper.TYPE) { 1109 this.wrapper.sendMessage(message); 1110 if (successCallback) { successCallback(); } 1111 } else { 1112 Ajax.post({url: getPublisherUrl(this), data: message, success: successCallback, error: errorCallback, crossDomain: this._crossDomain}); 1113 } 1114 } 1115 }; 1116 1117 PushStream.sendMessage = function(url, message, successCallback, errorCallback) { 1118 Ajax.post({url: url, data: escapeText(message), success: successCallback, error: errorCallback}); 1119 }; 1120 1121 // to make server header template more clear, it calls register and 1122 // by a url parameter we find the stream wrapper instance 1123 PushStream.register = function(iframe) { 1124 var matcher = iframe.window.location.href.match(/streamid=([0-9]*)&?/); 1125 if (matcher[1] && PushStreamManager[matcher[1]]) { 1126 PushStreamManager[matcher[1]].wrapper.register(iframe); 1127 } 1128 }; 1129 1130 PushStream.unload = function() { 1131 for (var i = 0; i < PushStreamManager.length; i++) { 1132 try { PushStreamManager[i].disconnect(); } catch(e){} 1133 } 1134 }; 1135 1136 /* make class public */ 1137 window.PushStream = PushStream; 1138 window.PushStreamManager = PushStreamManager; 1139 if (window.jasmine) { window.Utils = Utils; } 1140 1141 if (window.attachEvent) { window.attachEvent("onunload", PushStream.unload); } 1142 if (window.addEventListener) { window.addEventListener.call(window, "unload", PushStream.unload, false); } 1143 1144})(window, document);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.