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