X-Git-Url: http://git.babelmonkeys.de/?p=xmppchat.git;a=blobdiff_plain;f=js%2Fstrophejs%2Fstrophe.js;fp=js%2Fstrophejs%2Fstrophe.js;h=aba59978224c0e79b736ce79af689aebfe483a3d;hp=0000000000000000000000000000000000000000;hb=0f774d8b5c4049745e6f8560e80f744d50eda3ee;hpb=7edeada8f83809dd0c748a1c11914e2c93fb6ad0 diff --git a/js/strophejs/strophe.js b/js/strophejs/strophe.js new file mode 100644 index 0000000..aba5997 --- /dev/null +++ b/js/strophejs/strophe.js @@ -0,0 +1,2814 @@ +/* + This program is distributed under the terms of the MIT license. + Please see the LICENSE file for details. + + Copyright 2006-2008, OGG, LLC +*/ + +/** File: strophe.js + * A JavaScript library for XMPP BOSH. + * + * This is the JavaScript version of the Strophe library. Since JavaScript + * has no facilities for persistent TCP connections, this library uses + * Bidirectional-streams Over Synchronous HTTP (BOSH) to emulate + * a persistent, stateful, two-way connection to an XMPP server. More + * information on BOSH can be found in XEP 124. + */ + +/** PrivateFunction: Function.prototype.bind + * Bind a function to an instance. + * + * This Function object extension method creates a bound method similar + * to those in Python. This means that the 'this' object will point + * to the instance you want. See + * Bound Functions and Function Imports in JavaScript + * for a complete explanation. + * + * This extension already exists in some browsers (namely, Firefox 3), but + * we provide it to support those that don't. + * + * Parameters: + * (Object) obj - The object that will become 'this' in the bound function. + * + * Returns: + * The bound function. + */ +if (!Function.prototype.bind) { + Function.prototype.bind = function (obj) + { + var func = this; + return function () { return func.apply(obj, arguments); }; + }; +} + +/** PrivateFunction: Function.prototype.prependArg + * Prepend an argument to a function. + * + * This Function object extension method returns a Function that will + * invoke the original function with an argument prepended. This is useful + * when some object has a callback that needs to get that same object as + * an argument. The following fragment illustrates a simple case of this + * > var obj = new Foo(this.someMethod); + * + * Foo's constructor can now use func.prependArg(this) to ensure the + * passed in callback function gets the instance of Foo as an argument. + * Doing this without prependArg would mean not setting the callback + * from the constructor. + * + * This is used inside Strophe for passing the Strophe.Request object to + * the onreadystatechange handler of XMLHttpRequests. + * + * Parameters: + * arg - The argument to pass as the first parameter to the function. + * + * Returns: + * A new Function which calls the original with the prepended argument. + */ +if (!Function.prototype.prependArg) { + Function.prototype.prependArg = function (arg) + { + var func = this; + + return function () { + var newargs = [arg]; + for (var i = 0; i < arguments.length; i++) + newargs.push(arguments[i]); + return func.apply(this, newargs); + }; + }; +} + +/** PrivateFunction: Array.prototype.indexOf + * Return the index of an object in an array. + * + * This function is not supplied by some JavaScript implementations, so + * we provide it if it is missing. This code is from: + * http://developer.mozilla.org/En/Core_JavaScript_1.5_Reference:Objects:Array:indexOf + * + * Parameters: + * (Object) elt - The object to look for. + * (Integer) from - The index from which to start looking. (optional). + * + * Returns: + * The index of elt in the array or -1 if not found. + */ +if (!Array.prototype.indexOf) +{ + Array.prototype.indexOf = function(elt /*, from*/) + { + var len = this.length; + + var from = Number(arguments[1]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + if (from < 0) + from += len; + + for (; from < len; from++) { + if (from in this && this[from] === elt) + return from; + } + + return -1; + }; +} + + +/** Function: $build + * Create a Strophe.Builder. + * This is an alias for 'new Strophe.Builder(name, attrs)'. + * + * Parameters: + * (String) name - The root element name. + * (Object) attrs - The attributes for the root element in object notation. + * + * Returns: + * A new Strophe.Builder object. + */ +function $build(name, attrs) { return new Strophe.Builder(name, attrs); } +/** Function: $msg + * Create a Strophe.Builder with a element as the root. + * + * Parmaeters: + * (Object) attrs - The element attributes in object notation. + * + * Returns: + * A new Strophe.Builder object. + */ +function $msg(attrs) { return new Strophe.Builder("message", attrs); } +/** Function: $iq + * Create a Strophe.Builder with an element as the root. + * + * Parameters: + * (Object) attrs - The element attributes in object notation. + * + * Returns: + * A new Strophe.Builder object. + */ +function $iq(attrs) { return new Strophe.Builder("iq", attrs); } +/** Function: $pres + * Create a Strophe.Builder with a element as the root. + * + * Parameters: + * (Object) attrs - The element attributes in object notation. + * + * Returns: + * A new Strophe.Builder object. + */ +function $pres(attrs) { return new Strophe.Builder("presence", attrs); } + +/** Class: Strophe + * An object container for all Strophe library functions. + * + * This class is just a container for all the objects and constants + * used in the library. It is not meant to be instantiated, but to + * provide a namespace for library objects, constants, and functions. + */ +Strophe = { + /** Constants: XMPP Namespace Constants + * Common namespace constants from the XMPP RFCs and XEPs. + * + * NS.HTTPBIND - HTTP BIND namespace from XEP 124. + * NS.BOSH - BOSH namespace from XEP 206. + * NS.CLIENT - Main XMPP client namespace. + * NS.AUTH - Legacy authentication namespace. + * NS.ROSTER - Roster operations namespace. + * NS.PROFILE - Profile namespace. + * NS.DISCO_INFO - Service discovery info namespace from XEP 30. + * NS.DISCO_ITEMS - Service discovery items namespace from XEP 30. + * NS.MUC - Multi-User Chat namespace from XEP 45. + * NS.SASL - XMPP SASL namespace from RFC 3920. + * NS.STREAM - XMPP Streams namespace from RFC 3920. + * NS.BIND - XMPP Binding namespace from RFC 3920. + * NS.SESSION - XMPP Session namespace from RFC 3920. + */ + NS: { + HTTPBIND: "http://jabber.org/protocol/httpbind", + BOSH: "urn:xmpp:xbosh", + CLIENT: "jabber:client", + AUTH: "jabber:iq:auth", + ROSTER: "jabber:iq:roster", + PROFILE: "jabber:iq:profile", + DISCO_INFO: "http://jabber.org/protocol/disco#info", + DISCO_ITEMS: "http://jabber.org/protocol/disco#items", + MUC: "http://jabber.org/protocol/muc", + SASL: "urn:ietf:params:xml:ns:xmpp-sasl", + STREAM: "http://etherx.jabber.org/streams", + BIND: "urn:ietf:params:xml:ns:xmpp-bind", + SESSION: "urn:ietf:params:xml:ns:xmpp-session", + VERSION: "jabber:iq:version" + }, + + /** Constants: Connection Status Constants + * Connection status constants for use by the connection handler + * callback. + * + * Status.ERROR - An error has occurred + * Status.CONNECTING - The connection is currently being made + * Status.CONNFAIL - The connection attempt failed + * Status.AUTHENTICATING - The connection is authenticating + * Status.AUTHFAIL - The authentication attempt failed + * Status.CONNECTED - The connection has succeeded + * Status.DISCONNECTED - The connection has been terminated + * Status.DISCONNECTING - The connection is currently being terminated + */ + Status: { + ERROR: 0, + CONNECTING: 1, + CONNFAIL: 2, + AUTHENTICATING: 3, + AUTHFAIL: 4, + CONNECTED: 5, + DISCONNECTED: 6, + DISCONNECTING: 7 + }, + + /** Constants: Log Level Constants + * Logging level indicators. + * + * LogLevel.DEBUG - Debug output + * LogLevel.INFO - Informational output + * LogLevel.WARN - Warnings + * LogLevel.ERROR - Errors + * LogLevel.FATAL - Fatal errors + */ + LogLevel: { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, + FATAL: 4 + }, + + /** PrivateConstants: DOM Element Type Constants + * DOM element types. + * + * ElementType.NORMAL - Normal element. + * ElementType.TEXT - Text data element. + */ + ElementType: { + NORMAL: 1, + TEXT: 3 + }, + + /** PrivateConstants: Timeout Values + * Timeout values for error states. These values are in seconds. + * These should not be changed unless you know exactly what you are + * doing. + * + * TIMEOUT - Time to wait for a request to return. This defaults to + * 70 seconds. + * SECONDARY_TIMEOUT - Time to wait for immediate request return. This + * defaults to 7 seconds. + */ + TIMEOUT: 70, + SECONDARY_TIMEOUT: 7, + + /** Function: forEachChild + * Map a function over some or all child elements of a given element. + * + * This is a small convenience function for mapping a function over + * some or all of the children of an element. If elemName is null, all + * children will be passed to the function, otherwise only children + * whose tag names match elemName will be passed. + * + * Parameters: + * (XMLElement) elem - The element to operate on. + * (String) elemName - The child element tag name filter. + * (Function) func - The function to apply to each child. This + * function should take a single argument, a DOM element. + */ + forEachChild: function (elem, elemName, func) + { + var i, childNode; + + for (i = 0; i < elem.childNodes.length; i++) { + childNode = elem.childNodes[i]; + if (childNode.nodeType == Strophe.ElementType.NORMAL && + (!elemName || this.isTagEqual(childNode, elemName))) { + func(childNode); + } + } + }, + + /** Function: isTagEqual + * Compare an element's tag name with a string. + * + * This function is case insensitive. + * + * Parameters: + * (XMLElement) el - A DOM element. + * (String) name - The element name. + * + * Returns: + * true if the element's tag name matches _el_, and false + * otherwise. + */ + isTagEqual: function (el, name) + { + return el.tagName.toLowerCase() == name.toLowerCase(); + }, + + /** Function: xmlElement + * Create an XML DOM element. + * + * This function creates an XML DOM element correctly across all + * implementations. Specifically the Microsoft implementation of + * document.createElement makes DOM elements with 43+ default attributes + * unless elements are created with the ActiveX object Microsoft.XMLDOM. + * + * Most DOMs force element names to lowercase, so we use the + * _realname attribute on the created element to store the case + * sensitive name. This is required to generate proper XML for + * things like vCard avatars (XEP 153). This attribute is stripped + * out before being sent over the wire or serialized, but you may + * notice it during debugging. + * + * Parameters: + * (String) name - The name for the element. + * (Array) attrs - An optional array of key/value pairs to use as + * element attributes in the following format [['key1', 'value1'], + * ['key2', 'value2']] + * (String) text - The text child data for the element. + * + * Returns: + * A new XML DOM element. + */ + xmlElement: function (name) + { + // FIXME: this should also support attrs argument in object notation + if (!name) { return null; } + + var node = null; + if (window.ActiveXObject) { + node = new ActiveXObject("Microsoft.XMLDOM").createElement(name); + } else { + node = document.createElement(name); + } + // use node._realname to store the case-sensitive version of the tag + // name, since some browsers will force tagnames to all lowercase. + // this is needed for the tag in XMPP specifically. + if (node.tagName != name) + node.setAttribute("_realname", name); + + // FIXME: this should throw errors if args are the wrong type or + // there are more than two optional args + var a, i; + for (a = 1; a < arguments.length; a++) { + if (!arguments[a]) { continue; } + if (typeof(arguments[a]) == "string" || + typeof(arguments[a]) == "number") { + node.appendChild(Strophe.xmlTextNode(arguments[a])); + } else if (typeof(arguments[a]) == "object" && + typeof(arguments[a]['sort']) == "function") { + for (i = 0; i < arguments[a].length; i++) { + if (typeof(arguments[a][i]) == "object" && + typeof(arguments[a][i]['sort']) == "function") { + node.setAttribute(arguments[a][i][0], + arguments[a][i][1]); + } + } + } + } + + return node; + }, + + /** Function: xmlTextNode + * Creates an XML DOM text node. + * + * Provides a cross implementation version of document.createTextNode. + * + * Parameters: + * (String) text - The content of the text node. + * + * Returns: + * A new XML DOM text node. + */ + xmlTextNode: function (text) + { + if (window.ActiveXObject) { + return new ActiveXObject("Microsoft.XMLDOM").createTextNode(text); + } else { + return document.createTextNode(text); + } + }, + + /** Function: getText + * Get the concatenation of all text children of an element. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * A String with the concatenated text of all text element children. + */ + getText: function (elem) + { + if (!elem) return null; + + var str = ""; + if (elem.childNodes.length === 0 && elem.nodeType == + Strophe.ElementType.TEXT) { + str += elem.nodeValue; + } + + for (var i = 0; i < elem.childNodes.length; i++) { + if (elem.childNodes[i].nodeType == Strophe.ElementType.TEXT) { + str += elem.childNodes[i].nodeValue; + } + } + + return str; + }, + + /** Function: copyElement + * Copy an XML DOM element. + * + * This function copies a DOM element and all its descendants and returns + * the new copy. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * A new, copied DOM element tree. + */ + copyElement: function (elem) + { + var i, el; + if (elem.nodeType == Strophe.ElementType.NORMAL) { + el = Strophe.xmlElement(elem.tagName); + + for (i = 0; i < elem.attributes.length; i++) { + el.setAttribute(elem.attributes[i].nodeName.toLowerCase(), + elem.attributes[i].value); + } + + for (i = 0; i < elem.childNodes.length; i++) { + el.appendChild(Strophe.copyElement(elem.childNodes[i])); + } + } else if (elem.nodeType == Strophe.ElementType.TEXT) { + el = Strophe.xmlTextNode(elem.nodeValue); + } + + return el; + }, + + /** Function: escapeJid + * Escape a JID. + * + * Parameters: + * (String) jid - A JID. + * + * Returns: + * An escaped JID String. + */ + escapeJid: function (jid) + { + var user = jid.split("@"); + if (user.length == 1) + // no user so nothing to escape + return jid; + + var host = user.splice(user.length - 1, 1)[0]; + user = user.join("@") + .replace(/^\s+|\s+$/g, '') + .replace(/\\/g, "\\5c") + .replace(/ /g, "\\20") + .replace(/\"/g, "\\22") + .replace(/\&/g, "\\26") + .replace(/\'/g, "\\27") + .replace(/\//g, "\\2f") + .replace(/:/g, "\\3a") + .replace(//g, "\\3e") + .replace(/@/g, "\\40"); + + return [user, host].join("@"); + }, + + /** Function: unescapeJid + * Unescape a JID. + * + * Parameters: + * (String) jid - A JID. + * + * Returns: + * An unescaped JID String. + */ + unescapeJid: function (jid) + { + return jid.replace(/\\20/g, " ") + .replace(/\\22/g, '"') + .replace(/\\26/g, "&") + .replace(/\\27/g, "'") + .replace(/\\2f/g, "/") + .replace(/\\3a/g, ":") + .replace(/\\3c/g, "<") + .replace(/\\3e/g, ">") + .replace(/\\40/g, "@") + .replace(/\\5c/g, "\\"); + }, + + /** Function: getNodeFromJid + * Get the node portion of a JID String. + * + * Parameters: + * (String) jid - A JID. + * + * Returns: + * A String containing the node. + */ + getNodeFromJid: function (jid) + { + if (jid.indexOf("@") < 0) + return null; + return Strophe.escapeJid(jid).split("@")[0]; + }, + + /** Function: getDomainFromJid + * Get the domain portion of a JID String. + * + * Parameters: + * (String) jid - A JID. + * + * Returns: + * A String containing the domain. + */ + getDomainFromJid: function (jid) + { + var bare = Strophe.escapeJid(Strophe.getBareJidFromJid(jid)); + if (bare.indexOf("@") < 0) + return bare; + else + return bare.split("@")[1]; + }, + + /** Function: getResourceFromJid + * Get the resource portion of a JID String. + * + * Parameters: + * (String) jid - A JID. + * + * Returns: + * A String containing the resource. + */ + getResourceFromJid: function (jid) + { + var s = Strophe.escapeJid(jid).split("/"); + if (s.length < 2) return null; + return s[1]; + }, + + /** Function: getBareJidFromJid + * Get the bare JID from a JID String. + * + * Parameters: + * (String) jid - A JID. + * + * Returns: + * A String containing the bare JID. + */ + getBareJidFromJid: function (jid) + { + return this.escapeJid(jid).split("/")[0]; + }, + + /** Function: log + * User overrideable logging function. + * + * This function is called whenever the Strophe library calls any + * of the logging functions. The default implementation of this + * function does nothing. If client code wishes to handle the logging + * messages, it should override this with + * > Strophe.log = function (level, msg) { + * > (user code here) + * > }; + * + * Please note that data sent and received over the wire is logged + * via Strophe.Connection.rawInput() and Strophe.Connection.rawOutput(). + * + * The different levels and their meanings are + * + * DEBUG - Messages useful for debugging purposes. + * INFO - Informational messages. This is mostly information like + * 'disconnect was called' or 'SASL auth succeeded'. + * WARN - Warnings about potential problems. This is mostly used + * to report transient connection errors like request timeouts. + * ERROR - Some error occurred. + * FATAL - A non-recoverable fatal error occurred. + * + * Parameters: + * (Integer) level - The log level of the log message. This will + * be one of the values in Strophe.LogLevel. + * (String) msg - The log message. + */ + log: function (level, msg) + { + return; + }, + + /** Function: debug + * Log a message at the Strophe.LogLevel.DEBUG level. + * + * Parameters: + * (String) msg - The log message. + */ + debug: function(msg) + { + this.log(this.LogLevel.DEBUG, msg); + }, + + /** Function: info + * Log a message at the Strophe.LogLevel.INFO level. + * + * Parameters: + * (String) msg - The log message. + */ + info: function (msg) + { + this.log(this.LogLevel.INFO, msg); + }, + + /** Function: warn + * Log a message at the Strophe.LogLevel.WARN level. + * + * Parameters: + * (String) msg - The log message. + */ + warn: function (msg) + { + this.log(this.LogLevel.WARN, msg); + }, + + /** Function: error + * Log a message at the Strophe.LogLevel.ERROR level. + * + * Parameters: + * (String) msg - The log message. + */ + error: function (msg) + { + this.log(this.LogLevel.ERROR, msg); + }, + + /** Function: fatal + * Log a message at the Strophe.LogLevel.FATAL level. + * + * Parameters: + * (String) msg - The log message. + */ + fatal: function (msg) + { + this.log(this.LogLevel.FATAL, msg); + }, + + /** Function: serialize + * Render a DOM element and all descendants to a String. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * The serialized element tree as a String. + */ + serialize: function (elem) + { + var result; + + if (!elem) return null; + + var nodeName = elem.nodeName; + var i, child; + + if (elem.getAttribute("_realname")) { + nodeName = elem.getAttribute("_realname"); + } + + result = "<" + nodeName; + for (i = 0; i < elem.attributes.length; i++) { + if(elem.attributes[i].nodeName != "_realname") { + result += " " + elem.attributes[i].nodeName.toLowerCase() + + "='" + elem.attributes[i].value + .replace("'", "'").replace("&", "&") + "'"; + } + } + + if (elem.childNodes.length > 0) { + result += ">"; + for (i = 0; i < elem.childNodes.length; i++) { + child = elem.childNodes[i]; + if (child.nodeType == Strophe.ElementType.NORMAL) { + // normal element, so recurse + result += Strophe.serialize(child); + } else if (child.nodeType == Strophe.ElementType.TEXT) { + // text element + result += child.nodeValue; + } + } + result += ""; + } else { + result += "/>"; + } + + return result; + }, + + /** PrivateVariable: _requestId + * _Private_ variable that keeps track of the request ids for + * connections. + */ + _requestId: 0 +}; + +/** Class: Strophe.Builder + * XML DOM builder. + * + * This object provides an interface similar to JQuery but for building + * DOM element easily and rapidly. All the functions except for toString() + * and tree() return the object, so calls can be chained. Here's an + * example using the $iq() builder helper. + * > $iq({to: 'you': from: 'me': type: 'get', id: '1'}) + * > .c('query', {xmlns: 'strophe:example'}) + * > .c('example') + * > .toString() + * The above generates this XML fragment + * > + * > + * > + * > + * > + * The corresponding DOM manipulations to get a similar fragment would be + * a lot more tedious and probably involve several helper variables. + * + * Since adding children makes new operations operate on the child, up() + * is provided to traverse up the tree. To add two children, do + * > builder.c('child1', ...).up().c('child2', ...) + * The next operation on the Builder will be relative to the second child. + */ + +/** Constructor: Strophe.Builder + * Create a Strophe.Builder object. + * + * The attributes should be passed in object notation. For example + * > var b = new Builder('message', {to: 'you', from: 'me'}); + * or + * > var b = new Builder('messsage', {'xml:lang': 'en'}); + * + * Parameters: + * (String) name - The name of the root element. + * (Object) attrs - The attributes for the root element in object notation. + * + * Returns: + * A new Strophe.Builder. + */ +Strophe.Builder = function (name, attrs) +{ + // Holds the tree being built. + this.nodeTree = this._makeNode(name, attrs); + + // Points to the current operation node. + this.node = this.nodeTree; +}; + +Strophe.Builder.prototype = { + /** Function: tree + * Return the DOM tree. + * + * This function returns the current DOM tree as an element object. This + * is suitable for passing to functions like Strophe.Connection.send(). + * + * Returns: + * The DOM tree as a element object. + */ + tree: function () + { + return this.nodeTree; + }, + + /** Function: toString + * Serialize the DOM tree to a String. + * + * This function returns a string serialization of the current DOM + * tree. It is often used internally to pass data to a + * Strophe.Request object. + * + * Returns: + * The serialized DOM tree in a String. + */ + toString: function () + { + return Strophe.serialize(this.nodeTree); + }, + + /** Function: up + * Make the current parent element the new current element. + * + * This function is often used after c() to traverse back up the tree. + * For example, to add two children to the same element + * > builder.c('child1', {}).up().c('child2', {}); + * + * Returns: + * The Stophe.Builder object. + */ + up: function () + { + this.node = this.node.parentNode; + return this; + }, + + /** Function: attrs + * Add or modify attributes of the current element. + * + * The attributes should be passed in object notation. This function + * does not move the current element pointer. + * + * Parameters: + * (Object) moreattrs - The attributes to add/modify in object notation. + * + * Returns: + * The Strophe.Builder object. + */ + attrs: function (moreattrs) + { + for (var k in moreattrs) + this.node.setAttribute(k, moreattrs[k]); + return this; + }, + + /** Function: c + * Add a child to the current element and make it the new current + * element. + * + * This function moves the current element pointer to the child. If you + * need to add another child, it is necessary to use up() to go back + * to the parent in the tree. + * + * Parameters: + * (String) name - The name of the child. + * (Object) attrs - The attributes of the child in object notation. + * + * Returns: + * The Strophe.Builder object. + */ + c: function (name, attrs) + { + var child = this._makeNode(name, attrs); + this.node.appendChild(child); + this.node = child; + return this; + }, + + /** Function: cnode + * Add a child to the current element and make it the new current + * element. + * + * This function is the same as c() except that instead of using a + * name and an attributes object to create the child it uses an + * existing DOM element object. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * The Strophe.Builder object. + */ + cnode: function (elem) + { + this.node.appendChild(elem); + this.node = elem; + return this; + }, + + /** Function: t + * Add a child text element. + * + * This *does not* make the child the new current element since there + * are no children of text elements. + * + * Parameters: + * (String) text - The text data to append to the current element. + * + * Returns: + * The Strophe.Builder object. + */ + t: function (text) + { + var child = Strophe.xmlTextNode(text); + this.node.appendChild(child); + return this; + }, + + /** PrivateFunction: _makeNode + * _Private_ helper function to create a DOM element. + * + * Parameters: + * (String) name - The name of the new element. + * (Object) attrs - The attributes for the new element in object + * notation. + * + * Returns: + * A new DOM element. + */ + _makeNode: function (name, attrs) + { + var node = Strophe.xmlElement(name); + for (var k in attrs) + node.setAttribute(k, attrs[k]); + return node; + } +}; + + +/** PrivateClass: Strophe.Handler + * _Private_ helper class for managing stanza handlers. + * + * A Strophe.Handler encapsulates a user provided callback function to be + * executed when matching stanzas are received by the connection. + * Handlers can be either one-off or persistant depending on their + * return value. Returning true will cause a Handler to remain active, and + * returning false will remove the Handler. + * + * Users will not use Strophe.Handler objects directly, but instead they + * will use Strophe.Connection.addHandler() and + * Strophe.Connection.deleteHandler(). + */ + +/** PrivateConstructor: Strophe.Handler + * Create and initialize a new Strophe.Handler. + * + * Parameters: + * (Function) handler - A function to be executed when the handler is run. + * (String) ns - The namespace to match. + * (String) name - The element name to match. + * (String) type - The element type to match. + * (String) id - The element id attribute to match. + * (String) from - The element from attribute to match. + * + * Returns: + * A new Strophe.Handler object. + */ +Strophe.Handler = function (handler, ns, name, type, id, from) +{ + this.handler = handler; + this.ns = ns; + this.name = name; + this.type = type; + this.id = id; + this.from = from; + + // whether the handler is a user handler or a system handler + this.user = true; +}; + +Strophe.Handler.prototype = { + /** PrivateFunction: isMatch + * Tests if a stanza matches the Strophe.Handler. + * + * Parameters: + * (XMLElement) elem - The XML element to test. + * + * Returns: + * true if the stanza matches and false otherwise. + */ + isMatch: function (elem) + { + var nsMatch, i; + + nsMatch = false; + if (!this.ns) { + nsMatch = true; + } else { + var self = this; + Strophe.forEachChild(elem, null, function (elem) { + if (elem.getAttribute("xmlns") == self.ns) + nsMatch = true; + }); + + nsMatch = nsMatch || elem.getAttribute("xmlns") == this.ns; + } + + if (nsMatch && + (!this.name || Strophe.isTagEqual(elem, this.name)) && + (!this.type || elem.getAttribute("type") == this.type) && + (!this.id || elem.getAttribute("id") == this.id) && + (!this.from || elem.getAttribute("from") == this.from)) { + return true; + } + + return false; + }, + + /** PrivateFunction: run + * Run the callback on a matching stanza. + * + * Parameters: + * (XMLElement) elem - The DOM element that triggered the + * Strophe.Handler. + * + * Returns: + * A boolean indicating if the handler should remain active. + */ + run: function (elem) + { + var result = null; + try { + result = this.handler(elem); + } catch (e) { + if (e.sourceURL) { + Strophe.fatal("error: " + this.handler + + " " + e.sourceURL + ":" + + e.line + " - " + e.name + ": " + e.message); + } else if (e.fileName) { + if (typeof(console) != "undefined") { + console.trace(); + console.error(this.handler, " - error - ", e, e.message); + } + Strophe.fatal("error: " + this.handler + " " + + e.fileName + ":" + e.lineNumber + " - " + + e.name + ": " + e.message); + } else { + Strophe.fatal("error: " + this.handler); + } + + throw e; + } + + return result; + }, + + /** PrivateFunction: toString + * Get a String representation of the Strophe.Handler object. + * + * Returns: + * A String. + */ + toString: function () + { + return "{Handler: " + this.handler + "(" + this.name + "," + + this.id + "," + this.ns + ")}"; + } +}; + +/** PrivateClass: Strophe.TimedHandler + * _Private_ helper class for managing timed handlers. + * + * A Strophe.TimedHandler encapsulates a user provided callback that + * should be called after a certain period of time or at regular + * intervals. The return value of the callback determines whether the + * Strophe.TimedHandler will continue to fire. + * + * Users will not use Strophe.TimedHandler objects directly, but instead + * they will use Strophe.Connection.addTimedHandler() and + * Strophe.Connection.deleteTimedHandler(). + */ + +/** PrivateConstructor: Strophe.TimedHandler + * Create and initialize a new Strophe.TimedHandler object. + * + * Parameters: + * (Integer) period - The number of milliseconds to wait before the + * handler is called. + * (Function) handler - The callback to run when the handler fires. This + * function should take no arguments. + * + * Returns: + * A new Strophe.TimedHandler object. + */ +Strophe.TimedHandler = function (period, handler) +{ + this.period = period; + this.handler = handler; + + this.lastCalled = new Date().getTime(); + this.user = true; +}; + +Strophe.TimedHandler.prototype = { + /** PrivateFunction: run + * Run the callback for the Strophe.TimedHandler. + * + * Returns: + * true if the Strophe.TimedHandler should be called again, and false + * otherwise. + */ + run: function () + { + this.lastCalled = new Date().getTime(); + return this.handler(); + }, + + /** PrivateFunction: reset + * Reset the last called time for the Strophe.TimedHandler. + */ + reset: function () + { + this.lastCalled = new Date().getTime(); + }, + + /** PrivateFunction: toString + * Get a string representation of the Strophe.TimedHandler object. + * + * Returns: + * The string representation. + */ + toString: function () + { + return "{TimedHandler: " + this.handler + "(" + this.period +")}"; + } +}; + +/** PrivateClass: Strophe.Request + * _Private_ helper class that provides a cross implementation abstraction + * for a BOSH related XMLHttpRequest. + * + * The Strophe.Request class is used internally to encapsulate BOSH request + * information. It is not meant to be used from user's code. + */ + +/** PrivateConstructor: Strophe.Request + * Create and initialize a new Strophe.Request object. + * + * Parameters: + * (String) data - The data to be sent in the request. + * (Function) func - The function that will be called when the + * XMLHttpRequest readyState changes. + * (Integer) rid - The BOSH rid attribute associated with this request. + * (Integer) sends - The number of times this same request has been + * sent. + */ +Strophe.Request = function (data, func, rid, sends) +{ + this.id = ++Strophe._requestId; + this.data = data; + // save original function in case we need to make a new request + // from this one. + this.origFunc = func; + this.func = func; + this.rid = rid; + this.date = NaN; + this.sends = sends || 0; + this.abort = false; + this.dead = null; + this.age = function () { + if (!this.date) return 0; + var now = new Date(); + return (now - this.date) / 1000; + }; + this.timeDead = function () { + if (!this.dead) return 0; + var now = new Date(); + return (now - this.dead) / 1000; + }; + this.xhr = this._newXHR(); +}; + +Strophe.Request.prototype = { + /** PrivateFunction: getResponse + * Get a response from the underlying XMLHttpRequest. + * + * This function attempts to get a response from the request and checks + * for errors. + * + * Throws: + * "parsererror" - A parser error occured. + * + * Returns: + * The DOM element tree of the response. + */ + getResponse: function () + { + var node = null; + if (this.xhr.responseXML && this.xhr.responseXML.documentElement) { + node = this.xhr.responseXML.documentElement; + if (node.tagName == "parsererror") { + Strophe.error("invalid response received"); + Strophe.error("responseText: " + this.xhr.responseText); + Strophe.error("responseXML: " + + Strophe.serialize(this.xhr.responseXML)); + throw "parsererror"; + } + } else if (this.xhr.responseText) { + Strophe.error("invalid response received"); + Strophe.error("responseText: " + this.xhr.responseText); + Strophe.error("responseXML: " + + Strophe.serialize(this.xhr.responseXML)); + } + + return node; + }, + + /** PrivateFunction: _newXHR + * _Private_ helper function to create XMLHttpRequests. + * + * This function creates XMLHttpRequests across all implementations. + * + * Returns: + * A new XMLHttpRequest. + */ + _newXHR: function () + { + var xhr = null; + if (window.XMLHttpRequest) { + xhr = new XMLHttpRequest(); + if (xhr.overrideMimeType) { + xhr.overrideMimeType("text/xml"); + } + } else if (window.ActiveXObject) { + xhr = new ActiveXObject("Microsoft.XMLHTTP"); + } + + xhr.onreadystatechange = this.func.prependArg(this); + + return xhr; + } +}; + +/** Class: Strophe.Connection + * XMPP Connection manager. + * + * Thie class is the main part of Strophe. It manages a BOSH connection + * to an XMPP server and dispatches events to the user callbacks as + * data arrives. It supports SASL PLAIN, SASL DIGEST-MD5, and legacy + * authentication. + * + * After creating a Strophe.Connection object, the user will typically + * call connect() with a user supplied callback to handle connection level + * events like authentication failure, disconnection, or connection + * complete. + * + * The user will also have several event handlers defined by using + * addHandler() and addTimedHandler(). These will allow the user code to + * respond to interesting stanzas or do something periodically with the + * connection. These handlers will be active once authentication is + * finished. + * + * To send data to the connection, use send(). + */ + +/** Constructor: Strophe.Connection + * Create and initialize a Strophe.Connection object. + * + * Parameters: + * (String) service - The BOSH service URL. + * + * Returns: + * A new Strophe.Connection object. + */ +Strophe.Connection = function (service) +{ + /* The path to the httpbind service. */ + this.service = service; + /* The connected JID. */ + this.jid = ""; + /* request id for body tags */ + this.rid = Math.floor(Math.random() * 4294967295); + /* The current session ID. */ + this.sid = null; + this.streamId = null; + + // SASL + this.do_session = false; + this.do_bind = false; + + // handler lists + this.timedHandlers = []; + this.handlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + + this._idleTimeout = null; + this._disconnectTimeout = null; + + this.authenticated = false; + this.disconnecting = false; + this.connected = false; + + this.errors = 0; + + this.paused = false; + + // default BOSH window + this.window = 5; + + this._data = []; + this._requests = []; + this._uniqueId = Math.round(Math.random() * 10000); + + this._sasl_success_handler = null; + this._sasl_failure_handler = null; + this._sasl_challenge_handler = null; + + // setup onIdle callback every 1/10th of a second + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); +}; + +Strophe.Connection.prototype = { + /** Function: reset + * Reset the connection. + * + * This function should be called after a connection is disconnected + * before that connection is reused. + */ + reset: function () + { + this.rid = Math.floor(Math.random() * 4294967295); + + this.sid = null; + this.streamId = null; + + // SASL + this.do_session = false; + this.do_bind = false; + + // handler lists + this.timedHandlers = []; + this.handlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + + this.authenticated = false; + this.disconnecting = false; + this.connected = false; + + this.errors = 0; + + this._requests = []; + this._uniqueId = Math.round(Math.random()*10000); + }, + + /** Function: pause + * Pause the request manager. + * + * This will prevent Strophe from sending any more requests to the + * server. This is very useful for temporarily pausing while a lot + * of send() calls are happening quickly. This causes Strophe to + * send the data in a single request, saving many request trips. + */ + pause: function () + { + this.paused = true; + }, + + /** Function: resume + * Resume the request manager. + * + * This resumes after pause() has been called. + */ + resume: function () + { + this.paused = false; + }, + + /** Function: getUniqueId + * Generate a unique ID for use in elements. + * + * All stanzas are required to have unique id attributes. This + * function makes creating these easy. Each connection instance has + * a counter which starts from zero, and the value of this counter + * plus a colon followed by the suffix becomes the unique id. If no + * suffix is supplied, the counter is used as the unique id. + * + * Suffixes are used to make debugging easier when reading the stream + * data, and their use is recommended. The counter resets to 0 for + * every new connection for the same reason. For connections to the + * same server that authenticate the same way, all the ids should be + * the same, which makes it easy to see changes. This is useful for + * automated testing as well. + * + * Parameters: + * (String) suffix - A optional suffix to append to the id. + * + * Returns: + * A unique string to be used for the id attribute. + */ + getUniqueId: function (suffix) + { + if (typeof(suffix) == "string" || typeof(suffix) == "number") { + return ++this._uniqueId + ":" + suffix; + } else { + return ++this._uniqueId + ""; + } + }, + + /** Function: connect + * Starts the connection process. + * + * As the connection process proceeds, the user supplied callback will + * be triggered multiple times with status updates. The callback + * should take two arguments - the status code and the error condition. + * + * The status code will be one of the values in the Strophe.Status + * constants. The error condition will be one of the conditions + * defined in RFC 3920 or the condition 'strophe-parsererror'. + * + * Please see XEP 124 for a more detailed explanation of the optional + * parameters below. + * + * Parameters: + * (String) jid - The user's JID. This may be a bare JID, + * or a full JID. If a node is not supplied, SASL ANONYMOUS + * authentication will be attempted. + * (String) pass - The user's password. + * (Function) callback The connect callback function. + * (Integer) wait - The optional HTTPBIND wait value. This is the + * time the server will wait before returning an empty result for + * a request. The default setting of 60 seconds is recommended. + * Other settings will require tweaks to the Strophe.TIMEOUT value. + * (Integer) hold - The optional HTTPBIND hold value. This is the + * number of connections the server will hold at one time. This + * should almost always be set to 1 (the default). + * (Integer) wind - The optional HTTBIND window value. This is the + * allowed range of request ids that are valid. The default is 5. + */ + connect: function (jid, pass, callback, wait, hold, wind) + { + this.jid = jid; + this.pass = pass; + this.connect_callback = callback; + this.disconnecting = false; + this.connected = false; + this.authenticated = false; + this.errors = 0; + + if (!wait) wait = 60; + if (!hold) hold = 1; + if (wind) this.window = wind; + + // parse jid for domain and resource + this.domain = Strophe.getDomainFromJid(this.jid); + + // build the body tag + var body = this._buildBody().attrs({ + to: this.domain, + "xml:lang": "en", + wait: wait, + hold: hold, + window: this.window, + content: "text/xml; charset=utf-8", + ver: "1.6", + "xmpp:version": "1.0", + "xmlns:xmpp": Strophe.NS.BOSH + }); + + this.connect_callback(Strophe.Status.CONNECTING, null); + + this._requests.push( + new Strophe.Request(body.toString(), + this._onRequestStateChange.bind(this) + .prependArg(this._connect_cb.bind(this)), + body.tree().getAttribute("rid"))); + this._throttledRequestHandler(); + }, + + /** Function: attach + * Attach to an already created and authenticated BOSH session. + * + * This function is provided to allow Strophe to attach to BOSH + * sessions which have been created externally, perhaps by a Web + * application. This is often used to support auto-login type features + * without putting user credentials into the page. + * + * Parameters: + * (String) jid - The full JID that is bound by the session. + * (String) sid - The SID of the BOSH session. + * (String) rid - The current RID of the BOSH session. This RID + * will be used by the next request. + * (Function) callback The connect callback function. + */ + attach: function (jid, sid, rid, callback) + { + this.jid = jid; + this.sid = sid; + this.rid = rid; + this.connect_callback = callback; + + this.domain = Strophe.getDomainFromJid(this.jid); + + this.authenticated = true; + this.connected = true; + }, + + /** Function: rawInput + * User overrideable function that receives raw data coming into the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.rawInput = function (data) { + * > (user code) + * > }; + * + * Parameters: + * (String) data - The data received by the connection. + */ + rawInput: function (data) + { + return; + }, + + /** Function: rawOutput + * User overrideable function that receives raw data sent to the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.rawOutput = function (data) { + * > (user code) + * > }; + * + * Parameters: + * (String) data - The data sent by the connection. + */ + rawOutput: function (data) + { + return; + }, + + /** Function: send + * Send a stanza. + * + * This function is called to push data onto the send queue to + * go out over the wire. Whenever a request is sent to the BOSH + * server, all pending data is sent and the queue is flushed. + * + * Parameters: + * (XMLElement) elem - The stanza to send. + */ + send: function (elem) + { + if (elem !== null && typeof(elem["sort"]) == "function") { + for (var i = 0; i < elem.length; i++) { + this._data.push(elem[i]); + } + } else { + this._data.push(elem); + } + + this._throttledRequestHandler(); + clearTimeout(this._idleTimeout); + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + }, + + /** PrivateFunction: _sendRestart + * Send an xmpp:restart stanza. + */ + _sendRestart: function () + { + this._data.push("restart"); + + this._throttledRequestHandler(); + clearTimeout(this._idleTimeout); + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + }, + + /** Function: addTimedHandler + * Add a timed handler to the connection. + * + * This function adds a timed handler. The provided handler will + * be called every period milliseconds until it returns false, + * the connection is terminated, or the handler is removed. Handlers + * that wish to continue being invoked should return true. + * + * Because of method binding it is necessary to save the result of + * this function if you wish to remove a handler with + * deleteTimedHandler(). + * + * Note that user handlers are not active until authentication is + * successful. + * + * Parameters: + * (Integer) period - The period of the handler. + * (Function) handler - The callback function. + * + * Returns: + * A reference to the handler that can be used to remove it. + */ + addTimedHandler: function (period, handler) + { + var thand = new Strophe.TimedHandler(period, handler); + this.addTimeds.push(thand); + return thand; + }, + + /** Function: deleteTimedHandler + * Delete a timed handler for a connection. + * + * This function removes a timed handler from the connection. The + * handRef parameter is *not* the function passed to addTimedHandler(), + * but is the reference returned from addTimedHandler(). + * + * Parameters: + * (Strophe.TimedHandler) handRef - The handler reference. + */ + deleteTimedHandler: function (handRef) + { + // this must be done in the Idle loop so that we don't change + // the handlers during iteration + this.removeTimeds.push(handRef); + }, + + /** Function: addHandler + * Add a stanza handler for the connection. + * + * This function adds a stanza handler to the connection. The + * handler callback will be called for any stanza that matches + * the parameters. Note that if multiple parameters are supplied, + * they must all match for the handler to be invoked. + * + * The handler will receive the stanza that triggered it as its argument. + * The handler should return true if it is to be invoked again; + * returning false will remove the handler after it returns. + * + * As a convenience, the ns parameters applies to the top level element + * and also any of its immediate children. This is primarily to make + * matching /iq/query elements easy. + * + * The return value should be saved if you wish to remove the handler + * with deleteHandler(). + * + * Parameters: + * (Function) handler - The user callback. + * (String) ns - The namespace to match. + * (String) name - The stanza name to match. + * (String) type - The stanza type attribute to match. + * (String) id - The stanza id attribute to match. + * (String) from - The stanza from attribute to match. + * + * Returns: + * A reference to the handler that can be used to remove it. + */ + addHandler: function (handler, ns, name, type, id, from) + { + var hand = new Strophe.Handler(handler, ns, name, type, id, from); + this.addHandlers.push(hand); + return hand; + }, + + /** Function: deleteHandler + * Delete a stanza handler for a connection. + * + * This function removes a stanza handler from the connection. The + * handRef parameter is *not* the function passed to addHandler(), + * but is the reference returned from addHandler(). + * + * Parameters: + * (Strophe.Handler) handRef - The handler reference. + */ + deleteHandler: function (handRef) + { + // this must be done in the Idle loop so that we don't change + // the handlers during iteration + this.removeHandlers.push(handRef); + }, + + /** Function: disconnect + * Start the graceful disconnection process. + * + * This function starts the disconnection process. This process starts + * by sending unavailable presence and sending BOSH body of type + * terminate. A timeout handler makes sure that disconnection happens + * even if the BOSH server does not respond. + * + * The user supplied connection callback will be notified of the + * progress as this process happens. + */ + disconnect: function () + { + Strophe.info("disconnect was called"); + if (this.connected) { + // setup timeout handler + this._disconnectTimeout = this._addSysTimedHandler( + 3000, this._onDisconnectTimeout.bind(this)); + this._sendTerminate(); + } + }, + + /** PrivateFunction: _buildBody + * _Private_ helper function to generate the wrapper for BOSH. + * + * Returns: + * A Strophe.Builder with a element. + */ + _buildBody: function () + { + var bodyWrap = $build('body', { + rid: this.rid++, + xmlns: Strophe.NS.HTTPBIND + }); + + if (this.sid !== null) { + bodyWrap.attrs({sid: this.sid}); + } + + return bodyWrap; + }, + + /** PrivateFunction: _removeRequest + * _Private_ function to remove a request from the queue. + * + * Parameters: + * (Strophe.Request) req - The request to remove. + */ + _removeRequest: function (req) + { + Strophe.debug("removing request"); + + var i; + for (i = this._requests.length - 1; i >= 0; i--) { + if (req == this._requests[i]) { + this._requests.splice(i, 1); + } + } + + // set the onreadystatechange handler to a null function so + // that we don't get any misfires + req.xhr.onreadystatechange = function () {}; + + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _restartRequest + * _Private_ function to restart a request that is presumed dead. + * + * Parameters: + * (Integer) i - The index of the request in the queue. + */ + _restartRequest: function (i) + { + var req = this._requests[i]; + if (req.dead === null) { + req.dead = new Date(); + } + + this._processRequest(i); + }, + + /** PrivateFunction: _processRequest + * _Private_ function to process a request in the queue. + * + * This function takes requests off the queue and sends them and + * restarts dead requests. + * + * Parameters: + * (Integer) i - The index of the request in the queue. + */ + _processRequest: function (i) + { + var req = this._requests[i]; + var reqStatus = -1; + + try { + if (req.xhr.readyState == 4) { + reqStatus = req.xhr.status; + } + } catch (e) { + Strophe.error("caught an error in _requests[" + i + + "], reqStatus: " + reqStatus); + } + + if (typeof(reqStatus) == "undefined") { + reqStatus = -1; + } + + var now = new Date(); + var time_elapsed = req.age(); + var primaryTimeout = (!isNaN(time_elapsed) && + time_elapsed > Strophe.TIMEOUT); + var secondaryTimeout = (req.dead !== null && + req.timeDead() > Strophe.SECONDARY_TIMEOUT); + var requestCompletedWithServerError = (req.xhr.readyState == 4 && + (reqStatus < 1 || + reqStatus >= 500)); + var oldreq; + + if (primaryTimeout || secondaryTimeout || + requestCompletedWithServerError) { + if (secondaryTimeout) { + Strophe.error("Request " + + this._requests[i].id + + " timed out (secondary), restarting"); + } + req.abort = true; + req.xhr.abort(); + oldreq = req; + this._requests[i] = new Strophe.Request(req.data, + req.origFunc, + req.rid, + req.sends); + req = this._requests[i]; + } + + if (req.xhr.readyState === 0) { + Strophe.debug("request id " + req.id + + "." + req.sends + " posting"); + + req.date = new Date(); + try { + req.xhr.open("POST", this.service, true); + } catch (e) { + Strophe.error("XHR open failed."); + if (!this.connected) + this.connect_callback(Strophe.Status.CONNFAIL, + "bad-service"); + this.disconnect(); + return; + } + + // Fires the XHR request -- may be invoked immediately + // or on a gradually expanding retry window for reconnects + var sendFunc = function () { + req.xhr.send(req.data); + }; + + // Implement progressive backoff for reconnects -- + // First retry (send == 1) should also be instantaneous + if (req.sends > 1) { + // Using a cube of the retry number creats a nicely + // expanding retry window + var backoff = Math.pow(req.sends, 3) * 1000; + setTimeout(sendFunc, backoff); + } else { + sendFunc(); + } + + req.sends++; + + this.rawOutput(req.data); + } else { + Strophe.debug("_throttledRequestHandler: " + + (i === 0 ? "first" : "second") + + " request has readyState of " + + req.xhr.readyState); + } + }, + + /** PrivateFunction: _throttledRequestHandler + * _Private_ function to throttle requests to the connection window. + * + * This function makes sure we don't send requests so fast that the + * request ids overflow the connection window in the case that one + * request died. + */ + _throttledRequestHandler: function () + { + if (!this._requests) { + Strophe.debug("_throttledRequestHandler called with " + + "undefined requests"); + } else { + Strophe.debug("_throttledRequestHandler called with " + + this._requests.length + " requests"); + } + + if (!this._requests || this._requests.length === 0) { + return; + } + + if (this._requests.length > 0) { + this._processRequest(0); + } + + if (this._requests.length > 1 && + Math.abs(this._requests[0].rid - + this._requests[1].rid) < this.window - 1) { + this._processRequest(1); + } + }, + + /** PrivateFunction: _onRequestStateChange + * _Private_ handler for Strophe.Request state changes. + * + * This function is called when the XMLHttpRequest readyState changes. + * It contains a lot of error handling logic for the many ways that + * requests can fail, and calls the request callback when requests + * succeed. + * + * Parameters: + * (Function) func - The handler for the request. + * (Strophe.Request) req - The request that is changing readyState. + */ + _onRequestStateChange: function (func, req) + { + Strophe.debug("request id " + req.id + + "." + req.sends + " state changed to " + + req.xhr.readyState); + + if (req.abort) { + req.abort = false; + return; + } + + // request complete + var reqStatus; + if (req.xhr.readyState == 4) { + reqStatus = 0; + try { + reqStatus = req.xhr.status; + } catch (e) { + // ignore errors from undefined status attribute. works + // around a browser bug + } + + if (typeof(reqStatus) == "undefined") { + reqStatus = 0; + } + + if (this.disconnecting) { + if (reqStatus >= 400) { + this._hitError(reqStatus); + return; + } + } + + var reqIs0 = (this._requests[0] == req); + var reqIs1 = (this._requests[1] == req); + + if ((reqStatus > 0 && reqStatus < 500) || req.sends > 5) { + // remove from internal queue + this._removeRequest(req); + Strophe.debug("request id " + + req.id + + " should now be removed"); + } + + // request succeeded + if (reqStatus == 200) { + // if request 1 finished, or request 0 finished and request + // 1 is over Strophe.SECONDARY_TIMEOUT seconds old, we need to + // restart the other - both will be in the first spot, as the + // completed request has been removed from the queue already + if (reqIs1 || + (reqIs0 && this._requests.length > 0 && + this._requests[0].age() > Strophe.SECONDARY_TIMEOUT)) { + this._restartRequest(0); + } + // call handler + Strophe.debug("request id " + + req.id + "." + + req.sends + " got 200"); + func(req); + this.errors = 0; + } else { + Strophe.error("request id " + + req.id + "." + + req.sends + " error " + reqStatus + + " happened"); + if (reqStatus === 0 || + (reqStatus >= 400 && reqStatus < 600) || + reqStatus >= 12000) { + this._hitError(reqStatus); + if (reqStatus >= 400 && reqStatus < 500) { + this.connect_callback(Strophe.Status.DISCONNECTING, + null); + this._doDisconnect(); + } + } + } + + if (!((reqStatus > 0 && reqStatus < 10000) || + req.sends > 5)) { + this._throttledRequestHandler(); + } + } + }, + + /** PrivateFunction: _hitError + * _Private_ function to handle the error count. + * + * Requests are resent automatically until their error count reaches + * 5. Each time an error is encountered, this function is called to + * increment the count and disconnect if the count is too high. + * + * Parameters: + * (Integer) reqStatus - The request status. + */ + _hitError: function (reqStatus) + { + this.errors++; + Strophe.warn("request errored, status: " + reqStatus + + ", number of errors: " + this.errors); + if (this.errors > 4) { + this._onDisconnectTimeout(); + } + }, + + /** PrivateFunction: _doDisconnect + * _Private_ function to disconnect. + * + * This is the last piece of the disconnection logic. This resets the + * connection and alerts the user's connection callback. + */ + _doDisconnect: function () + { + Strophe.info("_doDisconnect was called"); + this.authenticated = false; + this.disconnecting = false; + this.sid = null; + this.streamId = null; + this.rid = Math.floor(Math.random() * 4294967295); + + // tell the parent we disconnected + if (this.connected) { + this.connect_callback(Strophe.Status.DISCONNECTED, null); + this.connected = false; + } + + // delete handlers + this.handlers = []; + this.timedHandlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + }, + + /** PrivateFunction: _dataRecv + * _Private_ handler to processes incoming data from the the connection. + * + * Except for _connect_cb handling the initial connection request, + * this function handles the incoming data for all requests. This + * function also fires stanza handlers that match each incoming + * stanza. + * + * Parameters: + * (Strophe.Request) req - The request that has data ready. + */ + _dataRecv: function (req) + { + try { + var elem = req.getResponse(); + } catch (e) { + if (e != "parsererror") throw e; + + this.connect_callback(Strophe.Status.DISCONNECTING, + "strophe-parsererror"); + this.disconnect(); + } + if (elem === null) return; + + // handle graceful disconnect + if (this.disconnecting && this._requests.length == 0) { + this.deleteTimedHandler(this._disconnectTimeout); + this._disconnectTimeout = null; + this._doDisconnect(); + } + + this.rawInput(Strophe.serialize(elem)); + + var typ = elem.getAttribute("type"); + var cond, conflict; + if (typ !== null && typ == "terminate") { + // an error occurred + cond = elem.getAttribute("condition"); + conflict = elem.getElementsByTagName("conflict"); + if (cond !== null) { + if (cond == "remote-stream-error" && conflict.length > 0) { + cond = "conflict"; + } + this.connect_callback(Strophe.Status.CONNFAIL, cond); + } else { + this.connect_callback(Strophe.Status.CONNFAIL, "unknown"); + } + this.connect_callback(Strophe.Status.DISCONNECTING, null); + this.disconnect(); + return; + } + + // remove handlers scheduled for deletion + var i, hand; + while (this.removeHandlers.length > 0) { + hand = this.removeHandlers.pop(); + i = this.handlers.indexOf(hand); + if (i >= 0) + this.handlers.splice(i, 1); + } + + // add handlers scheduled for addition + while (this.addHandlers.length > 0) { + this.handlers.push(this.addHandlers.pop()); + } + + // send each incoming stanza through the handler chain + var self = this; + Strophe.forEachChild(elem, null, function (child) { + var i, newList; + // process handlers + newList = self.handlers; + self.handlers = []; + for (i = 0; i < newList.length; i++) { + var hand = newList[i]; + if (hand.isMatch(child) && + (self.authenticated || !hand.user)) { + if (hand.run(child)) { + self.handlers.push(hand); + } + } else { + self.handlers.push(hand); + } + } + }); + }, + + /** PrivateFunction: _sendTerminate + * _Private_ function to send initial disconnect sequence. + * + * This is the first step in a graceful disconnect. It sends + * the BOSH server a terminate body and includes an unavailable + * presence if authentication has completed. + */ + _sendTerminate: function () + { + Strophe.info("_sendTerminate was called"); + var body = this._buildBody().attrs({type: "terminate"}); + + var presence, i; + if (this.authenticated) { + body.c('presence', { + xmlns: Strophe.NS.CLIENT, + type: 'unavailable' + }); + } + + this.disconnecting = true; + + var req = new Strophe.Request(body.toString(), + this._onRequestStateChange.bind(this) + .prependArg(this._dataRecv.bind(this)), + body.tree().getAttribute("rid")); + + // abort and clear all waiting requests + var r; + while (this._requests.length > 0) { + r = this._requests.pop(); + r.xhr.abort(); + r.abort = true; + } + + this._requests.push(req); + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _connect_cb + * _Private_ handler for initial connection request. + * + * This handler is used to process the initial connection request + * response from the BOSH server. It is used to set up authentication + * handlers and start the authentication process. + * + * SASL authentication will be attempted if available, otherwise + * the code will fall back to legacy authentication. + * + * Parameters: + * (Strophe.Request) req - The current request. + */ + _connect_cb: function (req) + { + Strophe.info("_connect_cb was called"); + + this.connected = true; + var bodyWrap = req.getResponse(); + if (!bodyWrap) return; + + this.rawInput(Strophe.serialize(bodyWrap)); + + var typ = bodyWrap.getAttribute("type"); + var cond, conflict; + if (typ !== null && typ == "terminate") { + // an error occurred + cond = bodyWrap.getAttribute("condition"); + conflict = bodyWrap.getElementsByTagName("conflict"); + if (cond !== null) { + if (cond == "remote-stream-error" && conflict.length > 0) { + cond = "conflict"; + } + this.connect_callback(Strophe.Status.CONNFAIL, cond); + } else { + this.connect_callback(Strophe.Status.CONNFAIL, "unknown"); + } + return; + } + + this.sid = bodyWrap.getAttribute("sid"); + this.stream_id = bodyWrap.getAttribute("authid"); + + // TODO - add SASL anonymous for guest accounts + var do_sasl_plain = false; + var do_sasl_digest_md5 = false; + var do_sasl_anonymous = false; + + var mechanisms = bodyWrap.getElementsByTagName("mechanism"); + var i, mech, auth_str, hashed_auth_str; + if (mechanisms.length > 0) { + for (i = 0; i < mechanisms.length; i++) { + mech = Strophe.getText(mechanisms[i]); + if (mech == 'DIGEST-MD5') { + do_sasl_digest_md5 = true; + } else if (mech == 'PLAIN') { + do_sasl_plain = true; + } else if (mech == 'ANONYMOUS') { + do_sasl_anonymous = true; + } + } + } + + if (Strophe.getNodeFromJid(this.jid) === null && + do_sasl_anonymous) { + this.connect_callback(Strophe.Status.AUTHENTICATING, null); + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "ANONYMOUS" + }).tree()); + } else if (Strophe.getNodeFromJid(this.jid) === null) { + // we don't have a node, which is required for non-anonymous + // client connections + this.connect_callback(Strophe.Status.CONNFAIL, null); + this.disconnect(); + } else if (do_sasl_digest_md5) { + this.connect_callback(Strophe.Status.AUTHENTICATING, null); + this._sasl_challenge_handler = this._addSysHandler( + this._sasl_challenge1_cb.bind(this), null, + "challenge", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "DIGEST-MD5" + }).tree()); + } else if (do_sasl_plain) { + // Build the plain auth string (barejid null + // username null password) and base 64 encoded. + auth_str = Strophe.escapeJid( + Strophe.getBareJidFromJid(this.jid)); + auth_str = auth_str + "\u0000"; + auth_str = auth_str + Strophe.getNodeFromJid(this.jid); + auth_str = auth_str + "\u0000"; + auth_str = auth_str + this.pass; + + this.connect_callback(Strophe.Status.AUTHENTICATING, null); + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + hashed_auth_str = encode64(auth_str); + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "PLAIN" + }).t(hashed_auth_str).tree()); + } else { + this.connect_callback(Strophe.Status.AUTHENTICATING, null); + this._addSysHandler(this._auth1_cb.bind(this), null, null, + null, "_auth_1"); + + this.send($iq({ + type: "get", + to: this.domain, + id: "_auth_1" + }).c("query", { + xmlns: Strophe.NS.AUTH + }).c("username", {}).t(Strophe.getNodeFromJid(this.jid)).tree()); + } + }, + + /** PrivateFunction: _sasl_challenge1_cb + * _Private_ handler for DIGEST-MD5 SASL authentication. + * + * Parameters: + * (XMLElement) elem - The challenge stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_challenge1_cb: function (elem) + { + var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/; + + var challenge = decode64(Strophe.getText(elem)); + var cnonce = hex_md5(Math.random() * 1234567890); + var realm = ""; + var host = null; + var nonce = ""; + var qop = ""; + var matches; + + // remove unneeded handlers + this.deleteHandler(this._sasl_failure_handler); + + while (challenge.match(attribMatch)) { + matches = challenge.match(attribMatch); + challenge = challenge.replace(matches[0], ""); + matches[2] = matches[2].replace(/^"(.+)"$/, "$1"); + switch (matches[1]) { + case "realm": + realm = matches[2]; + break; + case "nonce": + nonce = matches[2]; + break; + case "qop": + qop = matches[2]; + break; + case "host": + host = matches[2]; + break; + } + } + + var digest_uri = "xmpp/" + realm; + if (host !== null) { + digest_uri = digest_uri + "/" + host; + } + + var A1 = str_md5(Strophe.getNodeFromJid(this.jid) + + ":" + realm + ":" + this.pass) + + ":" + nonce + ":" + cnonce; + var A2 = 'AUTHENTICATE:' + digest_uri; + + var responseText = ""; + responseText += 'username="' + + Strophe.getNodeFromJid(this.jid) + '",'; + responseText += 'realm="' + realm + '",'; + responseText += 'nonce="' + nonce + '",'; + responseText += 'cnonce="' + cnonce + '",'; + responseText += 'nc="00000001",'; + responseText += 'qop="auth",'; + responseText += 'digest-uri="' + digest_uri + '",'; + responseText += 'response="' + hex_md5(hex_md5(A1) + ":" + + nonce + ":00000001:" + + cnonce + ":auth:" + + hex_md5(A2)) + '",'; + responseText += 'charset="utf-8"'; + + this._sasl_challenge_handler = this._addSysHandler( + this._sasl_challenge2_cb.bind(this), null, + "challenge", null, null); + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build('response', { + xmlns: Strophe.NS.SASL + }).t(encode64(responseText)).tree()); + + return false; + }, + + /** PrivateFunction: _sasl_challenge2_cb + * _Private_ handler for second step of DIGEST-MD5 SASL authentication. + * + * Parameters: + * (XMLElement) elem - The challenge stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_challenge2_cb: function (elem) + { + // remove unneeded handlers + this.deleteHandler(this._sasl_success_handler); + this.deleteHandler(this._sasl_failure_handler); + + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + this.send($build('response', {xmlns: Strophe.NS.SASL}).tree()); + return false; + }, + + /** PrivateFunction: _auth1_cb + * _Private_ handler for legacy authentication. + * + * This handler is called in response to the initial + * for legacy authentication. It builds an authentication and + * sends it, creating a handler (calling back to _auth2_cb()) to + * handle the result + * + * Parameters: + * (XMLElement) elem - The stanza that triggered the callback. + * + * Returns: + * false to remove the handler. + */ + _auth1_cb: function (elem) + { + var use_digest = false; + var check_query, check_digest; + + if (elem.getAttribute("type") == "result") { + // Find digest + check_query = elem.childNodes[0]; + if (check_query) { + check_digest = check_query.getElementsByTagName("digest")[0]; + if (check_digest) { + use_digest = true; + } + } + } + + // Use digest or plaintext depending on the server features + var iq = $iq({type: "set", id: "_auth_2"}) + .c('query', {xmlns: Strophe.NS.AUTH}) + .c('username', {}).t(Strophe.getNodeFromJid(this.jid)); + if (use_digest) { + iq.up().c("digest", {}) + .t(hex_sha1(this.stream_id + this.pass)); + } else { + iq.up().c('password', {}).t(this.pass); + } + if (!Strophe.getResourceFromJid(this.jid)) { + // since the user has not supplied a resource, we pick + // a default one here. unlike other auth methods, the server + // cannot do this for us. + this.jid = Strophe.getBareJidFromJid(this.jid) + '/strophe'; + } + iq.up().c('resource', {}).t(Strophe.getResourceFromJid(this.jid)); + + this._addSysHandler(this._auth2_cb.bind(this), null, + null, null, "_auth_2"); + + this.send(iq.tree()); + + return false; + }, + + /** PrivateFunction: _sasl_success_cb + * _Private_ handler for succesful SASL authentication. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_success_cb: function (elem) + { + Strophe.info("SASL authentication succeeded."); + + // remove old handlers + this.deleteHandler(this._sasl_failure_handler); + this._sasl_failure_handler = null; + if (this._sasl_challenge_handler) { + this.deleteHandler(this._sasl_challenge_handler); + this._sasl_challenge_handler = null; + } + + this._addSysHandler(this._sasl_auth1_cb.bind(this), null, + "stream:features", null, null); + + // we must send an xmpp:restart now + this._sendRestart(); + + return false; + }, + + /** PrivateFunction: _sasl_auth1_cb + * _Private_ handler to start stream binding. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_auth1_cb: function (elem) + { + var i, child; + + for (i = 0; i < elem.childNodes.length; i++) { + child = elem.childNodes[i]; + if (child.nodeName == 'bind') { + this.do_bind = true; + } + + if (child.nodeName == 'session') { + this.do_session = true; + } + } + + if (!this.do_bind) { + this.connect_callback(Strophe.Status.AUTHFAIL, null); + return false; + } else { + this._addSysHandler(this._sasl_bind_cb.bind(this), null, null, + null, "_bind_auth_2"); + + var resource = Strophe.getResourceFromJid(this.jid); + if (resource) + this.send($iq({type: "set", id: "_bind_auth_2"}) + .c('bind', {xmlns: Strophe.NS.BIND}) + .c('resource', {}).t(resource).tree()); + else + this.send($iq({type: "set", id: "_bind_auth_2"}) + .c('bind', {xmlns: Strophe.NS.BIND}) + .tree()); + } + + return false; + }, + + /** PrivateFunction: _sasl_bind_cb + * _Private_ handler for binding result and session start. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_bind_cb: function (elem) + { + if (elem.getAttribute("type") == "error") { + Strophe.info("SASL binding failed."); + this.connect_callback(Strophe.Status.AUTHFAIL, null); + return false; + } + + // TODO - need to grab errors + var bind = elem.getElementsByTagName("bind"); + var jidNode; + if (bind.length > 0) { + // Grab jid + jidNode = bind[0].getElementsByTagName("jid"); + if (jidNode.length > 0) { + this.jid = Strophe.getText(jidNode[0]); + + if (this.do_session) { + this._addSysHandler(this._sasl_session_cb.bind(this), + null, null, null, "_session_auth_2"); + + this.send($iq({type: "set", id: "_session_auth_2"}) + .c('session', {xmlns: Strophe.NS.SESSION}) + .tree()); + } + } + } else { + Strophe.info("SASL binding failed."); + this.connect_callback(Strophe.Status.AUTHFAIL, null); + return false; + } + }, + + /** PrivateFunction: _sasl_session_cb + * _Private_ handler to finish successful SASL connection. + * + * This sets Connection.authenticated to true on success, which + * starts the processing of user handlers. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_session_cb: function (elem) + { + if (elem.getAttribute("type") == "result") { + this.authenticated = true; + this.connect_callback(Strophe.Status.CONNECTED, null); + } else if (elem.getAttribute("type") == "error") { + Strophe.info("Session creation failed."); + this.connect_callback(Strophe.Status.AUTHFAIL, null); + return false; + } + + return false; + }, + + /** PrivateFunction: _sasl_failure_cb + * _Private_ handler for SASL authentication failure. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_failure_cb: function (elem) + { + // delete unneeded handlers + if (this._sasl_success_handler) { + this.deleteHandler(this._sasl_success_handler); + this._sasl_success_handler = null; + } + if (this._sasl_challenge_handler) { + this.deleteHandler(this._sasl_challenge_handler); + this._sasl_challenge_handler = null; + } + + this.connect_callback(Strophe.Status.AUTHFAIL, null); + return false; + }, + + /** PrivateFunction: _auth2_cb + * _Private_ handler to finish legacy authentication. + * + * This handler is called when the result from the jabber:iq:auth + * stanza is returned. + * + * Parameters: + * (XMLElement) elem - The stanza that triggered the callback. + * + * Returns: + * false to remove the handler. + */ + _auth2_cb: function (elem) + { + if (elem.getAttribute("type") == "result") { + this.authenticated = true; + this.connect_callback(Strophe.Status.CONNECTED, null); + } else if (elem.getAttribute("type") == "error") { + this.connect_callback(Strophe.Status.AUTHFAIL, null); + this.disconnect(); + } + + return false; + }, + + /** PrivateFunction: _addSysTimedHandler + * _Private_ function to add a system level timed handler. + * + * This function is used to add a Strophe.TimedHandler for the + * library code. System timed handlers are allowed to run before + * authentication is complete. + * + * Parameters: + * (Integer) period - The period of the handler. + * (Function) handler - The callback function. + */ + _addSysTimedHandler: function (period, handler) + { + var thand = new Strophe.TimedHandler(period, handler); + thand.user = false; + this.addTimeds.push(thand); + return thand; + }, + + /** PrivateFunction: _addSysHandler + * _Private_ function to add a system level stanza handler. + * + * This function is used to add a Strophe.Handler for the + * library code. System stanza handlers are allowed to run before + * authentication is complete. + * + * Parameters: + * (Function) handler - The callback function. + * (String) ns - The namespace to match. + * (String) name - The stanza name to match. + * (String) type - The stanza type attribute to match. + * (String) id - The stanza id attribute to match. + */ + _addSysHandler: function (handler, ns, name, type, id) + { + var hand = new Strophe.Handler(handler, ns, name, type, id); + hand.user = false; + this.addHandlers.push(hand); + return hand; + }, + + /** PrivateFunction: _onDisconnectTimeout + * _Private_ timeout handler for handling non-graceful disconnection. + * + * If the graceful disconnect process does not complete within the + * time allotted, this handler finishes the disconnect anyway. + * + * Returns: + * false to remove the handler. + */ + _onDisconnectTimeout: function () + { + Strophe.info("_onDisconnectTimeout was called"); + + // cancel all remaining requests and clear the queue + var req; + while (this._requests.length > 0) { + req = this._requests.pop(); + req.xhr.abort(); + req.abort = true; + } + + // actually disconnect + this._doDisconnect(); + + return false; + }, + + /** PrivateFunction: _onIdle + * _Private_ handler to process events during idle cycle. + * + * This handler is called every 100ms to fire timed handlers that + * are ready and keep poll requests going. + */ + _onIdle: function () + { + var i, thand, since, newList; + + // remove timed handlers that have been scheduled for deletion + while (this.removeTimeds.length > 0) { + thand = this.removeTimeds.pop(); + i = this.timedHandlers.indexOf(thand); + if (i >= 0) + this.timedHandlers.splice(i, 1); + } + + // add timed handlers scheduled for addition + while (this.addTimeds.length > 0) { + this.timedHandlers.push(this.addTimeds.pop()); + } + + // call ready timed handlers + var now = new Date().getTime(); + newList = []; + for (i = 0; i < this.timedHandlers.length; i++) { + thand = this.timedHandlers[i]; + if (this.authenticated || !thand.user) { + since = thand.lastCalled + thand.period; + if (since - now <= 0) { + if (thand.run()) { + newList.push(thand); + } + } else { + newList.push(thand); + } + } + } + this.timedHandlers = newList; + + var body, time_elapsed; + + // if no requests are in progress, poll + if (this.authenticated && this._requests.length === 0 && + this._data.length === 0 && !this.disconnecting) { + Strophe.info("no requests during idle cycle, sending " + + "blank request"); + this.send(null); + } else { + if (this._requests.length < 2 && this._data.length > 0 && + !this.paused) { + body = this._buildBody(); + for (i = 0; i < this._data.length; i++) { + if (this._data[i] !== null) { + if (this._data[i] === "restart") { + body.attrs({ + to: this.domain, + "xml:lang": "en", + "xmpp:restart": "true", + "xmlns:xmpp": Strophe.NS.BOSH + }) + } else { + body.cnode(this._data[i]).up(); + } + } + } + delete this._data; + this._data = []; + this._requests.push( + new Strophe.Request(body.toString(), + this._onRequestStateChange.bind(this) + .prependArg(this._dataRecv.bind(this)), + body.tree().getAttribute("rid"))); + this._processRequest(this._requests.length - 1); + } + + if (this._requests.length > 0) { + time_elapsed = this._requests[0].age(); + if (this._requests[0].dead !== null) { + if (this._requests[0].timeDead() > + Strophe.SECONDARY_TIMEOUT) { + this._throttledRequestHandler(); + } + } + + if (time_elapsed > Strophe.TIMEOUT) { + Strophe.warn("Request " + + this._requests[0].id + + " timed out, over " + Strophe.TIMEOUT + + " seconds since last activity"); + this._throttledRequestHandler(); + } + } + } + + // reactivate the timer + clearTimeout(this._idleTimeout); + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + } +};