From 96ffefa78a180d4b1ee4eab98965e423b27acc1d Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 18 Jan 2015 02:33:02 +0100 Subject: [PATCH 01/22] Add AMD support for Strophe. All modules in src/ are now wrapped as AMD modules. Add main.js for usage with require.js. Add example/amd.html to show an example of Strophe.js working with require.js --- examples/amd.html | 21 + examples/basic.js | 4 +- examples/main.js | 59 + main.js | 43 + src/base64.js | 28 +- src/bosh.js | 1518 ++++++------ src/core.js | 6037 ++++++++++++++++++++++----------------------- src/md5.js | 54 +- src/polyfill.js | 77 + src/sha1.js | 307 ++- src/websocket.js | 965 ++++---- src/wrapper.js | 7 + 12 files changed, 4650 insertions(+), 4470 deletions(-) create mode 100644 examples/amd.html create mode 100644 examples/main.js create mode 100644 main.js create mode 100644 src/polyfill.js create mode 100644 src/wrapper.js diff --git a/examples/amd.html b/examples/amd.html new file mode 100644 index 00000000..f7c08fbe --- /dev/null +++ b/examples/amd.html @@ -0,0 +1,21 @@ + + + + Strophe.js AMD Example + + + + +
+
+ + + + + +
+
+
+
+ + diff --git a/examples/basic.js b/examples/basic.js index a128e3d7..6b0d1dd3 100644 --- a/examples/basic.js +++ b/examples/basic.js @@ -1,4 +1,4 @@ -var BOSH_SERVICE = 'http://bosh.metajack.im:5280/xmpp-httpbind' +var BOSH_SERVICE = 'http://bosh.metajack.im:5280/xmpp-httpbind'; var connection = null; function log(msg) @@ -52,4 +52,4 @@ $(document).ready(function () { connection.disconnect(); } }); -}); \ No newline at end of file +}); diff --git a/examples/main.js b/examples/main.js new file mode 100644 index 00000000..233874c5 --- /dev/null +++ b/examples/main.js @@ -0,0 +1,59 @@ +config.baseUrl = '../'; +require.config(config); +if (typeof(require) === 'function') { + require(["jquery", "strophe-full", ], function($, wrapper) { + Strophe = wrapper.Strophe; + + var BOSH_SERVICE = 'http://bosh.metajack.im:5280/xmpp-httpbind'; + var connection = null; + + function log(msg) { + $('#log').append('
').append(document.createTextNode(msg)); + } + + function rawInput(data) { + log('RECV: ' + data); + } + + function rawOutput(data) { + log('SENT: ' + data); + } + + function onConnect(status) { + if (status == Strophe.Status.CONNECTING) { + log('Strophe is connecting.'); + } else if (status == Strophe.Status.CONNFAIL) { + log('Strophe failed to connect.'); + $('#connect').get(0).value = 'connect'; + } else if (status == Strophe.Status.DISCONNECTING) { + log('Strophe is disconnecting.'); + } else if (status == Strophe.Status.DISCONNECTED) { + log('Strophe is disconnected.'); + $('#connect').get(0).value = 'connect'; + } else if (status == Strophe.Status.CONNECTED) { + log('Strophe is connected.'); + connection.disconnect(); + } + } + + $(document).ready(function () { + connection = new Strophe.Connection(BOSH_SERVICE); + connection.rawInput = rawInput; + connection.rawOutput = rawOutput; + $('#connect').bind('click', function () { + var button = $('#connect').get(0); + if (button.value == 'connect') { + button.value = 'disconnect'; + connection.connect( + $('#jid').get(0).value, + $('#pass').get(0).value, + onConnect + ); + } else { + button.value = 'connect'; + connection.disconnect(); + } + }); + }); + }); +} diff --git a/main.js b/main.js new file mode 100644 index 00000000..3afb9f88 --- /dev/null +++ b/main.js @@ -0,0 +1,43 @@ +var config; +if (typeof(require) === 'undefined') { + /* XXX: Hack to work around r.js's stupid parsing. + * We want to save the configuration in a variable so that we can reuse it in + * tests/main.js. + */ + require = { + config: function (c) { + config = c; + } + }; +} + +require.config({ + baseUrl: '.', + paths: { + // Strophe.js src files + "strophe-base64": "src/base64", + "strophe-bosh": "src/bosh", + "strophe-core": "src/core", + "strophe-full": "src/wrapper", + "strophe-md5": "src/md5", + "strophe-sha1": "src/sha1", + "strophe-websocket": "src/websocket", + "strophe-polyfill": "src/polyfill", + + // Examples + "basic": "examples/basic", + + // Tests + "jquery": "bower_components/jquery/dist/jquery", + "sinon": "bower_components/sinon/index", + "sinon-qunit": "bower_components/sinon-qunit/lib/sinon-qunit", + "strophe": "src/strophe", + "tests": "tests/tests" + } +}); + +if (typeof(require) === 'function') { + require(["strophe-full"], function(Strophe) { + window.Strophe = Strophe; + }); +} diff --git a/src/base64.js b/src/base64.js index 39272b06..31980b55 100644 --- a/src/base64.js +++ b/src/base64.js @@ -2,14 +2,23 @@ // public domain. It would be nice if you left this header intact. // Base64 code from Tyler Akins -- http://rumkin.com -var Base64 = (function () { +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(function () { + return factory(); + }); + } else { + // Browser globals + root.Base64 = factory(); + } +}(this, function () { var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; - var obj = { /** - * Encodes a string in base64 - * @param {String} input The string to encode in base64. - */ + * Encodes a string in base64 + * @param {String} input The string to encode in base64. + */ encode: function (input) { var output = ""; var chr1, chr2, chr3; @@ -41,9 +50,9 @@ var Base64 = (function () { }, /** - * Decodes a base64 string. - * @param {String} input The string to decode. - */ + * Decodes a base64 string. + * @param {String} input The string to decode. + */ decode: function (input) { var output = ""; var chr1, chr2, chr3; @@ -76,6 +85,5 @@ var Base64 = (function () { return output; } }; - return obj; -})(); +})); diff --git a/src/bosh.js b/src/bosh.js index 510e73bd..59197bab 100644 --- a/src/bosh.js +++ b/src/bosh.js @@ -6,844 +6,858 @@ */ /* jshint undef: true, unused: true:, noarg: true, latedef: true */ -/*global window, setTimeout, clearTimeout, - XMLHttpRequest, ActiveXObject, - Strophe, $build */ - - -/** 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: - * (XMLElement) elem - The XML 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 (elem, func, rid, sends) -{ - this.id = ++Strophe._requestId; - this.xmlData = elem; - this.data = Strophe.serialize(elem); - // 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(); -}; +/* global define, window, setTimeout, clearTimeout, XMLHttpRequest, ActiveXObject, Strophe, $build */ + +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['strophe-core'], function (core) { + return factory( + core.Strophe, + core.$build + ); + }); + } else { + // Browser globals + return factory(Strophe, $build); + } +}(this, function (Strophe, $build) { -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. + /** PrivateClass: Strophe.Request + * _Private_ helper class that provides a cross implementation abstraction + * for a BOSH related XMLHttpRequest. * - * Throws: - * "parsererror" - A parser error occured. + * 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. * - * Returns: - * The DOM element tree of the response. + * Parameters: + * (XMLElement) elem - The XML 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. */ - getResponse: function () + Strophe.Request = function (elem, func, rid, sends) { - var node = null; - if (this.xhr.responseXML && this.xhr.responseXML.documentElement) { - node = this.xhr.responseXML.documentElement; - if (node.tagName == "parsererror") { + this.id = ++Strophe._requestId; + this.xmlData = elem; + this.data = Strophe.serialize(elem); + // 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)); - 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; charset=utf-8"); + 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; charset=utf-8"); + } + } else if (window.ActiveXObject) { + xhr = new ActiveXObject("Microsoft.XMLHTTP"); } - } else if (window.ActiveXObject) { - xhr = new ActiveXObject("Microsoft.XMLHTTP"); - } - // use Function.bind() to prepend ourselves as an argument - xhr.onreadystatechange = this.func.bind(null, this); + // use Function.bind() to prepend ourselves as an argument + xhr.onreadystatechange = this.func.bind(null, this); - return xhr; - } -}; - -/** Class: Strophe.Bosh - * _Private_ helper class that handles BOSH Connections - * - * The Strophe.Bosh class is used internally by Strophe.Connection - * to encapsulate BOSH sessions. It is not meant to be used from user's code. - */ - -/** File: bosh.js - * A JavaScript library to enable BOSH in Strophejs. - * - * 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. - */ - -/** PrivateConstructor: Strophe.Bosh - * Create and initialize a Strophe.Bosh object. - * - * Parameters: - * (Strophe.Connection) connection - The Strophe.Connection that will use BOSH. - * - * Returns: - * A new Strophe.Bosh object. - */ -Strophe.Bosh = function(connection) { - this._conn = connection; - /* request id for body tags */ - this.rid = Math.floor(Math.random() * 4294967295); - /* The current session ID. */ - this.sid = null; - - // default BOSH values - this.hold = 1; - this.wait = 60; - this.window = 5; - this.errors = 0; - - this._requests = []; -}; - -Strophe.Bosh.prototype = { - /** Variable: strip - * - * BOSH-Connections will have all stanzas wrapped in a tag when - * passed to or . - * To strip this tag, User code can set to "body": - * - * > Strophe.Bosh.prototype.strip = "body"; + return xhr; + } + }; + + /** Class: Strophe.Bosh + * _Private_ helper class that handles BOSH Connections * - * This will enable stripping of the body tag in both - * and . + * The Strophe.Bosh class is used internally by Strophe.Connection + * to encapsulate BOSH sessions. It is not meant to be used from user's code. */ - strip: null, - /** PrivateFunction: _buildBody - * _Private_ helper function to generate the wrapper for BOSH. + /** File: bosh.js + * A JavaScript library to enable BOSH in Strophejs. * - * Returns: - * A Strophe.Builder with a element. + * 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. */ - _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: _reset - * Reset the connection. + /** PrivateConstructor: Strophe.Bosh + * Create and initialize a Strophe.Bosh object. + * + * Parameters: + * (Strophe.Connection) connection - The Strophe.Connection that will use BOSH. * - * This function is called by the reset function of the Strophe Connection + * Returns: + * A new Strophe.Bosh object. */ - _reset: function () - { + Strophe.Bosh = function(connection) { + this._conn = connection; + /* request id for body tags */ this.rid = Math.floor(Math.random() * 4294967295); + /* The current session ID. */ this.sid = null; - this.errors = 0; - }, - /** PrivateFunction: _connect - * _Private_ function that initializes the BOSH connection. - * - * Creates and sends the Request that initializes the BOSH connection. - */ - _connect: function (wait, hold, route) - { - this.wait = wait || this.wait; - this.hold = hold || this.hold; + // default BOSH values + this.hold = 1; + this.wait = 60; + this.window = 5; this.errors = 0; - // build the body tag - var body = this._buildBody().attrs({ - to: this._conn.domain, - "xml:lang": "en", - wait: this.wait, - hold: this.hold, - content: "text/xml; charset=utf-8", - ver: "1.6", - "xmpp:version": "1.0", - "xmlns:xmpp": Strophe.NS.BOSH - }); + this._requests = []; + }; - if(route){ - body.attrs({ - route: route + Strophe.Bosh.prototype = { + /** Variable: strip + * + * BOSH-Connections will have all stanzas wrapped in a tag when + * passed to or . + * To strip this tag, User code can set to "body": + * + * > Strophe.Bosh.prototype.strip = "body"; + * + * This will enable stripping of the body tag in both + * and . + */ + strip: null, + + /** 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 }); - } - - var _connect_cb = this._conn._connect_cb; - this._requests.push( - new Strophe.Request(body.tree(), - this._onRequestStateChange.bind( - this, _connect_cb.bind(this._conn)), - body.tree().getAttribute("rid"))); - this._throttledRequestHandler(); - }, - - /** PrivateFunction: _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. - * (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. - */ - _attach: function (jid, sid, rid, callback, wait, hold, wind) - { - this._conn.jid = jid; - this.sid = sid; - this.rid = rid; - - this._conn.connect_callback = callback; - - this._conn.domain = Strophe.getDomainFromJid(this._conn.jid); - - this._conn.authenticated = true; - this._conn.connected = true; - - this.wait = wait || this.wait; - this.hold = hold || this.hold; - this.window = wind || this.window; - - this._conn._changeConnectStatus(Strophe.Status.ATTACHED, null); - }, - - /** PrivateFunction: _connect_cb - * _Private_ handler for initial connection request. - * - * This handler is used to process the Bosh-part of the initial request. - * Parameters: - * (Strophe.Request) bodyWrap - The received stanza. - */ - _connect_cb: function (bodyWrap) - { - var typ = bodyWrap.getAttribute("type"); - var cond, conflict; - if (typ !== null && typ == "terminate") { - // an error occurred - Strophe.error("BOSH-Connection failed: " + cond); - cond = bodyWrap.getAttribute("condition"); - conflict = bodyWrap.getElementsByTagName("conflict"); - if (cond !== null) { - if (cond == "remote-stream-error" && conflict.length > 0) { - cond = "conflict"; - } - this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, cond); - } else { - this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); + if (this.sid !== null) { + bodyWrap.attrs({sid: this.sid}); } - this._conn._doDisconnect(); - return Strophe.Status.CONNFAIL; - } - // check to make sure we don't overwrite these if _connect_cb is - // called multiple times in the case of missing stream:features - if (!this.sid) { - this.sid = bodyWrap.getAttribute("sid"); - } - var wind = bodyWrap.getAttribute('requests'); - if (wind) { this.window = parseInt(wind, 10); } - var hold = bodyWrap.getAttribute('hold'); - if (hold) { this.hold = parseInt(hold, 10); } - var wait = bodyWrap.getAttribute('wait'); - if (wait) { this.wait = parseInt(wait, 10); } - }, - - /** PrivateFunction: _disconnect - * _Private_ part of Connection.disconnect for Bosh - * - * Parameters: - * (Request) pres - This stanza will be sent before disconnecting. - */ - _disconnect: function (pres) - { - this._sendTerminate(pres); - }, + return bodyWrap; + }, + + /** PrivateFunction: _reset + * Reset the connection. + * + * This function is called by the reset function of the Strophe Connection + */ + _reset: function () + { + this.rid = Math.floor(Math.random() * 4294967295); + this.sid = null; + this.errors = 0; + }, + + /** PrivateFunction: _connect + * _Private_ function that initializes the BOSH connection. + * + * Creates and sends the Request that initializes the BOSH connection. + */ + _connect: function (wait, hold, route) + { + this.wait = wait || this.wait; + this.hold = hold || this.hold; + this.errors = 0; + + // build the body tag + var body = this._buildBody().attrs({ + to: this._conn.domain, + "xml:lang": "en", + wait: this.wait, + hold: this.hold, + content: "text/xml; charset=utf-8", + ver: "1.6", + "xmpp:version": "1.0", + "xmlns:xmpp": Strophe.NS.BOSH + }); - /** PrivateFunction: _doDisconnect - * _Private_ function to disconnect. - * - * Resets the SID and RID. - */ - _doDisconnect: function () - { - this.sid = null; - this.rid = Math.floor(Math.random() * 4294967295); - }, + if(route){ + body.attrs({ + route: route + }); + } - /** PrivateFunction: _emptyQueue - * _Private_ function to check if the Request queue is empty. - * - * Returns: - * True, if there are no Requests queued, False otherwise. - */ - _emptyQueue: function () - { - return this._requests.length === 0; - }, + var _connect_cb = this._conn._connect_cb; - /** 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._conn._onDisconnectTimeout(); - } - }, - - /** PrivateFunction: _no_auth_received - * - * Called on stream start/restart when no stream:features - * has been received and sends a blank poll request. - */ - _no_auth_received: function (_callback) - { - if (_callback) { - _callback = _callback.bind(this._conn); - } else { - _callback = this._conn._connect_cb.bind(this._conn); - } - var body = this._buildBody(); - this._requests.push( + this._requests.push( new Strophe.Request(body.tree(), - this._onRequestStateChange.bind( - this, _callback.bind(this._conn)), - body.tree().getAttribute("rid"))); - this._throttledRequestHandler(); - }, - - /** PrivateFunction: _onDisconnectTimeout - * _Private_ timeout handler for handling non-graceful disconnection. - * - * Cancels all remaining Requests and clears the queue. - */ - _onDisconnectTimeout: function () - { - var req; - while (this._requests.length > 0) { - req = this._requests.pop(); - req.abort = true; - req.xhr.abort(); - // jslint complains, but this is fine. setting to empty func - // is necessary for IE6 - req.xhr.onreadystatechange = function () {}; // jshint ignore:line - } - }, - - /** PrivateFunction: _onIdle - * _Private_ handler called by Strophe.Connection._onIdle - * - * Sends all queued Requests or polls with empty Request if there are none. - */ - _onIdle: function () { - var data = this._conn._data; - - // if no requests are in progress, poll - if (this._conn.authenticated && this._requests.length === 0 && - data.length === 0 && !this._conn.disconnecting) { - Strophe.info("no requests during idle cycle, sending " + - "blank request"); - data.push(null); - } - - if (this._conn.paused) { - return; - } - - if (this._requests.length < 2 && data.length > 0) { - var body = this._buildBody(); - for (var i = 0; i < data.length; i++) { - if (data[i] !== null) { - if (data[i] === "restart") { - body.attrs({ - to: this._conn.domain, - "xml:lang": "en", - "xmpp:restart": "true", - "xmlns:xmpp": Strophe.NS.BOSH - }); - } else { - body.cnode(data[i]).up(); + this._onRequestStateChange.bind( + this, _connect_cb.bind(this._conn)), + body.tree().getAttribute("rid"))); + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _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. + * (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. + */ + _attach: function (jid, sid, rid, callback, wait, hold, wind) + { + this._conn.jid = jid; + this.sid = sid; + this.rid = rid; + + this._conn.connect_callback = callback; + + this._conn.domain = Strophe.getDomainFromJid(this._conn.jid); + + this._conn.authenticated = true; + this._conn.connected = true; + + this.wait = wait || this.wait; + this.hold = hold || this.hold; + this.window = wind || this.window; + + this._conn._changeConnectStatus(Strophe.Status.ATTACHED, null); + }, + + /** PrivateFunction: _connect_cb + * _Private_ handler for initial connection request. + * + * This handler is used to process the Bosh-part of the initial request. + * Parameters: + * (Strophe.Request) bodyWrap - The received stanza. + */ + _connect_cb: function (bodyWrap) + { + var typ = bodyWrap.getAttribute("type"); + var cond, conflict; + if (typ !== null && typ == "terminate") { + // an error occurred + Strophe.error("BOSH-Connection failed: " + cond); + cond = bodyWrap.getAttribute("condition"); + conflict = bodyWrap.getElementsByTagName("conflict"); + if (cond !== null) { + if (cond == "remote-stream-error" && conflict.length > 0) { + cond = "conflict"; } + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, cond); + } else { + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); } + this._conn._doDisconnect(); + return Strophe.Status.CONNFAIL; + } + + // check to make sure we don't overwrite these if _connect_cb is + // called multiple times in the case of missing stream:features + if (!this.sid) { + this.sid = bodyWrap.getAttribute("sid"); + } + var wind = bodyWrap.getAttribute('requests'); + if (wind) { this.window = parseInt(wind, 10); } + var hold = bodyWrap.getAttribute('hold'); + if (hold) { this.hold = parseInt(hold, 10); } + var wait = bodyWrap.getAttribute('wait'); + if (wait) { this.wait = parseInt(wait, 10); } + }, + + /** PrivateFunction: _disconnect + * _Private_ part of Connection.disconnect for Bosh + * + * Parameters: + * (Request) pres - This stanza will be sent before disconnecting. + */ + _disconnect: function (pres) + { + this._sendTerminate(pres); + }, + + /** PrivateFunction: _doDisconnect + * _Private_ function to disconnect. + * + * Resets the SID and RID. + */ + _doDisconnect: function () + { + this.sid = null; + this.rid = Math.floor(Math.random() * 4294967295); + }, + + /** PrivateFunction: _emptyQueue + * _Private_ function to check if the Request queue is empty. + * + * Returns: + * True, if there are no Requests queued, False otherwise. + */ + _emptyQueue: function () + { + return this._requests.length === 0; + }, + + /** 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._conn._onDisconnectTimeout(); + } + }, + + /** PrivateFunction: _no_auth_received + * + * Called on stream start/restart when no stream:features + * has been received and sends a blank poll request. + */ + _no_auth_received: function (_callback) + { + if (_callback) { + _callback = _callback.bind(this._conn); + } else { + _callback = this._conn._connect_cb.bind(this._conn); } - delete this._conn._data; - this._conn._data = []; + var body = this._buildBody(); this._requests.push( - new Strophe.Request(body.tree(), - this._onRequestStateChange.bind( - this, this._conn._dataRecv.bind(this._conn)), - body.tree().getAttribute("rid"))); + new Strophe.Request(body.tree(), + this._onRequestStateChange.bind( + this, _callback.bind(this._conn)), + body.tree().getAttribute("rid"))); this._throttledRequestHandler(); - } + }, + + /** PrivateFunction: _onDisconnectTimeout + * _Private_ timeout handler for handling non-graceful disconnection. + * + * Cancels all remaining Requests and clears the queue. + */ + _onDisconnectTimeout: function () + { + var req; + while (this._requests.length > 0) { + req = this._requests.pop(); + req.abort = true; + req.xhr.abort(); + // jslint complains, but this is fine. setting to empty func + // is necessary for IE6 + req.xhr.onreadystatechange = function () {}; // jshint ignore:line + } + }, + + /** PrivateFunction: _onIdle + * _Private_ handler called by Strophe.Connection._onIdle + * + * Sends all queued Requests or polls with empty Request if there are none. + */ + _onIdle: function () { + var data = this._conn._data; + + // if no requests are in progress, poll + if (this._conn.authenticated && this._requests.length === 0 && + data.length === 0 && !this._conn.disconnecting) { + Strophe.info("no requests during idle cycle, sending " + + "blank request"); + data.push(null); + } - if (this._requests.length > 0) { - var time_elapsed = this._requests[0].age(); - if (this._requests[0].dead !== null) { - if (this._requests[0].timeDead() > - Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)) { - this._throttledRequestHandler(); - } + if (this._conn.paused) { + return; } - if (time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)) { - Strophe.warn("Request " + - this._requests[0].id + - " timed out, over " + Math.floor(Strophe.TIMEOUT * this.wait) + - " seconds since last activity"); + if (this._requests.length < 2 && data.length > 0) { + var body = this._buildBody(); + for (var i = 0; i < data.length; i++) { + if (data[i] !== null) { + if (data[i] === "restart") { + body.attrs({ + to: this._conn.domain, + "xml:lang": "en", + "xmpp:restart": "true", + "xmlns:xmpp": Strophe.NS.BOSH + }); + } else { + body.cnode(data[i]).up(); + } + } + } + delete this._conn._data; + this._conn._data = []; + this._requests.push( + new Strophe.Request(body.tree(), + this._onRequestStateChange.bind( + this, this._conn._dataRecv.bind(this._conn)), + body.tree().getAttribute("rid"))); this._throttledRequestHandler(); } - } - }, - /** 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 (this._requests.length > 0) { + var time_elapsed = this._requests[0].age(); + if (this._requests[0].dead !== null) { + if (this._requests[0].timeDead() > + Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)) { + this._throttledRequestHandler(); + } + } - if (req.abort) { - req.abort = false; - return; - } + if (time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)) { + Strophe.warn("Request " + + this._requests[0].id + + " timed out, over " + Math.floor(Strophe.TIMEOUT * this.wait) + + " seconds since last activity"); + this._throttledRequestHandler(); + } + } + }, + + /** 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); - // 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 (req.abort) { + req.abort = false; + return; } - if (typeof(reqStatus) == "undefined") { + // 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 (this.disconnecting) { - if (reqStatus >= 400) { - this._hitError(reqStatus); - return; + if (typeof(reqStatus) == "undefined") { + reqStatus = 0; } - } - var reqIs0 = (this._requests[0] == req); - var reqIs1 = (this._requests[1] == req); + if (this.disconnecting) { + if (reqStatus >= 400) { + this._hitError(reqStatus); + return; + } + } - 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"); - } + var reqIs0 = (this._requests[0] == req); + var reqIs1 = (this._requests[1] == req); - // 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() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait))) { - this._restartRequest(0); + 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"); } - // 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._conn._changeConnectStatus(Strophe.Status.DISCONNECTING, - null); - this._conn._doDisconnect(); + + // 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() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait))) { + 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._conn._changeConnectStatus(Strophe.Status.DISCONNECTING, + null); + this._conn._doDisconnect(); + } } } - } - if (!((reqStatus > 0 && reqStatus < 500) || - req.sends > 5)) { - this._throttledRequestHandler(); + if (!((reqStatus > 0 && reqStatus < 500) || + req.sends > 5)) { + this._throttledRequestHandler(); + } } - } - }, + }, + + /** 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 self = this; + var req = this._requests[i]; + var reqStatus = -1; - /** 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 self = this; - var req = this._requests[i]; - var reqStatus = -1; - - try { - if (req.xhr.readyState == 4) { - reqStatus = req.xhr.status; + try { + if (req.xhr.readyState == 4) { + reqStatus = req.xhr.status; + } + } catch (e) { + Strophe.error("caught an error in _requests[" + i + + "], reqStatus: " + reqStatus); } - } catch (e) { - Strophe.error("caught an error in _requests[" + i + - "], reqStatus: " + reqStatus); - } - - if (typeof(reqStatus) == "undefined") { - reqStatus = -1; - } - - // make sure we limit the number of retries - if (req.sends > this._conn.maxRetries) { - this._conn._onDisconnectTimeout(); - return; - } - var time_elapsed = req.age(); - var primaryTimeout = (!isNaN(time_elapsed) && - time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)); - var secondaryTimeout = (req.dead !== null && - req.timeDead() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)); - var requestCompletedWithServerError = (req.xhr.readyState == 4 && - (reqStatus < 1 || - reqStatus >= 500)); - if (primaryTimeout || secondaryTimeout || - requestCompletedWithServerError) { - if (secondaryTimeout) { - Strophe.error("Request " + - this._requests[i].id + - " timed out (secondary), restarting"); + if (typeof(reqStatus) == "undefined") { + reqStatus = -1; } - req.abort = true; - req.xhr.abort(); - // setting to null fails on IE6, so set to empty function - req.xhr.onreadystatechange = function () {}; - this._requests[i] = new Strophe.Request(req.xmlData, - req.origFunc, - req.rid, - req.sends); - req = this._requests[i]; - } - if (req.xhr.readyState === 0) { - Strophe.debug("request id " + req.id + - "." + req.sends + " posting"); + // make sure we limit the number of retries + if (req.sends > this._conn.maxRetries) { + this._conn._onDisconnectTimeout(); + return; + } - try { - req.xhr.open("POST", this._conn.service, this._conn.options.sync ? false : true); - req.xhr.setRequestHeader("Content-Type", "text/xml; charset=utf-8"); - } catch (e2) { - Strophe.error("XHR open failed."); - if (!this._conn.connected) { - this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, - "bad-service"); + var time_elapsed = req.age(); + var primaryTimeout = (!isNaN(time_elapsed) && + time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)); + var secondaryTimeout = (req.dead !== null && + req.timeDead() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)); + var requestCompletedWithServerError = (req.xhr.readyState == 4 && + (reqStatus < 1 || + reqStatus >= 500)); + if (primaryTimeout || secondaryTimeout || + requestCompletedWithServerError) { + if (secondaryTimeout) { + Strophe.error("Request " + + this._requests[i].id + + " timed out (secondary), restarting"); } - this._conn.disconnect(); - return; + req.abort = true; + req.xhr.abort(); + // setting to null fails on IE6, so set to empty function + req.xhr.onreadystatechange = function () {}; + this._requests[i] = new Strophe.Request(req.xmlData, + req.origFunc, + req.rid, + req.sends); + req = this._requests[i]; } - // Fires the XHR request -- may be invoked immediately - // or on a gradually expanding retry window for reconnects - var sendFunc = function () { - req.date = new Date(); - if (self._conn.options.customHeaders){ - var headers = self._conn.options.customHeaders; - for (var header in headers) { - if (headers.hasOwnProperty(header)) { - req.xhr.setRequestHeader(header, headers[header]); + if (req.xhr.readyState === 0) { + Strophe.debug("request id " + req.id + + "." + req.sends + " posting"); + + try { + req.xhr.open("POST", this._conn.service, this._conn.options.sync ? false : true); + req.xhr.setRequestHeader("Content-Type", "text/xml; charset=utf-8"); + } catch (e2) { + Strophe.error("XHR open failed."); + if (!this._conn.connected) { + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, + "bad-service"); + } + this._conn.disconnect(); + return; + } + + // Fires the XHR request -- may be invoked immediately + // or on a gradually expanding retry window for reconnects + var sendFunc = function () { + req.date = new Date(); + if (self._conn.options.customHeaders){ + var headers = self._conn.options.customHeaders; + for (var header in headers) { + if (headers.hasOwnProperty(header)) { + req.xhr.setRequestHeader(header, headers[header]); + } } } + 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 creates a nicely + // expanding retry window + var backoff = Math.min(Math.floor(Strophe.TIMEOUT * this.wait), + Math.pow(req.sends, 3)) * 1000; + setTimeout(sendFunc, backoff); + } else { + sendFunc(); } - 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 creates a nicely - // expanding retry window - var backoff = Math.min(Math.floor(Strophe.TIMEOUT * this.wait), - Math.pow(req.sends, 3)) * 1000; - setTimeout(sendFunc, backoff); - } else { - sendFunc(); - } - req.sends++; + req.sends++; - if (this._conn.xmlOutput !== Strophe.Connection.prototype.xmlOutput) { - if (req.xmlData.nodeName === this.strip && req.xmlData.childNodes.length) { - this._conn.xmlOutput(req.xmlData.childNodes[0]); - } else { - this._conn.xmlOutput(req.xmlData); + if (this._conn.xmlOutput !== Strophe.Connection.prototype.xmlOutput) { + if (req.xmlData.nodeName === this.strip && req.xmlData.childNodes.length) { + this._conn.xmlOutput(req.xmlData.childNodes[0]); + } else { + this._conn.xmlOutput(req.xmlData); + } + } + if (this._conn.rawOutput !== Strophe.Connection.prototype.rawOutput) { + this._conn.rawOutput(req.data); } + } else { + Strophe.debug("_processRequest: " + + (i === 0 ? "first" : "second") + + " request has readyState of " + + req.xhr.readyState); } - if (this._conn.rawOutput !== Strophe.Connection.prototype.rawOutput) { - this._conn.rawOutput(req.data); + }, + + /** 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); + } } - } else { - Strophe.debug("_processRequest: " + - (i === 0 ? "first" : "second") + - " request has readyState of " + - req.xhr.readyState); - } - }, - /** 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"); + // IE6 fails on setting to null, so set to empty function + req.xhr.onreadystatechange = function () {}; - var i; - for (i = this._requests.length - 1; i >= 0; i--) { - if (req == this._requests[i]) { - this._requests.splice(i, 1); + 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(); } - } - // IE6 fails on setting to null, so set to empty function - 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: _reqToData - * _Private_ function to get a stanza out of a request. - * - * Tries to extract a stanza out of a Request Object. - * When this fails the current connection will be disconnected. - * - * Parameters: - * (Object) req - The Request. - * - * Returns: - * The stanza that was passed. - */ - _reqToData: function (req) - { - try { - return req.getResponse(); - } catch (e) { - if (e != "parsererror") { throw e; } - this._conn.disconnect("strophe-parsererror"); - } - }, - - /** 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 (pres) - { - Strophe.info("_sendTerminate was called"); - var body = this._buildBody().attrs({type: "terminate"}); - - if (pres) { - body.cnode(pres.tree()); - } - - var req = new Strophe.Request(body.tree(), - this._onRequestStateChange.bind( - this, this._conn._dataRecv.bind(this._conn)), - body.tree().getAttribute("rid")); - - this._requests.push(req); - this._throttledRequestHandler(); - }, - - /** PrivateFunction: _send - * _Private_ part of the Connection.send function for BOSH - * - * Just triggers the RequestHandler to send the messages that are in the queue - */ - _send: function () { - clearTimeout(this._conn._idleTimeout); - this._throttledRequestHandler(); - this._conn._idleTimeout = setTimeout(this._conn._onIdle.bind(this._conn), 100); - }, + this._processRequest(i); + }, + + /** PrivateFunction: _reqToData + * _Private_ function to get a stanza out of a request. + * + * Tries to extract a stanza out of a Request Object. + * When this fails the current connection will be disconnected. + * + * Parameters: + * (Object) req - The Request. + * + * Returns: + * The stanza that was passed. + */ + _reqToData: function (req) + { + try { + return req.getResponse(); + } catch (e) { + if (e != "parsererror") { throw e; } + this._conn.disconnect("strophe-parsererror"); + } + }, + + /** 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 (pres) + { + Strophe.info("_sendTerminate was called"); + var body = this._buildBody().attrs({type: "terminate"}); + + if (pres) { + body.cnode(pres.tree()); + } - /** PrivateFunction: _sendRestart - * - * Send an xmpp:restart stanza. - */ - _sendRestart: function () - { - this._throttledRequestHandler(); - clearTimeout(this._conn._idleTimeout); - }, + var req = new Strophe.Request(body.tree(), + this._onRequestStateChange.bind( + this, this._conn._dataRecv.bind(this._conn)), + body.tree().getAttribute("rid")); - /** 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"); - } + this._requests.push(req); + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _send + * _Private_ part of the Connection.send function for BOSH + * + * Just triggers the RequestHandler to send the messages that are in the queue + */ + _send: function () { + clearTimeout(this._conn._idleTimeout); + this._throttledRequestHandler(); + this._conn._idleTimeout = setTimeout(this._conn._onIdle.bind(this._conn), 100); + }, + + /** PrivateFunction: _sendRestart + * + * Send an xmpp:restart stanza. + */ + _sendRestart: function () + { + this._throttledRequestHandler(); + clearTimeout(this._conn._idleTimeout); + }, + + /** 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 || this._requests.length === 0) { + return; + } - if (this._requests.length > 0) { - this._processRequest(0); - } + 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) { - this._processRequest(1); + if (this._requests.length > 1 && + Math.abs(this._requests[0].rid - + this._requests[1].rid) < this.window) { + this._processRequest(1); + } } - } -}; + }; + return Strophe; +})); diff --git a/src/core.js b/src/core.js index 127040a6..f6ba6914 100644 --- a/src/core.js +++ b/src/core.js @@ -4,12 +4,8 @@ Copyright 2006-2008, OGG, LLC */ - /* jshint undef: true, unused: true:, noarg: true, latedef: true */ -/*global document, window, setTimeout, clearTimeout, console, - ActiveXObject, Base64, MD5, DOMParser */ -// from sha1.js -/*global core_hmac_sha1, binb2str, str_hmac_sha1, str_sha1, b64_hmac_sha1*/ +/*global define, document, window, setTimeout, clearTimeout, console, ActiveXObject, DOMParser */ /** File: strophe.js * A JavaScript library for XMPP BOSH/XMPP over Websocket. @@ -24,3318 +20,3255 @@ * For more information on XMPP-over WebSocket see this RFC draft: * http://tools.ietf.org/html/draft-ietf-xmpp-websocket-00 */ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([ + 'strophe-sha1', + 'strophe-base64', + 'strophe-md5', + "strophe-polyfill" + ], function () { + return factory.apply(this, arguments); + }); + } else { + // Browser globals + var o = factory(root.SHA1, root.Base64, root.MD5); + window.Strophe = o.Strophe; + window.$build = o.$build; + window.$iq = o.$iq; + window.$msg = o.$msg; + window.$pres = o.$pres; + } +}(this, function (SHA1, Base64, MD5) { -/** PrivateFunction: Function.prototype.bind - * Bind a function to an instance. This is a polyfill for the ES5 bind method. - * which already exists in more modern browsers, but we provide it to support - * those that don't. - * - * Parameters: - * (Object) obj - The object that will become 'this' in the bound function. - * (Object) argN - An option argument that will be prepended to the - * arguments given for the function call - * - * Returns: - * The bound function. - */ -if (!Function.prototype.bind) { - Function.prototype.bind = function (obj /*, arg1, arg2, ... */) - { - var func = this; - var _slice = Array.prototype.slice; - var _concat = Array.prototype.concat; - var _args = _slice.call(arguments, 1); - - return function () { - return func.apply(obj ? obj : this, - _concat.call(_args, - _slice.call(arguments, 0))); - }; - }; -} - -/** PrivateFunction: Array.isArray - * This is a polyfill for the ES5 Array.isArray method. - */ -if (!Array.isArray) { - Array.isArray = function(arg) { - return Object.prototype.toString.call(arg) === '[object Array]'; - }; -} - -/** 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; - }; -} - -/* All of the Strophe globals are defined in this special function below so - * that references to the globals become closures. This will ensure that - * on page reload, these references will still be available to callbacks - * that are still executing. - */ - -(function (callback) { -var Strophe; - -/** 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 = { - /** Constant: VERSION - * The version of the Strophe library. Unreleased builds will have - * a version of head-HASH where HASH is a partial revision. - */ - VERSION: "@VERSION@", - - /** 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.XHTML_IM - XHTML-IM namespace from XEP 71. - * NS.XHTML - XHTML body namespace from XEP 71. - */ - 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", - STANZAS: "urn:ietf:params:xml:ns:xmpp-stanzas", - XHTML_IM: "http://jabber.org/protocol/xhtml-im", - XHTML: "http://www.w3.org/1999/xhtml" - }, - - - /** Constants: XHTML_IM Namespace - * contains allowed tags, tag attributes, and css properties. - * Used in the createHtml function to filter incoming html into the allowed XHTML-IM subset. - * See http://xmpp.org/extensions/xep-0071.html#profile-summary for the list of recommended - * allowed tags and their attributes. - */ - XHTML: { - tags: ['a','blockquote','br','cite','em','img','li','ol','p','span','strong','ul','body'], - attributes: { - 'a': ['href'], - 'blockquote': ['style'], - 'br': [], - 'cite': ['style'], - 'em': [], - 'img': ['src', 'alt', 'style', 'height', 'width'], - 'li': ['style'], - 'ol': ['style'], - 'p': ['style'], - 'span': ['style'], - 'strong': [], - 'ul': ['style'], - 'body': [] - }, - css: ['background-color','color','font-family','font-size','font-style','font-weight','margin-left','margin-right','text-align','text-decoration'], - validTag: function(tag) - { - for(var i = 0; i < Strophe.XHTML.tags.length; i++) { - if(tag == Strophe.XHTML.tags[i]) { - return true; - } - } - return false; - }, - validAttribute: function(tag, attribute) - { - if(typeof Strophe.XHTML.attributes[tag] !== 'undefined' && Strophe.XHTML.attributes[tag].length > 0) { - for(var i = 0; i < Strophe.XHTML.attributes[tag].length; i++) { - if(attribute == Strophe.XHTML.attributes[tag][i]) { - return true; - } - } - } - return false; - }, - validCSS: function(style) - { - for(var i = 0; i < Strophe.XHTML.css.length; i++) { - if(style == Strophe.XHTML.css[i]) { - return true; - } - } - return false; - } - }, - - /** 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.ATTACHED - The connection has been attached - */ - Status: { - ERROR: 0, - CONNECTING: 1, - CONNFAIL: 2, - AUTHENTICATING: 3, - AUTHFAIL: 4, - CONNECTED: 5, - DISCONNECTED: 6, - DISCONNECTING: 7, - ATTACHED: 8 - }, - - /** 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.FRAGMENT - XHTML fragment element. - */ - ElementType: { - NORMAL: 1, - TEXT: 3, - CDATA: 4, - FRAGMENT: 11 - }, - - /** 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 - Timeout multiplier. A waiting request will be considered - * failed after Math.floor(TIMEOUT * wait) seconds have elapsed. - * This defaults to 1.1, and with default wait, 66 seconds. - * SECONDARY_TIMEOUT - Secondary timeout multiplier. In cases where - * Strophe can detect early failure, it will consider the request - * failed if it doesn't return after - * Math.floor(SECONDARY_TIMEOUT * wait) seconds have elapsed. - * This defaults to 0.1, and with default wait, 6 seconds. - */ - TIMEOUT: 1.1, - SECONDARY_TIMEOUT: 0.1, - - /** Function: addNamespace - * This function is used to extend the current namespaces in - * Strophe.NS. It takes a key and a value with the key being the - * name of the new namespace, with its actual value. - * For example: - * Strophe.addNamespace('PUBSUB', "http://jabber.org/protocol/pubsub"); + /** Function: $build + * Create a Strophe.Builder. + * This is an alias for 'new Strophe.Builder(name, attrs)'. * * Parameters: - * (String) name - The name under which the namespace will be - * referenced under Strophe.NS - * (String) value - The actual namespace. + * (String) name - The root element name. + * (Object) attrs - The attributes for the root element in object notation. + * + * Returns: + * A new Strophe.Builder object. */ - addNamespace: function (name, value) - { - Strophe.NS[name] = value; - }, + function $build(name, attrs) { return new Strophe.Builder(name, attrs); } - /** Function: forEachChild - * Map a function over some or all child elements of a given element. + /** Function: $msg + * Create a Strophe.Builder with a element as the root. * - * 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. + * Parmaeters: + * (Object) attrs - The element attributes in object notation. * - * 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. + * Returns: + * A new Strophe.Builder object. */ - 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 $msg(attrs) { return new Strophe.Builder("message", attrs); } - /** Function: isTagEqual - * Compare an element's tag name with a string. - * - * This function is case insensitive. + /** Function: $iq + * Create a Strophe.Builder with an element as the root. * * Parameters: - * (XMLElement) el - A DOM element. - * (String) name - The element name. + * (Object) attrs - The element attributes in object notation. * * Returns: - * true if the element's tag name matches _el_, and false - * otherwise. - */ - isTagEqual: function (el, name) - { - return el.tagName == name; - }, - - /** PrivateVariable: _xmlGenerator - * _Private_ variable that caches a DOM document to - * generate elements. - */ - _xmlGenerator: null, - - /** PrivateFunction: _makeGenerator - * _Private_ function that creates a dummy XML DOM document to serve as - * an element and text node generator. + * A new Strophe.Builder object. */ - _makeGenerator: function () { - var doc; - - // IE9 does implement createDocument(); however, using it will cause the browser to leak memory on page unload. - // Here, we test for presence of createDocument() plus IE's proprietary documentMode attribute, which would be - // less than 10 in the case of IE9 and below. - if (document.implementation.createDocument === undefined || - document.implementation.createDocument && document.documentMode && document.documentMode < 10) { - doc = this._getIEXmlDom(); - doc.appendChild(doc.createElement('strophe')); - } else { - doc = document.implementation - .createDocument('jabber:client', 'strophe', null); - } + function $iq(attrs) { return new Strophe.Builder("iq", attrs); } - return doc; - }, - - /** Function: xmlGenerator - * Get the DOM document to generate elements. + /** Function: $pres + * Create a Strophe.Builder with a element as the root. + * + * Parameters: + * (Object) attrs - The element attributes in object notation. * * Returns: - * The currently used DOM document. + * A new Strophe.Builder object. */ - xmlGenerator: function () { - if (!Strophe._xmlGenerator) { - Strophe._xmlGenerator = Strophe._makeGenerator(); - } - return Strophe._xmlGenerator; - }, + function $pres(attrs) { return new Strophe.Builder("presence", attrs); } - /** PrivateFunction: _getIEXmlDom - * Gets IE xml doc object + /** Class: Strophe + * An object container for all Strophe library functions. * - * Returns: - * A Microsoft XML DOM Object - * See Also: - * http://msdn.microsoft.com/en-us/library/ms757837%28VS.85%29.aspx + * 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. */ - _getIEXmlDom : function() { - var doc = null; - var docStrings = [ - "Msxml2.DOMDocument.6.0", - "Msxml2.DOMDocument.5.0", - "Msxml2.DOMDocument.4.0", - "MSXML2.DOMDocument.3.0", - "MSXML2.DOMDocument", - "MSXML.DOMDocument", - "Microsoft.XMLDOM" - ]; - - for (var d = 0; d < docStrings.length; d++) { - if (doc === null) { - try { - doc = new ActiveXObject(docStrings[d]); - } catch (e) { - doc = null; + var Strophe = { + /** Constant: VERSION + * The version of the Strophe library. Unreleased builds will have + * a version of head-HASH where HASH is a partial revision. + */ + VERSION: "@VERSION@", + + /** 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.XHTML_IM - XHTML-IM namespace from XEP 71. + * NS.XHTML - XHTML body namespace from XEP 71. + */ + 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", + STANZAS: "urn:ietf:params:xml:ns:xmpp-stanzas", + XHTML_IM: "http://jabber.org/protocol/xhtml-im", + XHTML: "http://www.w3.org/1999/xhtml" + }, + + + /** Constants: XHTML_IM Namespace + * contains allowed tags, tag attributes, and css properties. + * Used in the createHtml function to filter incoming html into the allowed XHTML-IM subset. + * See http://xmpp.org/extensions/xep-0071.html#profile-summary for the list of recommended + * allowed tags and their attributes. + */ + XHTML: { + tags: ['a','blockquote','br','cite','em','img','li','ol','p','span','strong','ul','body'], + attributes: { + 'a': ['href'], + 'blockquote': ['style'], + 'br': [], + 'cite': ['style'], + 'em': [], + 'img': ['src', 'alt', 'style', 'height', 'width'], + 'li': ['style'], + 'ol': ['style'], + 'p': ['style'], + 'span': ['style'], + 'strong': [], + 'ul': ['style'], + 'body': [] + }, + css: ['background-color','color','font-family','font-size','font-style','font-weight','margin-left','margin-right','text-align','text-decoration'], + validTag: function(tag) + { + for(var i = 0; i < Strophe.XHTML.tags.length; i++) { + if(tag == Strophe.XHTML.tags[i]) { + return true; + } + } + return false; + }, + validAttribute: function(tag, attribute) + { + if(typeof Strophe.XHTML.attributes[tag] !== 'undefined' && Strophe.XHTML.attributes[tag].length > 0) { + for(var i = 0; i < Strophe.XHTML.attributes[tag].length; i++) { + if(attribute == Strophe.XHTML.attributes[tag][i]) { + return true; + } + } + } + return false; + }, + validCSS: function(style) + { + for(var i = 0; i < Strophe.XHTML.css.length; i++) { + if(style == Strophe.XHTML.css[i]) { + return true; + } + } + return false; + } + }, + + /** 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.ATTACHED - The connection has been attached + */ + Status: { + ERROR: 0, + CONNECTING: 1, + CONNFAIL: 2, + AUTHENTICATING: 3, + AUTHFAIL: 4, + CONNECTED: 5, + DISCONNECTED: 6, + DISCONNECTING: 7, + ATTACHED: 8 + }, + + /** 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.FRAGMENT - XHTML fragment element. + */ + ElementType: { + NORMAL: 1, + TEXT: 3, + CDATA: 4, + FRAGMENT: 11 + }, + + /** 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 - Timeout multiplier. A waiting request will be considered + * failed after Math.floor(TIMEOUT * wait) seconds have elapsed. + * This defaults to 1.1, and with default wait, 66 seconds. + * SECONDARY_TIMEOUT - Secondary timeout multiplier. In cases where + * Strophe can detect early failure, it will consider the request + * failed if it doesn't return after + * Math.floor(SECONDARY_TIMEOUT * wait) seconds have elapsed. + * This defaults to 0.1, and with default wait, 6 seconds. + */ + TIMEOUT: 1.1, + SECONDARY_TIMEOUT: 0.1, + + /** Function: addNamespace + * This function is used to extend the current namespaces in + * Strophe.NS. It takes a key and a value with the key being the + * name of the new namespace, with its actual value. + * For example: + * Strophe.addNamespace('PUBSUB', "http://jabber.org/protocol/pubsub"); + * + * Parameters: + * (String) name - The name under which the namespace will be + * referenced under Strophe.NS + * (String) value - The actual namespace. + */ + addNamespace: function (name, value) + { + Strophe.NS[name] = value; + }, + + /** 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 == name; + }, + + /** PrivateVariable: _xmlGenerator + * _Private_ variable that caches a DOM document to + * generate elements. + */ + _xmlGenerator: null, + + /** PrivateFunction: _makeGenerator + * _Private_ function that creates a dummy XML DOM document to serve as + * an element and text node generator. + */ + _makeGenerator: function () { + var doc; + + // IE9 does implement createDocument(); however, using it will cause the browser to leak memory on page unload. + // Here, we test for presence of createDocument() plus IE's proprietary documentMode attribute, which would be + // less than 10 in the case of IE9 and below. + if (document.implementation.createDocument === undefined || + document.implementation.createDocument && document.documentMode && document.documentMode < 10) { + doc = this._getIEXmlDom(); + doc.appendChild(doc.createElement('strophe')); } else { - break; + doc = document.implementation + .createDocument('jabber:client', 'strophe', null); } - } - return doc; - }, + return doc; + }, - /** Function: xmlElement - * Create an XML DOM element. - * - * This function creates an XML DOM element correctly across all - * implementations. Note that these are not HTML DOM elements, which - * aren't appropriate for XMPP stanzas. - * - * Parameters: - * (String) name - The name for the element. - * (Array|Object) attrs - An optional array or object containing - * key/value pairs to use as element attributes. The object should - * be in the format {'key': 'value'} or {key: 'value'}. The array - * should have the format [['key1', 'value1'], ['key2', 'value2']]. - * (String) text - The text child data for the element. - * - * Returns: - * A new XML DOM element. - */ - xmlElement: function (name) - { - if (!name) { return null; } - - var node = Strophe.xmlGenerator().createElement(name); - - // FIXME: this should throw errors if args are the wrong type or - // there are more than two optional args - var a, i, k; - 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]); + /** Function: xmlGenerator + * Get the DOM document to generate elements. + * + * Returns: + * The currently used DOM document. + */ + xmlGenerator: function () { + if (!Strophe._xmlGenerator) { + Strophe._xmlGenerator = Strophe._makeGenerator(); + } + return Strophe._xmlGenerator; + }, + + /** PrivateFunction: _getIEXmlDom + * Gets IE xml doc object + * + * Returns: + * A Microsoft XML DOM Object + * See Also: + * http://msdn.microsoft.com/en-us/library/ms757837%28VS.85%29.aspx + */ + _getIEXmlDom : function() { + var doc = null; + var docStrings = [ + "Msxml2.DOMDocument.6.0", + "Msxml2.DOMDocument.5.0", + "Msxml2.DOMDocument.4.0", + "MSXML2.DOMDocument.3.0", + "MSXML2.DOMDocument", + "MSXML.DOMDocument", + "Microsoft.XMLDOM" + ]; + + for (var d = 0; d < docStrings.length; d++) { + if (doc === null) { + try { + doc = new ActiveXObject(docStrings[d]); + } catch (e) { + doc = null; } + } else { + break; } - } else if (typeof(arguments[a]) == "object") { - for (k in arguments[a]) { - if (arguments[a].hasOwnProperty(k)) { - node.setAttribute(k, arguments[a][k]); + } + + return doc; + }, + + /** Function: xmlElement + * Create an XML DOM element. + * + * This function creates an XML DOM element correctly across all + * implementations. Note that these are not HTML DOM elements, which + * aren't appropriate for XMPP stanzas. + * + * Parameters: + * (String) name - The name for the element. + * (Array|Object) attrs - An optional array or object containing + * key/value pairs to use as element attributes. The object should + * be in the format {'key': 'value'} or {key: 'value'}. The array + * should have the format [['key1', 'value1'], ['key2', 'value2']]. + * (String) text - The text child data for the element. + * + * Returns: + * A new XML DOM element. + */ + xmlElement: function (name) + { + if (!name) { return null; } + + var node = Strophe.xmlGenerator().createElement(name); + + // FIXME: this should throw errors if args are the wrong type or + // there are more than two optional args + var a, i, k; + 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]); + } + } + } else if (typeof(arguments[a]) == "object") { + for (k in arguments[a]) { + if (arguments[a].hasOwnProperty(k)) { + node.setAttribute(k, arguments[a][k]); + } } } } - } - - return node; - }, - - /* Function: xmlescape - * Excapes invalid xml characters. - * - * Parameters: - * (String) text - text to escape. - * - * Returns: - * Escaped text. - */ - xmlescape: function(text) - { - text = text.replace(/\&/g, "&"); - text = text.replace(//g, ">"); - text = text.replace(/'/g, "'"); - text = text.replace(/"/g, """); - return text; - }, - - /* Function: xmlunescape - * Unexcapes invalid xml characters. - * - * Parameters: - * (String) text - text to unescape. - * - * Returns: - * Unescaped text. - */ - xmlunescape: function(text) - { - text = text.replace(/\&/g, "&"); - text = text.replace(/</g, "<"); - text = text.replace(/>/g, ">"); - text = text.replace(/'/g, "'"); - text = text.replace(/"/g, "\""); - return text; - }, - - /** 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) - { - return Strophe.xmlGenerator().createTextNode(text); - }, - - /** Function: xmlHtmlNode - * Creates an XML DOM html node. - * - * Parameters: - * (String) html - The content of the html node. - * - * Returns: - * A new XML DOM text node. - */ - xmlHtmlNode: function (html) - { - var node; - //ensure text is escaped - if (window.DOMParser) { - var parser = new DOMParser(); - node = parser.parseFromString(html, "text/xml"); - } else { - node = new ActiveXObject("Microsoft.XMLDOM"); - node.async="false"; - node.loadXML(html); - } - return node; - }, - - /** 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 node; + }, + + /* Function: xmlescape + * Excapes invalid xml characters. + * + * Parameters: + * (String) text - text to escape. + * + * Returns: + * Escaped text. + */ + xmlescape: function(text) + { + text = text.replace(/\&/g, "&"); + text = text.replace(//g, ">"); + text = text.replace(/'/g, "'"); + text = text.replace(/"/g, """); + return text; + }, + + /* Function: xmlunescape + * Unexcapes invalid xml characters. + * + * Parameters: + * (String) text - text to unescape. + * + * Returns: + * Unescaped text. + */ + xmlunescape: function(text) + { + text = text.replace(/\&/g, "&"); + text = text.replace(/</g, "<"); + text = text.replace(/>/g, ">"); + text = text.replace(/'/g, "'"); + text = text.replace(/"/g, "\""); + return text; + }, + + /** 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) + { + return Strophe.xmlGenerator().createTextNode(text); + }, + + /** Function: xmlHtmlNode + * Creates an XML DOM html node. + * + * Parameters: + * (String) html - The content of the html node. + * + * Returns: + * A new XML DOM text node. + */ + xmlHtmlNode: function (html) + { + var node; + //ensure text is escaped + if (window.DOMParser) { + var parser = new DOMParser(); + node = parser.parseFromString(html, "text/xml"); + } else { + node = new ActiveXObject("Microsoft.XMLDOM"); + node.async="false"; + node.loadXML(html); } - } - - return Strophe.xmlescape(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, - elem.attributes[i].value); + return node; + }, + + /** 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 (i = 0; i < elem.childNodes.length; i++) { - el.appendChild(Strophe.copyElement(elem.childNodes[i])); + for (var i = 0; i < elem.childNodes.length; i++) { + if (elem.childNodes[i].nodeType == Strophe.ElementType.TEXT) { + str += elem.childNodes[i].nodeValue; + } } - } else if (elem.nodeType == Strophe.ElementType.TEXT) { - el = Strophe.xmlGenerator().createTextNode(elem.nodeValue); - } - return el; - }, + return Strophe.xmlescape(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, + 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.xmlGenerator().createTextNode(elem.nodeValue); + } - /** Function: createHtml - * Copy an HTML DOM element into an XML DOM. - * - * This function copies a DOM element and all its descendants and returns - * the new copy. - * - * Parameters: - * (HTMLElement) elem - A DOM element. - * - * Returns: - * A new, copied DOM element tree. - */ - createHtml: function (elem) - { - var i, el, j, tag, attribute, value, css, cssAttrs, attr, cssName, cssValue; - if (elem.nodeType == Strophe.ElementType.NORMAL) { - tag = elem.nodeName; - if(Strophe.XHTML.validTag(tag)) { - try { - el = Strophe.xmlElement(tag); - for(i = 0; i < Strophe.XHTML.attributes[tag].length; i++) { - attribute = Strophe.XHTML.attributes[tag][i]; - value = elem.getAttribute(attribute); - if(typeof value == 'undefined' || value === null || value === '' || value === false || value === 0) { - continue; - } - if(attribute == 'style' && typeof value == 'object') { - if(typeof value.cssText != 'undefined') { - value = value.cssText; // we're dealing with IE, need to get CSS out + return el; + }, + + + /** Function: createHtml + * Copy an HTML DOM element into an XML DOM. + * + * This function copies a DOM element and all its descendants and returns + * the new copy. + * + * Parameters: + * (HTMLElement) elem - A DOM element. + * + * Returns: + * A new, copied DOM element tree. + */ + createHtml: function (elem) + { + var i, el, j, tag, attribute, value, css, cssAttrs, attr, cssName, cssValue; + if (elem.nodeType == Strophe.ElementType.NORMAL) { + tag = elem.nodeName; + if(Strophe.XHTML.validTag(tag)) { + try { + el = Strophe.xmlElement(tag); + for(i = 0; i < Strophe.XHTML.attributes[tag].length; i++) { + attribute = Strophe.XHTML.attributes[tag][i]; + value = elem.getAttribute(attribute); + if(typeof value == 'undefined' || value === null || value === '' || value === false || value === 0) { + continue; } - } - // filter out invalid css styles - if(attribute == 'style') { - css = []; - cssAttrs = value.split(';'); - for(j = 0; j < cssAttrs.length; j++) { - attr = cssAttrs[j].split(':'); - cssName = attr[0].replace(/^\s*/, "").replace(/\s*$/, "").toLowerCase(); - if(Strophe.XHTML.validCSS(cssName)) { - cssValue = attr[1].replace(/^\s*/, "").replace(/\s*$/, ""); - css.push(cssName + ': ' + cssValue); + if(attribute == 'style' && typeof value == 'object') { + if(typeof value.cssText != 'undefined') { + value = value.cssText; // we're dealing with IE, need to get CSS out } } - if(css.length > 0) { - value = css.join('; '); + // filter out invalid css styles + if(attribute == 'style') { + css = []; + cssAttrs = value.split(';'); + for(j = 0; j < cssAttrs.length; j++) { + attr = cssAttrs[j].split(':'); + cssName = attr[0].replace(/^\s*/, "").replace(/\s*$/, "").toLowerCase(); + if(Strophe.XHTML.validCSS(cssName)) { + cssValue = attr[1].replace(/^\s*/, "").replace(/\s*$/, ""); + css.push(cssName + ': ' + cssValue); + } + } + if(css.length > 0) { + value = css.join('; '); + el.setAttribute(attribute, value); + } + } else { el.setAttribute(attribute, value); } - } else { - el.setAttribute(attribute, value); } - } + for (i = 0; i < elem.childNodes.length; i++) { + el.appendChild(Strophe.createHtml(elem.childNodes[i])); + } + } catch(e) { // invalid elements + el = Strophe.xmlTextNode(''); + } + } else { + el = Strophe.xmlGenerator().createDocumentFragment(); for (i = 0; i < elem.childNodes.length; i++) { el.appendChild(Strophe.createHtml(elem.childNodes[i])); } - } catch(e) { // invalid elements - el = Strophe.xmlTextNode(''); } - } else { + } else if (elem.nodeType == Strophe.ElementType.FRAGMENT) { el = Strophe.xmlGenerator().createDocumentFragment(); for (i = 0; i < elem.childNodes.length; i++) { el.appendChild(Strophe.createHtml(elem.childNodes[i])); } + } else if (elem.nodeType == Strophe.ElementType.TEXT) { + el = Strophe.xmlTextNode(elem.nodeValue); } - } else if (elem.nodeType == Strophe.ElementType.FRAGMENT) { - el = Strophe.xmlGenerator().createDocumentFragment(); - for (i = 0; i < elem.childNodes.length; i++) { - el.appendChild(Strophe.createHtml(elem.childNodes[i])); + + return el; + }, + + /** Function: escapeNode + * Escape the node part (also called local part) of a JID. + * + * Parameters: + * (String) node - A node (or local part). + * + * Returns: + * An escaped node (or local part). + */ + escapeNode: function (node) + { + return node.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"); + }, + + /** Function: unescapeNode + * Unescape a node part (also called local part) of a JID. + * + * Parameters: + * (String) node - A node (or local part). + * + * Returns: + * An unescaped node (or local part). + */ + unescapeNode: function (node) + { + return node.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 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.getBareJidFromJid(jid); + if (bare.indexOf("@") < 0) { + return bare; + } else { + var parts = bare.split("@"); + parts.splice(0, 1); + return parts.join('@'); } - } else if (elem.nodeType == Strophe.ElementType.TEXT) { - el = Strophe.xmlTextNode(elem.nodeValue); - } + }, + + /** 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 = jid.split("/"); + if (s.length < 2) { return null; } + s.splice(0, 1); + return s.join('/'); + }, + + /** 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 jid ? jid.split("/")[0] : null; + }, + + /** 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. + */ + /* jshint ignore:start */ + log: function (level, msg) + { + return; + }, + /* jshint ignore:end */ + + /** 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; - return el; - }, + if (!elem) { return null; } - /** Function: escapeNode - * Escape the node part (also called local part) of a JID. - * - * Parameters: - * (String) node - A node (or local part). - * - * Returns: - * An escaped node (or local part). - */ - escapeNode: function (node) - { - return node.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"); - }, - - /** Function: unescapeNode - * Unescape a node part (also called local part) of a JID. + if (typeof(elem.tree) === "function") { + elem = elem.tree(); + } + + 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 + + "='" + elem.attributes[i].value + .replace(/&/g, "&") + .replace(/\'/g, "'") + .replace(/>/g, ">") + .replace(/ 0) { + result += ">"; + for (i = 0; i < elem.childNodes.length; i++) { + child = elem.childNodes[i]; + switch( child.nodeType ){ + case Strophe.ElementType.NORMAL: + // normal element, so recurse + result += Strophe.serialize(child); + break; + case Strophe.ElementType.TEXT: + // text element to escape values + result += Strophe.xmlescape(child.nodeValue); + break; + case Strophe.ElementType.CDATA: + // cdata section so don't escape values + result += ""; + } + } + result += ""; + } else { + result += "/>"; + } + + return result; + }, + + /** PrivateVariable: _requestId + * _Private_ variable that keeps track of the request ids for + * connections. + */ + _requestId: 0, + + /** PrivateVariable: Strophe.connectionPlugins + * _Private_ variable Used to store plugin names that need + * initialization on Strophe.Connection construction. + */ + _connectionPlugins: {}, + + /** Function: addConnectionPlugin + * Extends the Strophe.Connection object with the given plugin. + * + * Parameters: + * (String) name - The name of the extension. + * (Object) ptype - The plugin's prototype. + */ + addConnectionPlugin: function (name, ptype) + { + Strophe._connectionPlugins[name] = ptype; + } + }; + + /** 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) node - A node (or local part). + * (String) name - The name of the root element. + * (Object) attrs - The attributes for the root element in object notation. * * Returns: - * An unescaped node (or local part). + * A new Strophe.Builder. */ - unescapeNode: function (node) + Strophe.Builder = function (name, attrs) { - return node.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 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.getBareJidFromJid(jid); - if (bare.indexOf("@") < 0) { - return bare; - } else { - var parts = bare.split("@"); - parts.splice(0, 1); - return parts.join('@'); - } - }, - - /** 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 = jid.split("/"); - if (s.length < 2) { return null; } - s.splice(0, 1); - return s.join('/'); - }, - - /** 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 jid ? jid.split("/")[0] : null; - }, - - /** 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. - */ - /* jshint ignore:start */ - log: function (level, msg) - { - return; - }, - /* jshint ignore:end */ - - /** 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; } - - if (typeof(elem.tree) === "function") { - elem = elem.tree(); - } - - 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 + - "='" + elem.attributes[i].value - .replace(/&/g, "&") - .replace(/\'/g, "'") - .replace(/>/g, ">") - .replace(/ 0) { - result += ">"; - for (i = 0; i < elem.childNodes.length; i++) { - child = elem.childNodes[i]; - switch( child.nodeType ){ - case Strophe.ElementType.NORMAL: - // normal element, so recurse - result += Strophe.serialize(child); - break; - case Strophe.ElementType.TEXT: - // text element to escape values - result += Strophe.xmlescape(child.nodeValue); - break; - case Strophe.ElementType.CDATA: - // cdata section so don't escape values - result += ""; - } + // Set correct namespace for jabber:client elements + if (name == "presence" || name == "message" || name == "iq") { + if (attrs && !attrs.xmlns) { + attrs.xmlns = Strophe.NS.CLIENT; + } else if (!attrs) { + attrs = {xmlns: Strophe.NS.CLIENT}; } - result += ""; - } else { - result += "/>"; - } - - return result; - }, - - /** PrivateVariable: _requestId - * _Private_ variable that keeps track of the request ids for - * connections. - */ - _requestId: 0, - - /** PrivateVariable: Strophe.connectionPlugins - * _Private_ variable Used to store plugin names that need - * initialization on Strophe.Connection construction. - */ - _connectionPlugins: {}, - - /** Function: addConnectionPlugin - * Extends the Strophe.Connection object with the given plugin. - * - * Parameters: - * (String) name - The name of the extension. - * (Object) ptype - The plugin's prototype. - */ - addConnectionPlugin: function (name, ptype) - { - Strophe._connectionPlugins[name] = ptype; - } -}; - -/** 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) -{ - // Set correct namespace for jabber:client elements - if (name == "presence" || name == "message" || name == "iq") { - if (attrs && !attrs.xmlns) { - attrs.xmlns = Strophe.NS.CLIENT; - } else if (!attrs) { - attrs = {xmlns: Strophe.NS.CLIENT}; - } - } - - // Holds the tree being built. - this.nodeTree = Strophe.xmlElement(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) { - if (moreattrs.hasOwnProperty(k)) { - 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, - * unless text is provided. 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. - * (String) text - The text to add to the child. - * - * Returns: - * The Strophe.Builder object. - */ - c: function (name, attrs, text) - { - var child = Strophe.xmlElement(name, attrs, text); - this.node.appendChild(child); - if (!text) { - 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) - { - var impNode; - var xmlGen = Strophe.xmlGenerator(); - try { - impNode = (xmlGen.importNode !== undefined); - } - catch (e) { - impNode = false; - } - var newElem = impNode ? - xmlGen.importNode(elem, true) : - Strophe.copyElement(elem); - this.node.appendChild(newElem); - this.node = newElem; - 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; - }, - - /** Function: h - * Replace current element contents with the HTML passed in. - * - * This *does not* make the child the new current element - * - * Parameters: - * (String) html - The html to insert as contents of current element. - * - * Returns: - * The Strophe.Builder object. - */ - h: function (html) - { - var fragment = document.createElement('body'); - - // force the browser to try and fix any invalid HTML tags - fragment.innerHTML = html; - - // copy cleaned html into an xml dom - var xhtml = Strophe.createHtml(fragment); - - while(xhtml.childNodes.length > 0) { - this.node.appendChild(xhtml.childNodes[0]); - } - return this; - } -}; - -/** 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. - * (Object) options - Handler options - * - * Returns: - * A new Strophe.Handler object. - */ -Strophe.Handler = function (handler, ns, name, type, id, from, options) -{ - this.handler = handler; - this.ns = ns; - this.name = name; - this.type = type; - this.id = id; - this.options = options || {matchBare: false}; - - // default matchBare to false if undefined - if (!this.options.matchBare) { - this.options.matchBare = false; - } - - if (this.options.matchBare) { - this.from = from ? Strophe.getBareJidFromJid(from) : null; - } else { - 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; - var from = null; - - if (this.options.matchBare) { - from = Strophe.getBareJidFromJid(elem.getAttribute('from')); - } else { - from = elem.getAttribute('from'); - } - - nsMatch = false; - if (!this.ns) { - nsMatch = true; - } else { - var that = this; - Strophe.forEachChild(elem, null, function (elem) { - if (elem.getAttribute("xmlns") == that.ns) { - nsMatch = true; - } - }); - - nsMatch = nsMatch || elem.getAttribute("xmlns") == this.ns; } - var elem_type = elem.getAttribute("type"); - if (nsMatch && - (!this.name || Strophe.isTagEqual(elem, this.name)) && - (!this.type || (Array.isArray(this.type) ? elem_type in this.type : elem_type == this.type)) && - (!this.id || elem.getAttribute("id") == this.id) && - (!this.from || from == this.from)) { - return true; - } + // Holds the tree being built. + this.nodeTree = Strophe.xmlElement(name, attrs); - return false; - }, + // Points to the current operation node. + this.node = this.nodeTree; + }; - /** 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.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) { + if (moreattrs.hasOwnProperty(k)) { + this.node.setAttribute(k, moreattrs[k]); } - Strophe.fatal("error: " + this.handler + " " + - e.fileName + ":" + e.lineNumber + " - " + - e.name + ": " + e.message); - } else { - Strophe.fatal("error: " + e.message + "\n" + e.stack); } - - 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 +")}"; - } -}; - -/** Class: Strophe.Connection - * XMPP Connection manager. - * - * This 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, SASL SCRAM-SHA1 - * 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. - * - * The transport-protocol for this connection will be chosen automatically - * based on the given service parameter. URLs starting with "ws://" or - * "wss://" will use WebSockets, URLs starting with "http://", "https://" - * or without a protocol will use BOSH. - * - * To make Strophe connect to the current host you can leave out the protocol - * and host part and just pass the path, e.g. - * - * > var conn = new Strophe.Connection("/http-bind/"); - * - * WebSocket options: - * - * If you want to connect to the current host with a WebSocket connection you - * can tell Strophe to use WebSockets through a "protocol" attribute in the - * optional options parameter. Valid values are "ws" for WebSocket and "wss" - * for Secure WebSocket. - * So to connect to "wss://CURRENT_HOSTNAME/xmpp-websocket" you would call - * - * > var conn = new Strophe.Connection("/xmpp-websocket/", {protocol: "wss"}); - * - * Note that relative URLs _NOT_ starting with a "/" will also include the path - * of the current site. - * - * Also because downgrading security is not permitted by browsers, when using - * relative URLs both BOSH and WebSocket connections will use their secure - * variants if the current connection to the site is also secure (https). - * - * BOSH options: - * - * by adding "sync" to the options, you can control if requests will - * be made synchronously or not. The default behaviour is asynchronous. - * If you want to make requests synchronous, make "sync" evaluate to true: - * > var conn = new Strophe.Connection("/http-bind/", {sync: true}); - * You can also toggle this on an already established connection: - * > conn.options.sync = true; - * - * - * Parameters: - * (String) service - The BOSH or WebSocket service URL. - * (Object) options - A hash of configuration options - * - * Returns: - * A new Strophe.Connection object. - */ -Strophe.Connection = function (service, options) -{ - // The service URL - this.service = service; - - // Configuration options - this.options = options || {}; - var proto = this.options.protocol || ""; - - // Select protocal based on service or options - if (service.indexOf("ws:") === 0 || service.indexOf("wss:") === 0 || - proto.indexOf("ws") === 0) { - this._proto = new Strophe.Websocket(this); - } else { - this._proto = new Strophe.Bosh(this); - } - /* The connected JID. */ - this.jid = ""; - /* the JIDs domain */ - this.domain = null; - /* stream:features */ - this.features = null; - - // SASL - this._sasl_data = {}; - this.do_session = false; - this.do_bind = false; - - // handler lists - this.timedHandlers = []; - this.handlers = []; - this.removeTimeds = []; - this.removeHandlers = []; - this.addTimeds = []; - this.addHandlers = []; - - this._authentication = {}; - this._idleTimeout = null; - this._disconnectTimeout = null; - - this.do_authentication = true; - this.authenticated = false; - this.disconnecting = false; - this.connected = false; - - this.paused = false; - - this._data = []; - this._uniqueId = 0; - - this._sasl_success_handler = null; - this._sasl_failure_handler = null; - this._sasl_challenge_handler = null; - - // Max retries before disconnecting - this.maxRetries = 5; - - // setup onIdle callback every 1/10th of a second - this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); - - // initialize plugins - for (var k in Strophe._connectionPlugins) { - if (Strophe._connectionPlugins.hasOwnProperty(k)) { - var ptype = Strophe._connectionPlugins[k]; - // jslint complaints about the below line, but this is fine - var F = function () {}; // jshint ignore:line - F.prototype = ptype; - this[k] = new F(); - this[k].init(this); - } - } -}; - -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._proto._reset(); - - // SASL - this.do_session = false; - this.do_bind = false; - - // handler lists - this.timedHandlers = []; - this.handlers = []; - this.removeTimeds = []; - this.removeHandlers = []; - this.addTimeds = []; - this.addHandlers = []; - this._authentication = {}; - - this.authenticated = false; - this.disconnecting = false; - this.connected = false; - - this._data = []; - this._requests = []; - this._uniqueId = 0; - }, - - /** 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 - * BOSH-Connections 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'. - * - * The Parameters _wait_, _hold_ and _route_ are optional and only relevant - * for BOSH connections. Please see XEP 124 for a more detailed explanation - * of the optional parameters. - * - * 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. - * (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). - * (String) route - The optional route value. - */ - connect: function (jid, pass, callback, wait, hold, route) - { - this.jid = jid; - /** Variable: authzid - * Authorization identity. + 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, + * unless text is provided. 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. + * (String) text - The text to add to the child. + * + * Returns: + * The Strophe.Builder object. */ - this.authzid = Strophe.getBareJidFromJid(this.jid); - /** Variable: authcid - * Authentication identity (User name). + c: function (name, attrs, text) + { + var child = Strophe.xmlElement(name, attrs, text); + this.node.appendChild(child); + if (!text) { + 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. */ - this.authcid = Strophe.getNodeFromJid(this.jid); - /** Variable: pass - * Authentication identity (User password). + cnode: function (elem) + { + var impNode; + var xmlGen = Strophe.xmlGenerator(); + try { + impNode = (xmlGen.importNode !== undefined); + } + catch (e) { + impNode = false; + } + var newElem = impNode ? + xmlGen.importNode(elem, true) : + Strophe.copyElement(elem); + this.node.appendChild(newElem); + this.node = newElem; + 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. */ - this.pass = pass; - /** Variable: servtype - * Digest MD5 compatibility. + t: function (text) + { + var child = Strophe.xmlTextNode(text); + this.node.appendChild(child); + return this; + }, + + /** Function: h + * Replace current element contents with the HTML passed in. + * + * This *does not* make the child the new current element + * + * Parameters: + * (String) html - The html to insert as contents of current element. + * + * Returns: + * The Strophe.Builder object. */ - this.servtype = "xmpp"; - this.connect_callback = callback; - this.disconnecting = false; - this.connected = false; - this.authenticated = false; - - // parse jid for domain - this.domain = Strophe.getDomainFromJid(this.jid); - - this._changeConnectStatus(Strophe.Status.CONNECTING, null); - - this._proto._connect(wait, hold, route); - }, + h: function (html) + { + var fragment = document.createElement('body'); - /** 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. - * (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. - */ - attach: function (jid, sid, rid, callback, wait, hold, wind) - { - this._proto._attach(jid, sid, rid, callback, wait, hold, wind); - }, + // force the browser to try and fix any invalid HTML tags + fragment.innerHTML = html; - /** Function: xmlInput - * User overrideable function that receives XML data coming into the - * connection. - * - * The default function does nothing. User code can override this with - * > Strophe.Connection.xmlInput = function (elem) { - * > (user code) - * > }; - * - * Due to limitations of current Browsers' XML-Parsers the opening and closing - * tag for WebSocket-Connoctions will be passed as selfclosing here. - * - * BOSH-Connections will have all stanzas wrapped in a tag. See - * if you want to strip this tag. - * - * Parameters: - * (XMLElement) elem - The XML data received by the connection. - */ - /* jshint unused:false */ - xmlInput: function (elem) - { - return; - }, - /* jshint unused:true */ + // copy cleaned html into an xml dom + var xhtml = Strophe.createHtml(fragment); - /** Function: xmlOutput - * User overrideable function that receives XML data sent to the - * connection. - * - * The default function does nothing. User code can override this with - * > Strophe.Connection.xmlOutput = function (elem) { - * > (user code) - * > }; - * - * Due to limitations of current Browsers' XML-Parsers the opening and closing - * tag for WebSocket-Connoctions will be passed as selfclosing here. - * - * BOSH-Connections will have all stanzas wrapped in a tag. See - * if you want to strip this tag. - * - * Parameters: - * (XMLElement) elem - The XMLdata sent by the connection. - */ - /* jshint unused:false */ - xmlOutput: function (elem) - { - return; - }, - /* jshint unused: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. - */ - /* jshint unused:false */ - rawInput: function (data) - { - return; - }, - /* jshint unused:true */ - - /** 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. - */ - /* jshint unused:false */ - rawOutput: function (data) - { - return; - }, - /* jshint unused:true */ - - /** 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 | - * [XMLElement] | - * Strophe.Builder) elem - The stanza to send. - */ - send: function (elem) - { - if (elem === null) { return ; } - if (typeof(elem.sort) === "function") { - for (var i = 0; i < elem.length; i++) { - this._queueData(elem[i]); + while(xhtml.childNodes.length > 0) { + this.node.appendChild(xhtml.childNodes[0]); } - } else if (typeof(elem.tree) === "function") { - this._queueData(elem.tree()); - } else { - this._queueData(elem); + return this; } + }; - this._proto._send(); - }, - - /** Function: flush - * Immediately send any pending outgoing data. + /** 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. * - * Normally send() queues outgoing data until the next idle period - * (100ms), which optimizes network use in the common cases when - * several send()s are called in succession. flush() can be used to - * immediately send all pending data. + * Users will not use Strophe.Handler objects directly, but instead they + * will use Strophe.Connection.addHandler() and + * Strophe.Connection.deleteHandler(). */ - flush: function () - { - // cancel the pending idle period and run the idle function - // immediately - clearTimeout(this._idleTimeout); - this._onIdle(); - }, - - /** Function: sendIQ - * Helper function to send IQ stanzas. + + /** PrivateConstructor: Strophe.Handler + * Create and initialize a new Strophe.Handler. * * Parameters: - * (XMLElement) elem - The stanza to send. - * (Function) callback - The callback function for a successful request. - * (Function) errback - The callback function for a failed or timed - * out request. On timeout, the stanza will be null. - * (Integer) timeout - The time specified in milliseconds for a - * timeout to occur. + * (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. + * (Object) options - Handler options * * Returns: - * The id used to send the IQ. - */ - sendIQ: function(elem, callback, errback, timeout) { - var timeoutHandler = null; - var that = this; - - if (typeof(elem.tree) === "function") { - elem = elem.tree(); + * A new Strophe.Handler object. + */ + Strophe.Handler = function (handler, ns, name, type, id, from, options) + { + this.handler = handler; + this.ns = ns; + this.name = name; + this.type = type; + this.id = id; + this.options = options || {matchBare: false}; + + // default matchBare to false if undefined + if (!this.options.matchBare) { + this.options.matchBare = false; } - var id = elem.getAttribute('id'); - // inject id if not found - if (!id) { - id = this.getUniqueId("sendIQ"); - elem.setAttribute("id", id); + if (this.options.matchBare) { + this.from = from ? Strophe.getBareJidFromJid(from) : null; + } else { + this.from = from; } - var expectedFrom = elem.getAttribute("to"); - var fulljid = this.jid; + // whether the handler is a user handler or a system handler + this.user = true; + }; - var handler = this.addHandler(function (stanza) { - // remove timeout handler if there is one - if (timeoutHandler) { - that.deleteTimedHandler(timeoutHandler); - } + 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; + var from = null; - var acceptable = false; - var from = stanza.getAttribute("from"); - if (from === expectedFrom || - (expectedFrom === null && - (from === Strophe.getBareJidFromJid(fulljid) || - from === Strophe.getDomainFromJid(fulljid) || - from === fulljid))) { - acceptable = true; + if (this.options.matchBare) { + from = Strophe.getBareJidFromJid(elem.getAttribute('from')); + } else { + from = elem.getAttribute('from'); } - if (!acceptable) { - throw { - name: "StropheError", - message: "Got answer to IQ from wrong jid:" + from + - "\nExpected jid: " + expectedFrom - }; + nsMatch = false; + if (!this.ns) { + nsMatch = true; + } else { + var that = this; + Strophe.forEachChild(elem, null, function (elem) { + if (elem.getAttribute("xmlns") == that.ns) { + nsMatch = true; + } + }); + + nsMatch = nsMatch || elem.getAttribute("xmlns") == this.ns; } - var iqtype = stanza.getAttribute('type'); - if (iqtype == 'result') { - if (callback) { - callback(stanza); - } - } else if (iqtype == 'error') { - if (errback) { - errback(stanza); - } - } else { - throw { - name: "StropheError", - message: "Got bad IQ type of " + iqtype - }; + var elem_type = elem.getAttribute("type"); + if (nsMatch && + (!this.name || Strophe.isTagEqual(elem, this.name)) && + (!this.type || (Array.isArray(this.type) ? elem_type in this.type : elem_type == this.type)) && + (!this.id || elem.getAttribute("id") == this.id) && + (!this.from || from == this.from)) { + return true; } - }, null, 'iq', ['error', 'result'], id); - // if timeout specified, setup timeout handler. - if (timeout) { - timeoutHandler = this.addTimedHandler(timeout, function () { - // get rid of normal handler - that.deleteHandler(handler); - // call errback on timeout with null stanza - if (errback) { - errback(null); + 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: " + e.message + "\n" + e.stack); } - return false; - }); - } - this.send(elem); - return id; - }, - - /** PrivateFunction: _queueData - * Queue outgoing data for later sending. Also ensures that the data - * is a DOMElement. - */ - _queueData: function (element) { - if (element === null || - !element.tagName || - !element.childNodes) { - throw { - name: "StropheError", - message: "Cannot queue non-DOMElement." - }; - } - - this._data.push(element); - }, - /** PrivateFunction: _sendRestart - * Send an xmpp:restart stanza. - */ - _sendRestart: function () - { - this._data.push("restart"); + throw e; + } - this._proto._sendRestart(); + return result; + }, - this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); - }, + /** 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 + ")}"; + } + }; - /** 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. + /** PrivateClass: Strophe.TimedHandler + * _Private_ helper class for managing timed handlers. * - * Because of method binding it is necessary to save the result of - * this function if you wish to remove a handler with - * deleteTimedHandler(). + * 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. * - * Note that user handlers are not active until authentication is - * successful. + * 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 period of the handler. - * (Function) handler - The callback function. + * (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 reference to the handler that can be used to remove it. + * A new Strophe.TimedHandler object. */ - addTimedHandler: function (period, handler) + Strophe.TimedHandler = function (period, handler) { - var thand = new Strophe.TimedHandler(period, handler); - this.addTimeds.push(thand); - return thand; - }, + this.period = period; + this.handler = handler; - /** Function: deleteTimedHandler - * Delete a timed handler for a connection. + 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 +")}"; + } + }; + + /** Class: Strophe.Connection + * XMPP Connection manager. * - * 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(). + * This 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, SASL SCRAM-SHA1 + * and legacy authentication. * - * Parameters: - * (Strophe.TimedHandler) handRef - The handler reference. + * 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(). */ - 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. + /** Constructor: Strophe.Connection + * Create and initialize a Strophe.Connection object. * - * 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 transport-protocol for this connection will be chosen automatically + * based on the given service parameter. URLs starting with "ws://" or + * "wss://" will use WebSockets, URLs starting with "http://", "https://" + * or without a protocol will use BOSH. * - * 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.* + * To make Strophe connect to the current host you can leave out the protocol + * and host part and just pass the path, e.g. * - * 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. + * > var conn = new Strophe.Connection("/http-bind/"); * - * The options argument contains handler matching flags that affect how - * matches are determined. Currently the only flag is matchBare (a - * boolean). When matchBare is true, the from parameter and the from - * attribute on the stanza will be matched as bare JIDs instead of - * full JIDs. To use this, pass {matchBare: true} as the value of - * options. The default value for matchBare is false. + * WebSocket options: * - * The return value should be saved if you wish to remove the handler - * with deleteHandler(). + * If you want to connect to the current host with a WebSocket connection you + * can tell Strophe to use WebSockets through a "protocol" attribute in the + * optional options parameter. Valid values are "ws" for WebSocket and "wss" + * for Secure WebSocket. + * So to connect to "wss://CURRENT_HOSTNAME/xmpp-websocket" you would call * - * 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. - * (String) options - The handler options + * > var conn = new Strophe.Connection("/xmpp-websocket/", {protocol: "wss"}); * - * Returns: - * A reference to the handler that can be used to remove it. - */ - addHandler: function (handler, ns, name, type, id, from, options) - { - var hand = new Strophe.Handler(handler, ns, name, type, id, from, options); - this.addHandlers.push(hand); - return hand; - }, - - /** Function: deleteHandler - * Delete a stanza handler for a connection. + * Note that relative URLs _NOT_ starting with a "/" will also include the path + * of the current site. * - * 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(). + * Also because downgrading security is not permitted by browsers, when using + * relative URLs both BOSH and WebSocket connections will use their secure + * variants if the current connection to the site is also secure (https). * - * 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); - // If a handler is being deleted while it is being added, - // prevent it from getting added - var i = this.addHandlers.indexOf(handRef); - if (i >= 0) { - this.addHandlers.splice(i, 1); - } - }, - - /** Function: disconnect - * Start the graceful disconnection process. + * BOSH options: * - * 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. + * by adding "sync" to the options, you can control if requests will + * be made synchronously or not. The default behaviour is asynchronous. + * If you want to make requests synchronous, make "sync" evaluate to true: + * > var conn = new Strophe.Connection("/http-bind/", {sync: true}); + * You can also toggle this on an already established connection: + * > conn.options.sync = true; * - * The user supplied connection callback will be notified of the - * progress as this process happens. * * Parameters: - * (String) reason - The reason the disconnect is occuring. - */ - disconnect: function (reason) - { - this._changeConnectStatus(Strophe.Status.DISCONNECTING, reason); - - Strophe.info("Disconnect was called because: " + reason); - if (this.connected) { - var pres = false; - this.disconnecting = true; - if (this.authenticated) { - pres = $pres({ - xmlns: Strophe.NS.CLIENT, - type: 'unavailable' - }); - } - // setup timeout handler - this._disconnectTimeout = this._addSysTimedHandler( - 3000, this._onDisconnectTimeout.bind(this)); - this._proto._disconnect(pres); - } - }, - - /** PrivateFunction: _changeConnectStatus - * _Private_ helper function that makes sure plugins and the user's - * callback are notified of connection status changes. + * (String) service - The BOSH or WebSocket service URL. + * (Object) options - A hash of configuration options * - * Parameters: - * (Integer) status - the new connection status, one of the values - * in Strophe.Status - * (String) condition - the error condition or null + * Returns: + * A new Strophe.Connection object. */ - _changeConnectStatus: function (status, condition) + Strophe.Connection = function (service, options) { - // notify all plugins listening for status changes - for (var k in Strophe._connectionPlugins) { - if (Strophe._connectionPlugins.hasOwnProperty(k)) { - var plugin = this[k]; - if (plugin.statusChanged) { - try { - plugin.statusChanged(status, condition); - } catch (err) { - Strophe.error("" + k + " plugin caused an exception " + - "changing status: " + err); - } - } - } - } - - // notify the user's callback - if (this.connect_callback) { - try { - this.connect_callback(status, condition); - } catch (e) { - Strophe.error("User connection callback caused an " + - "exception: " + e); - } - } - }, + // The service URL + this.service = service; - /** 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 () - { - if (typeof this._idleTimeout == "number") { - clearTimeout(this._idleTimeout); - } + // Configuration options + this.options = options || {}; + var proto = this.options.protocol || ""; - // Cancel Disconnect Timeout - if (this._disconnectTimeout !== null) { - this.deleteTimedHandler(this._disconnectTimeout); - this._disconnectTimeout = null; + // Select protocal based on service or options + if (service.indexOf("ws:") === 0 || service.indexOf("wss:") === 0 || + proto.indexOf("ws") === 0) { + this._proto = new Strophe.Websocket(this); + } else { + this._proto = new Strophe.Bosh(this); } + /* The connected JID. */ + this.jid = ""; + /* the JIDs domain */ + this.domain = null; + /* stream:features */ + this.features = null; - Strophe.info("_doDisconnect was called"); - this._proto._doDisconnect(); - - this.authenticated = false; - this.disconnecting = false; + // SASL + this._sasl_data = {}; + this.do_session = false; + this.do_bind = false; - // delete handlers - this.handlers = []; + // handler lists this.timedHandlers = []; + this.handlers = []; this.removeTimeds = []; this.removeHandlers = []; this.addTimeds = []; this.addHandlers = []; - // tell the parent we disconnected - this._changeConnectStatus(Strophe.Status.DISCONNECTED, null); + this._authentication = {}; + this._idleTimeout = null; + this._disconnectTimeout = null; + + this.do_authentication = true; + this.authenticated = false; + this.disconnecting = false; this.connected = false; - }, - /** 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. - * (string) req - The stanza a raw string (optiona). - */ - _dataRecv: function (req, raw) - { - Strophe.info("_dataRecv called"); - var elem = this._proto._reqToData(req); - if (elem === null) { return; } + this.paused = false; - if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) { - if (elem.nodeName === this._proto.strip && elem.childNodes.length) { - this.xmlInput(elem.childNodes[0]); - } else { - this.xmlInput(elem); + this._data = []; + this._uniqueId = 0; + + this._sasl_success_handler = null; + this._sasl_failure_handler = null; + this._sasl_challenge_handler = null; + + // Max retries before disconnecting + this.maxRetries = 5; + + // setup onIdle callback every 1/10th of a second + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + + // initialize plugins + for (var k in Strophe._connectionPlugins) { + if (Strophe._connectionPlugins.hasOwnProperty(k)) { + var ptype = Strophe._connectionPlugins[k]; + // jslint complaints about the below line, but this is fine + var F = function () {}; // jshint ignore:line + F.prototype = ptype; + this[k] = new F(); + this[k].init(this); } } - if (this.rawInput !== Strophe.Connection.prototype.rawInput) { - if (raw) { - this.rawInput(raw); + }; + + 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._proto._reset(); + + // SASL + this.do_session = false; + this.do_bind = false; + + // handler lists + this.timedHandlers = []; + this.handlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + this._authentication = {}; + + this.authenticated = false; + this.disconnecting = false; + this.connected = false; + + this._data = []; + this._requests = []; + this._uniqueId = 0; + }, + + /** 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 + * BOSH-Connections 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 { - this.rawInput(Strophe.serialize(elem)); + 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'. + * + * The Parameters _wait_, _hold_ and _route_ are optional and only relevant + * for BOSH connections. Please see XEP 124 for a more detailed explanation + * of the optional parameters. + * + * 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. + * (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). + * (String) route - The optional route value. + */ + connect: function (jid, pass, callback, wait, hold, route) + { + this.jid = jid; + /** Variable: authzid + * Authorization identity. + */ + this.authzid = Strophe.getBareJidFromJid(this.jid); + /** Variable: authcid + * Authentication identity (User name). + */ + this.authcid = Strophe.getNodeFromJid(this.jid); + /** Variable: pass + * Authentication identity (User password). + */ + this.pass = pass; + /** Variable: servtype + * Digest MD5 compatibility. + */ + this.servtype = "xmpp"; + this.connect_callback = callback; + this.disconnecting = false; + this.connected = false; + this.authenticated = false; + + // parse jid for domain + this.domain = Strophe.getDomainFromJid(this.jid); + + this._changeConnectStatus(Strophe.Status.CONNECTING, null); + + this._proto._connect(wait, hold, route); + }, + + /** 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. + * (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. + */ + attach: function (jid, sid, rid, callback, wait, hold, wind) + { + this._proto._attach(jid, sid, rid, callback, wait, hold, wind); + }, + + /** Function: xmlInput + * User overrideable function that receives XML data coming into the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.xmlInput = function (elem) { + * > (user code) + * > }; + * + * Due to limitations of current Browsers' XML-Parsers the opening and closing + * tag for WebSocket-Connoctions will be passed as selfclosing here. + * + * BOSH-Connections will have all stanzas wrapped in a tag. See + * if you want to strip this tag. + * + * Parameters: + * (XMLElement) elem - The XML data received by the connection. + */ + /* jshint unused:false */ + xmlInput: function (elem) + { + return; + }, + /* jshint unused:true */ + + /** Function: xmlOutput + * User overrideable function that receives XML data sent to the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.xmlOutput = function (elem) { + * > (user code) + * > }; + * + * Due to limitations of current Browsers' XML-Parsers the opening and closing + * tag for WebSocket-Connoctions will be passed as selfclosing here. + * + * BOSH-Connections will have all stanzas wrapped in a tag. See + * if you want to strip this tag. + * + * Parameters: + * (XMLElement) elem - The XMLdata sent by the connection. + */ + /* jshint unused:false */ + xmlOutput: function (elem) + { + return; + }, + /* jshint unused: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. + */ + /* jshint unused:false */ + rawInput: function (data) + { + return; + }, + /* jshint unused:true */ + + /** 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. + */ + /* jshint unused:false */ + rawOutput: function (data) + { + return; + }, + /* jshint unused:true */ + + /** 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 | + * [XMLElement] | + * Strophe.Builder) elem - The stanza to send. + */ + send: function (elem) + { + if (elem === null) { return ; } + if (typeof(elem.sort) === "function") { + for (var i = 0; i < elem.length; i++) { + this._queueData(elem[i]); + } + } else if (typeof(elem.tree) === "function") { + this._queueData(elem.tree()); + } else { + this._queueData(elem); + } + + this._proto._send(); + }, + + /** Function: flush + * Immediately send any pending outgoing data. + * + * Normally send() queues outgoing data until the next idle period + * (100ms), which optimizes network use in the common cases when + * several send()s are called in succession. flush() can be used to + * immediately send all pending data. + */ + flush: function () + { + // cancel the pending idle period and run the idle function + // immediately + clearTimeout(this._idleTimeout); + this._onIdle(); + }, + + /** Function: sendIQ + * Helper function to send IQ stanzas. + * + * Parameters: + * (XMLElement) elem - The stanza to send. + * (Function) callback - The callback function for a successful request. + * (Function) errback - The callback function for a failed or timed + * out request. On timeout, the stanza will be null. + * (Integer) timeout - The time specified in milliseconds for a + * timeout to occur. + * + * Returns: + * The id used to send the IQ. + */ + sendIQ: function(elem, callback, errback, timeout) { + var timeoutHandler = null; + var that = this; + + if (typeof(elem.tree) === "function") { + elem = elem.tree(); + } + var id = elem.getAttribute('id'); + + // inject id if not found + if (!id) { + id = this.getUniqueId("sendIQ"); + elem.setAttribute("id", id); + } + + var expectedFrom = elem.getAttribute("to"); + var fulljid = this.jid; + + var handler = this.addHandler(function (stanza) { + // remove timeout handler if there is one + if (timeoutHandler) { + that.deleteTimedHandler(timeoutHandler); + } + + var acceptable = false; + var from = stanza.getAttribute("from"); + if (from === expectedFrom || + (expectedFrom === null && + (from === Strophe.getBareJidFromJid(fulljid) || + from === Strophe.getDomainFromJid(fulljid) || + from === fulljid))) { + acceptable = true; + } + + if (!acceptable) { + throw { + name: "StropheError", + message: "Got answer to IQ from wrong jid:" + from + + "\nExpected jid: " + expectedFrom + }; + } + + var iqtype = stanza.getAttribute('type'); + if (iqtype == 'result') { + if (callback) { + callback(stanza); + } + } else if (iqtype == 'error') { + if (errback) { + errback(stanza); + } + } else { + throw { + name: "StropheError", + message: "Got bad IQ type of " + iqtype + }; + } + }, null, 'iq', ['error', 'result'], id); + + // if timeout specified, setup timeout handler. + if (timeout) { + timeoutHandler = this.addTimedHandler(timeout, function () { + // get rid of normal handler + that.deleteHandler(handler); + // call errback on timeout with null stanza + if (errback) { + errback(null); + } + return false; + }); + } + this.send(elem); + return id; + }, + + /** PrivateFunction: _queueData + * Queue outgoing data for later sending. Also ensures that the data + * is a DOMElement. + */ + _queueData: function (element) { + if (element === null || + !element.tagName || + !element.childNodes) { + throw { + name: "StropheError", + message: "Cannot queue non-DOMElement." + }; + } + + this._data.push(element); + }, - // remove handlers scheduled for deletion - var i, hand; - while (this.removeHandlers.length > 0) { - hand = this.removeHandlers.pop(); - i = this.handlers.indexOf(hand); + /** PrivateFunction: _sendRestart + * Send an xmpp:restart stanza. + */ + _sendRestart: function () + { + this._data.push("restart"); + + this._proto._sendRestart(); + + 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 options argument contains handler matching flags that affect how + * matches are determined. Currently the only flag is matchBare (a + * boolean). When matchBare is true, the from parameter and the from + * attribute on the stanza will be matched as bare JIDs instead of + * full JIDs. To use this, pass {matchBare: true} as the value of + * options. The default value for matchBare is false. + * + * 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. + * (String) options - The handler options + * + * Returns: + * A reference to the handler that can be used to remove it. + */ + addHandler: function (handler, ns, name, type, id, from, options) + { + var hand = new Strophe.Handler(handler, ns, name, type, id, from, options); + 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); + // If a handler is being deleted while it is being added, + // prevent it from getting added + var i = this.addHandlers.indexOf(handRef); if (i >= 0) { - this.handlers.splice(i, 1); + this.addHandlers.splice(i, 1); + } + }, + + /** 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. + * + * Parameters: + * (String) reason - The reason the disconnect is occuring. + */ + disconnect: function (reason) + { + this._changeConnectStatus(Strophe.Status.DISCONNECTING, reason); + + Strophe.info("Disconnect was called because: " + reason); + if (this.connected) { + var pres = false; + this.disconnecting = true; + if (this.authenticated) { + pres = $pres({ + xmlns: Strophe.NS.CLIENT, + type: 'unavailable' + }); + } + // setup timeout handler + this._disconnectTimeout = this._addSysTimedHandler( + 3000, this._onDisconnectTimeout.bind(this)); + this._proto._disconnect(pres); + } + }, + + /** PrivateFunction: _changeConnectStatus + * _Private_ helper function that makes sure plugins and the user's + * callback are notified of connection status changes. + * + * Parameters: + * (Integer) status - the new connection status, one of the values + * in Strophe.Status + * (String) condition - the error condition or null + */ + _changeConnectStatus: function (status, condition) + { + // notify all plugins listening for status changes + for (var k in Strophe._connectionPlugins) { + if (Strophe._connectionPlugins.hasOwnProperty(k)) { + var plugin = this[k]; + if (plugin.statusChanged) { + try { + plugin.statusChanged(status, condition); + } catch (err) { + Strophe.error("" + k + " plugin caused an exception " + + "changing status: " + err); + } + } + } } - } - // add handlers scheduled for addition - while (this.addHandlers.length > 0) { - this.handlers.push(this.addHandlers.pop()); - } + // notify the user's callback + if (this.connect_callback) { + try { + this.connect_callback(status, condition); + } catch (e) { + Strophe.error("User connection callback caused an " + + "exception: " + e); + } + } + }, - // handle graceful disconnect - if (this.disconnecting && this._proto._emptyQueue()) { - this._doDisconnect(); - return; - } + /** 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 () + { + if (typeof this._idleTimeout == "number") { + clearTimeout(this._idleTimeout); + } + + // Cancel Disconnect Timeout + if (this._disconnectTimeout !== null) { + this.deleteTimedHandler(this._disconnectTimeout); + this._disconnectTimeout = null; + } + + Strophe.info("_doDisconnect was called"); + this._proto._doDisconnect(); + + this.authenticated = false; + this.disconnecting = false; + + // delete handlers + this.handlers = []; + this.timedHandlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + + // tell the parent we disconnected + this._changeConnectStatus(Strophe.Status.DISCONNECTED, null); + this.connected = false; + }, + + /** 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. + * (string) req - The stanza a raw string (optiona). + */ + _dataRecv: function (req, raw) + { + Strophe.info("_dataRecv called"); + var elem = this._proto._reqToData(req); + if (elem === null) { return; } + + if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) { + if (elem.nodeName === this._proto.strip && elem.childNodes.length) { + this.xmlInput(elem.childNodes[0]); + } else { + this.xmlInput(elem); + } + } + if (this.rawInput !== Strophe.Connection.prototype.rawInput) { + if (raw) { + this.rawInput(raw); + } else { + this.rawInput(Strophe.serialize(elem)); + } + } + + // 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); + } + } - var type = elem.getAttribute("type"); - var cond, conflict; - if (type !== null && type == "terminate") { - // Don't process stanzas that come in after disconnect - if (this.disconnecting) { + // add handlers scheduled for addition + while (this.addHandlers.length > 0) { + this.handlers.push(this.addHandlers.pop()); + } + + // handle graceful disconnect + if (this.disconnecting && this._proto._emptyQueue()) { + this._doDisconnect(); return; } - // an error occurred - cond = elem.getAttribute("condition"); - conflict = elem.getElementsByTagName("conflict"); - if (cond !== null) { - if (cond == "remote-stream-error" && conflict.length > 0) { - cond = "conflict"; + var type = elem.getAttribute("type"); + var cond, conflict; + if (type !== null && type == "terminate") { + // Don't process stanzas that come in after disconnect + if (this.disconnecting) { + return; } - this._changeConnectStatus(Strophe.Status.CONNFAIL, cond); - } else { - this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); + + // 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._changeConnectStatus(Strophe.Status.CONNFAIL, cond); + } else { + this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); + } + this._doDisconnect(); + return; } - this._doDisconnect(); - return; - } - // send each incoming stanza through the handler chain - var that = this; - Strophe.forEachChild(elem, null, function (child) { - var i, newList; - // process handlers - newList = that.handlers; - that.handlers = []; - for (i = 0; i < newList.length; i++) { - var hand = newList[i]; - // encapsulate 'handler.run' not to lose the whole handler list if - // one of the handlers throws an exception - try { - if (hand.isMatch(child) && - (that.authenticated || !hand.user)) { - if (hand.run(child)) { + // send each incoming stanza through the handler chain + var that = this; + Strophe.forEachChild(elem, null, function (child) { + var i, newList; + // process handlers + newList = that.handlers; + that.handlers = []; + for (i = 0; i < newList.length; i++) { + var hand = newList[i]; + // encapsulate 'handler.run' not to lose the whole handler list if + // one of the handlers throws an exception + try { + if (hand.isMatch(child) && + (that.authenticated || !hand.user)) { + if (hand.run(child)) { + that.handlers.push(hand); + } + } else { that.handlers.push(hand); } - } else { - that.handlers.push(hand); + } catch(e) { + // if the handler throws an exception, we consider it as false + Strophe.warn('Removing Strophe handlers due to uncaught exception: ' + e.message); } - } catch(e) { - // if the handler throws an exception, we consider it as false - Strophe.warn('Removing Strophe handlers due to uncaught exception: ' + e.message); } - } - }); - }, - + }); + }, - /** Attribute: mechanisms - * SASL Mechanisms available for Conncection. - */ - mechanisms: {}, - /** 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. - * (Function) _callback - low level (xmpp) connect callback function. - * Useful for plugins with their own xmpp connect callback (when their) - * want to do something special). - */ - _connect_cb: function (req, _callback, raw) - { - Strophe.info("_connect_cb was called"); + /** Attribute: mechanisms + * SASL Mechanisms available for Conncection. + */ + mechanisms: {}, + + /** 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. + * (Function) _callback - low level (xmpp) connect callback function. + * Useful for plugins with their own xmpp connect callback (when their) + * want to do something special). + */ + _connect_cb: function (req, _callback, raw) + { + Strophe.info("_connect_cb was called"); - this.connected = true; + this.connected = true; - var bodyWrap = this._proto._reqToData(req); - if (!bodyWrap) { return; } + var bodyWrap = this._proto._reqToData(req); + if (!bodyWrap) { return; } - if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) { - if (bodyWrap.nodeName === this._proto.strip && bodyWrap.childNodes.length) { - this.xmlInput(bodyWrap.childNodes[0]); - } else { - this.xmlInput(bodyWrap); - } - } - if (this.rawInput !== Strophe.Connection.prototype.rawInput) { - if (raw) { - this.rawInput(raw); - } else { - this.rawInput(Strophe.serialize(bodyWrap)); + if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) { + if (bodyWrap.nodeName === this._proto.strip && bodyWrap.childNodes.length) { + this.xmlInput(bodyWrap.childNodes[0]); + } else { + this.xmlInput(bodyWrap); + } + } + if (this.rawInput !== Strophe.Connection.prototype.rawInput) { + if (raw) { + this.rawInput(raw); + } else { + this.rawInput(Strophe.serialize(bodyWrap)); + } } - } - var conncheck = this._proto._connect_cb(bodyWrap); - if (conncheck === Strophe.Status.CONNFAIL) { - return; - } + var conncheck = this._proto._connect_cb(bodyWrap); + if (conncheck === Strophe.Status.CONNFAIL) { + return; + } - this._authentication.sasl_scram_sha1 = false; - this._authentication.sasl_plain = false; - this._authentication.sasl_digest_md5 = false; - this._authentication.sasl_anonymous = false; + this._authentication.sasl_scram_sha1 = false; + this._authentication.sasl_plain = false; + this._authentication.sasl_digest_md5 = false; + this._authentication.sasl_anonymous = false; - this._authentication.legacy_auth = false; + this._authentication.legacy_auth = false; - // Check for the stream:features tag - var hasFeatures = bodyWrap.getElementsByTagNameNS(Strophe.NS.STREAM, "features").length > 0; - var mechanisms = bodyWrap.getElementsByTagName("mechanism"); - var matched = []; - var i, mech, found_authentication = false; - if (!hasFeatures) { - this._proto._no_auth_received(_callback); - return; - } - if (mechanisms.length > 0) { - for (i = 0; i < mechanisms.length; i++) { - mech = Strophe.getText(mechanisms[i]); - if (this.mechanisms[mech]) matched.push(this.mechanisms[mech]); + // Check for the stream:features tag + var hasFeatures = bodyWrap.getElementsByTagNameNS(Strophe.NS.STREAM, "features").length > 0; + var mechanisms = bodyWrap.getElementsByTagName("mechanism"); + var matched = []; + var i, mech, found_authentication = false; + if (!hasFeatures) { + this._proto._no_auth_received(_callback); + return; + } + if (mechanisms.length > 0) { + for (i = 0; i < mechanisms.length; i++) { + mech = Strophe.getText(mechanisms[i]); + if (this.mechanisms[mech]) matched.push(this.mechanisms[mech]); + } + } + this._authentication.legacy_auth = + bodyWrap.getElementsByTagName("auth").length > 0; + found_authentication = this._authentication.legacy_auth || + matched.length > 0; + if (!found_authentication) { + this._proto._no_auth_received(_callback); + return; + } + if (this.do_authentication !== false) + this.authenticate(matched); + }, + + /** Function: authenticate + * Set up authentication + * + * Contiunues the initial connection request by setting up authentication + * handlers and start the authentication process. + * + * SASL authentication will be attempted if available, otherwise + * the code will fall back to legacy authentication. + * + */ + authenticate: function (matched) + { + var i; + // Sorting matched mechanisms according to priority. + for (i = 0; i < matched.length - 1; ++i) { + var higher = i; + for (var j = i + 1; j < matched.length; ++j) { + if (matched[j].prototype.priority > matched[higher].prototype.priority) { + higher = j; + } + } + if (higher != i) { + var swap = matched[i]; + matched[i] = matched[higher]; + matched[higher] = swap; } - } - this._authentication.legacy_auth = - bodyWrap.getElementsByTagName("auth").length > 0; - found_authentication = this._authentication.legacy_auth || - matched.length > 0; - if (!found_authentication) { - this._proto._no_auth_received(_callback); - return; - } - if (this.do_authentication !== false) - this.authenticate(matched); - }, - - /** Function: authenticate - * Set up authentication - * - * Contiunues the initial connection request by setting up authentication - * handlers and start the authentication process. - * - * SASL authentication will be attempted if available, otherwise - * the code will fall back to legacy authentication. - * - */ - authenticate: function (matched) - { - var i; - // Sorting matched mechanisms according to priority. - for (i = 0; i < matched.length - 1; ++i) { - var higher = i; - for (var j = i + 1; j < matched.length; ++j) { - if (matched[j].prototype.priority > matched[higher].prototype.priority) { - higher = j; } - } - if (higher != i) { - var swap = matched[i]; - matched[i] = matched[higher]; - matched[higher] = swap; - } - } - // run each mechanism - var mechanism_found = false; - for (i = 0; i < matched.length; ++i) { - if (!matched[i].test(this)) continue; - - 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._sasl_challenge_handler = this._addSysHandler( - this._sasl_challenge_cb.bind(this), null, - "challenge", null, null); - - this._sasl_mechanism = new matched[i](); - this._sasl_mechanism.onStart(this); - - var request_auth_exchange = $build("auth", { - xmlns: Strophe.NS.SASL, - mechanism: this._sasl_mechanism.name - }); + // run each mechanism + var mechanism_found = false; + for (i = 0; i < matched.length; ++i) { + if (!matched[i].test(this)) continue; + + 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._sasl_challenge_handler = this._addSysHandler( + this._sasl_challenge_cb.bind(this), null, + "challenge", null, null); + + this._sasl_mechanism = new matched[i](); + this._sasl_mechanism.onStart(this); + + var request_auth_exchange = $build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: this._sasl_mechanism.name + }); - if (this._sasl_mechanism.isClientFirst) { - var response = this._sasl_mechanism.onChallenge(this, null); - request_auth_exchange.t(Base64.encode(response)); - } + if (this._sasl_mechanism.isClientFirst) { + var response = this._sasl_mechanism.onChallenge(this, null); + request_auth_exchange.t(Base64.encode(response)); + } - this.send(request_auth_exchange.tree()); + this.send(request_auth_exchange.tree()); - mechanism_found = true; - break; - } + mechanism_found = true; + break; + } - if (!mechanism_found) { - // if none of the mechanism worked - if (Strophe.getNodeFromJid(this.jid) === null) { - // we don't have a node, which is required for non-anonymous - // client connections - this._changeConnectStatus(Strophe.Status.CONNFAIL, - 'x-strophe-bad-non-anon-jid'); - this.disconnect('x-strophe-bad-non-anon-jid'); - } else { - // fall back to legacy authentication - this._changeConnectStatus(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()); - } - } + if (!mechanism_found) { + // if none of the mechanism worked + if (Strophe.getNodeFromJid(this.jid) === null) { + // we don't have a node, which is required for non-anonymous + // client connections + this._changeConnectStatus(Strophe.Status.CONNFAIL, + 'x-strophe-bad-non-anon-jid'); + this.disconnect('x-strophe-bad-non-anon-jid'); + } else { + // fall back to legacy authentication + this._changeConnectStatus(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()); + } + } - }, + }, - _sasl_challenge_cb: function(elem) { - var challenge = Base64.decode(Strophe.getText(elem)); - var response = this._sasl_mechanism.onChallenge(this, challenge); + _sasl_challenge_cb: function(elem) { + var challenge = Base64.decode(Strophe.getText(elem)); + var response = this._sasl_mechanism.onChallenge(this, challenge); - var stanza = $build('response', { - xmlns: Strophe.NS.SASL - }); - if (response !== "") { - stanza.t(Base64.encode(response)); - } - this.send(stanza.tree()); + var stanza = $build('response', { + xmlns: Strophe.NS.SASL + }); + if (response !== "") { + stanza.t(Base64.encode(response)); + } + this.send(stanza.tree()); + + return true; + }, + + /** 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. + */ + /* jshint unused:false */ + _auth1_cb: function (elem) + { + // build plaintext auth iq + var iq = $iq({type: "set", id: "_auth_2"}) + .c('query', {xmlns: Strophe.NS.AUTH}) + .c('username', {}).t(Strophe.getNodeFromJid(this.jid)) + .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)); - return true; - }, + this._addSysHandler(this._auth2_cb.bind(this), null, + null, null, "_auth_2"); - /** 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. - */ - /* jshint unused:false */ - _auth1_cb: function (elem) - { - // build plaintext auth iq - var iq = $iq({type: "set", id: "_auth_2"}) - .c('query', {xmlns: Strophe.NS.AUTH}) - .c('username', {}).t(Strophe.getNodeFromJid(this.jid)) - .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.send(iq.tree()); - this._addSysHandler(this._auth2_cb.bind(this), null, - null, null, "_auth_2"); + return false; + }, + /* jshint unused:true */ + + /** 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) + { + if (this._sasl_data["server-signature"]) { + var serverSignature; + var success = Base64.decode(Strophe.getText(elem)); + var attribMatch = /([a-z]+)=([^,]+)(,|$)/; + var matches = success.match(attribMatch); + if (matches[1] == "v") { + serverSignature = matches[2]; + } - this.send(iq.tree()); + if (serverSignature != this._sasl_data["server-signature"]) { + // 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._sasl_data = {}; + return this._sasl_failure_cb(null); + } + } - return false; - }, - /* jshint unused:true */ + Strophe.info("SASL authentication succeeded."); - /** 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) - { - if (this._sasl_data["server-signature"]) { - var serverSignature; - var success = Base64.decode(Strophe.getText(elem)); - var attribMatch = /([a-z]+)=([^,]+)(,|$)/; - var matches = success.match(attribMatch); - if (matches[1] == "v") { - serverSignature = matches[2]; - } - - if (serverSignature != this._sasl_data["server-signature"]) { - // remove old handlers - this.deleteHandler(this._sasl_failure_handler); - this._sasl_failure_handler = null; - if (this._sasl_challenge_handler) { + if(this._sasl_mechanism) + this._sasl_mechanism.onSuccess(); + + // 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._sasl_data = {}; - return this._sasl_failure_cb(null); } - } - Strophe.info("SASL authentication succeeded."); + this._addSysHandler(this._sasl_auth1_cb.bind(this), null, + "stream:features", null, null); - if(this._sasl_mechanism) - this._sasl_mechanism.onSuccess(); + // we must send an xmpp:restart now + this._sendRestart(); - // 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; - } + 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) + { + // save stream:features for future usage + this.features = elem; - this._addSysHandler(this._sasl_auth1_cb.bind(this), null, - "stream:features", null, null); + var i, child; - // we must send an xmpp:restart now - this._sendRestart(); + for (i = 0; i < elem.childNodes.length; i++) { + child = elem.childNodes[i]; + if (child.nodeName == 'bind') { + this.do_bind = true; + } - return false; - }, + if (child.nodeName == 'session') { + this.do_session = true; + } + } - /** 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) - { - // save stream:features for future usage - this.features = elem; + if (!this.do_bind) { + this._changeConnectStatus(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()); + } + } - var i, child; + 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."); + var conflict = elem.getElementsByTagName("conflict"), condition; + if (conflict.length > 0) { + condition = 'conflict'; + } + this._changeConnectStatus(Strophe.Status.AUTHFAIL, condition); + return false; + } - for (i = 0; i < elem.childNodes.length; i++) { - child = elem.childNodes[i]; - if (child.nodeName == 'bind') { - this.do_bind = true; + // 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 { + this.authenticated = true; + this._changeConnectStatus(Strophe.Status.CONNECTED, null); + } + } + } else { + Strophe.info("SASL binding failed."); + this._changeConnectStatus(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._changeConnectStatus(Strophe.Status.CONNECTED, null); + } else if (elem.getAttribute("type") == "error") { + Strophe.info("Session creation failed."); + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; } - if (child.nodeName == 'session') { - this.do_session = true; + return false; + }, + + /** PrivateFunction: _sasl_failure_cb + * _Private_ handler for SASL authentication failure. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + /* jshint unused:false */ + _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; } - } - if (!this.do_bind) { + if(this._sasl_mechanism) + this._sasl_mechanism.onFailure(); this._changeConnectStatus(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()); + }, + /* jshint unused:true */ + + /** 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._changeConnectStatus(Strophe.Status.CONNECTED, null); + } else if (elem.getAttribute("type") == "error") { + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + this.disconnect('authentication failed'); } - } - return false; - }, + 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"); + + this._proto._onDisconnectTimeout(); + + // actually disconnect + this._doDisconnect(); - /** 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."); - var conflict = elem.getElementsByTagName("conflict"), condition; - if (conflict.length > 0) { - condition = 'conflict'; - } - this._changeConnectStatus(Strophe.Status.AUTHFAIL, condition); 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 { - this.authenticated = true; - this._changeConnectStatus(Strophe.Status.CONNECTED, null); - } + /** 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; + + // add timed handlers scheduled for addition + // NOTE: we add before remove in the case a timed handler is + // added and then deleted before the next _onIdle() call. + while (this.addTimeds.length > 0) { + this.timedHandlers.push(this.addTimeds.pop()); } - } else { - Strophe.info("SASL binding failed."); - this._changeConnectStatus(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._changeConnectStatus(Strophe.Status.CONNECTED, null); - } else if (elem.getAttribute("type") == "error") { - Strophe.info("Session creation failed."); - this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); - return false; - } + // 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); + } + } - return false; - }, + // 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; - /** PrivateFunction: _sasl_failure_cb - * _Private_ handler for SASL authentication failure. - * - * Parameters: - * (XMLElement) elem - The matching stanza. - * - * Returns: - * false to remove the handler. - */ - /* jshint unused:false */ - _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; - } + clearTimeout(this._idleTimeout); - if(this._sasl_mechanism) - this._sasl_mechanism.onFailure(); - this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); - return false; - }, - /* jshint unused:true */ + this._proto._onIdle(); - /** 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._changeConnectStatus(Strophe.Status.CONNECTED, null); - } else if (elem.getAttribute("type") == "error") { - this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); - this.disconnect('authentication failed'); + // reactivate the timer only if connected + if (this.connected) { + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + } } + }; - return false; - }, - - /** PrivateFunction: _addSysTimedHandler - * _Private_ function to add a system level timed handler. + /** Class: Strophe.SASLMechanism * - * This function is used to add a Strophe.TimedHandler for the - * library code. System timed handlers are allowed to run before - * authentication is complete. + * encapsulates SASL authentication mechanisms. * - * 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. + * User code may override the priority for each mechanism or disable it completely. + * See for information about changing priority and for informatian on + * how to disable a mechanism. * - * This function is used to add a Strophe.Handler for the - * library code. System stanza handlers are allowed to run before - * authentication is complete. + * By default, all mechanisms are enabled and the priorities are * - * 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. + * SCRAM-SHA1 - 40 + * DIGEST-MD5 - 30 + * Plain - 20 */ - _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. + + /** + * PrivateConstructor: Strophe.SASLMechanism + * SASL auth mechanism abstraction. * - * If the graceful disconnect process does not complete within the - * time allotted, this handler finishes the disconnect anyway. + * Parameters: + * (String) name - SASL Mechanism name. + * (Boolean) isClientFirst - If client should send response first without challenge. + * (Number) priority - Priority. * * Returns: - * false to remove the handler. - */ - _onDisconnectTimeout: function () - { - Strophe.info("_onDisconnectTimeout was called"); + * A new Strophe.SASLMechanism object. + */ + Strophe.SASLMechanism = function(name, isClientFirst, priority) { + /** PrivateVariable: name + * Mechanism name. + */ + this.name = name; + /** PrivateVariable: isClientFirst + * If client sends response without initial server challenge. + */ + this.isClientFirst = isClientFirst; + /** Variable: priority + * Determines which is chosen for authentication (Higher is better). + * Users may override this to prioritize mechanisms differently. + * + * In the default configuration the priorities are + * + * SCRAM-SHA1 - 40 + * DIGEST-MD5 - 30 + * Plain - 20 + * + * Example: (This will cause Strophe to choose the mechanism that the server sent first) + * + * > Strophe.SASLMD5.priority = Strophe.SASLSHA1.priority; + * + * See for a list of available mechanisms. + * + */ + this.priority = priority; + }; - this._proto._onDisconnectTimeout(); + Strophe.SASLMechanism.prototype = { + /** + * Function: test + * Checks if mechanism able to run. + * To disable a mechanism, make this return false; + * + * To disable plain authentication run + * > Strophe.SASLPlain.test = function() { + * > return false; + * > } + * + * See for a list of available mechanisms. + * + * Parameters: + * (Strophe.Connection) connection - Target Connection. + * + * Returns: + * (Boolean) If mechanism was able to run. + */ + /* jshint unused:false */ + test: function(connection) { + return true; + }, + /* jshint unused:true */ + + /** PrivateFunction: onStart + * Called before starting mechanism on some connection. + * + * Parameters: + * (Strophe.Connection) connection - Target Connection. + */ + onStart: function(connection) + { + this._connection = connection; + }, + + /** PrivateFunction: onChallenge + * Called by protocol implementation on incoming challenge. If client is + * first (isClientFirst == true) challenge will be null on the first call. + * + * Parameters: + * (Strophe.Connection) connection - Target Connection. + * (String) challenge - current challenge to handle. + * + * Returns: + * (String) Mechanism response. + */ + /* jshint unused:false */ + onChallenge: function(connection, challenge) { + throw new Error("You should implement challenge handling!"); + }, + /* jshint unused:true */ + + /** PrivateFunction: onFailure + * Protocol informs mechanism implementation about SASL failure. + */ + onFailure: function() { + this._connection = null; + }, + + /** PrivateFunction: onSuccess + * Protocol informs mechanism implementation about SASL success. + */ + onSuccess: function() { + this._connection = null; + } + }; - // actually disconnect - this._doDisconnect(); + /** Constants: SASL mechanisms + * Available authentication mechanisms + * + * Strophe.SASLAnonymous - SASL Anonymous authentication. + * Strophe.SASLPlain - SASL Plain authentication. + * Strophe.SASLMD5 - SASL Digest-MD5 authentication + * Strophe.SASLSHA1 - SASL SCRAM-SHA1 authentication + */ - return false; - }, + // Building SASL callbacks - /** 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. + /** PrivateConstructor: SASLAnonymous + * SASL Anonymous authentication. */ - _onIdle: function () - { - var i, thand, since, newList; - - // add timed handlers scheduled for addition - // NOTE: we add before remove in the case a timed handler is - // added and then deleted before the next _onIdle() call. - while (this.addTimeds.length > 0) { - this.timedHandlers.push(this.addTimeds.pop()); - } - - // 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); - } - } - - // 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; + Strophe.SASLAnonymous = function() {}; - clearTimeout(this._idleTimeout); + Strophe.SASLAnonymous.prototype = new Strophe.SASLMechanism("ANONYMOUS", false, 10); - this._proto._onIdle(); + Strophe.SASLAnonymous.test = function(connection) { + return connection.authcid === null; + }; - // reactivate the timer only if connected - if (this.connected) { - this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); - } - } -}; + Strophe.Connection.prototype.mechanisms[Strophe.SASLAnonymous.prototype.name] = Strophe.SASLAnonymous; -if (callback) { - callback(Strophe, $build, $msg, $iq, $pres); -} + /** PrivateConstructor: SASLPlain + * SASL Plain authentication. + */ + Strophe.SASLPlain = function() {}; -/** Class: Strophe.SASLMechanism - * - * encapsulates SASL authentication mechanisms. - * - * User code may override the priority for each mechanism or disable it completely. - * See for information about changing priority and for informatian on - * how to disable a mechanism. - * - * By default, all mechanisms are enabled and the priorities are - * - * SCRAM-SHA1 - 40 - * DIGEST-MD5 - 30 - * Plain - 20 - */ + Strophe.SASLPlain.prototype = new Strophe.SASLMechanism("PLAIN", true, 20); -/** - * PrivateConstructor: Strophe.SASLMechanism - * SASL auth mechanism abstraction. - * - * Parameters: - * (String) name - SASL Mechanism name. - * (Boolean) isClientFirst - If client should send response first without challenge. - * (Number) priority - Priority. - * - * Returns: - * A new Strophe.SASLMechanism object. - */ -Strophe.SASLMechanism = function(name, isClientFirst, priority) { - /** PrivateVariable: name - * Mechanism name. - */ - this.name = name; - /** PrivateVariable: isClientFirst - * If client sends response without initial server challenge. - */ - this.isClientFirst = isClientFirst; - /** Variable: priority - * Determines which is chosen for authentication (Higher is better). - * Users may override this to prioritize mechanisms differently. - * - * In the default configuration the priorities are - * - * SCRAM-SHA1 - 40 - * DIGEST-MD5 - 30 - * Plain - 20 - * - * Example: (This will cause Strophe to choose the mechanism that the server sent first) - * - * > Strophe.SASLMD5.priority = Strophe.SASLSHA1.priority; - * - * See for a list of available mechanisms. - * - */ - this.priority = priority; -}; - -Strophe.SASLMechanism.prototype = { - /** - * Function: test - * Checks if mechanism able to run. - * To disable a mechanism, make this return false; - * - * To disable plain authentication run - * > Strophe.SASLPlain.test = function() { - * > return false; - * > } - * - * See for a list of available mechanisms. - * - * Parameters: - * (Strophe.Connection) connection - Target Connection. - * - * Returns: - * (Boolean) If mechanism was able to run. - */ - /* jshint unused:false */ - test: function(connection) { - return true; - }, - /* jshint unused:true */ - - /** PrivateFunction: onStart - * Called before starting mechanism on some connection. - * - * Parameters: - * (Strophe.Connection) connection - Target Connection. - */ - onStart: function(connection) - { - this._connection = connection; - }, - - /** PrivateFunction: onChallenge - * Called by protocol implementation on incoming challenge. If client is - * first (isClientFirst == true) challenge will be null on the first call. - * - * Parameters: - * (Strophe.Connection) connection - Target Connection. - * (String) challenge - current challenge to handle. - * - * Returns: - * (String) Mechanism response. - */ - /* jshint unused:false */ - onChallenge: function(connection, challenge) { - throw new Error("You should implement challenge handling!"); - }, - /* jshint unused:true */ - - /** PrivateFunction: onFailure - * Protocol informs mechanism implementation about SASL failure. - */ - onFailure: function() { - this._connection = null; - }, - - /** PrivateFunction: onSuccess - * Protocol informs mechanism implementation about SASL success. - */ - onSuccess: function() { - this._connection = null; - } -}; - - /** Constants: SASL mechanisms - * Available authentication mechanisms - * - * Strophe.SASLAnonymous - SASL Anonymous authentication. - * Strophe.SASLPlain - SASL Plain authentication. - * Strophe.SASLMD5 - SASL Digest-MD5 authentication - * Strophe.SASLSHA1 - SASL SCRAM-SHA1 authentication - */ - -// Building SASL callbacks - -/** PrivateConstructor: SASLAnonymous - * SASL Anonymous authentication. - */ -Strophe.SASLAnonymous = function() {}; + Strophe.SASLPlain.test = function(connection) { + return connection.authcid !== null; + }; -Strophe.SASLAnonymous.prototype = new Strophe.SASLMechanism("ANONYMOUS", false, 10); + Strophe.SASLPlain.prototype.onChallenge = function(connection) { + var auth_str = connection.authzid; + auth_str = auth_str + "\u0000"; + auth_str = auth_str + connection.authcid; + auth_str = auth_str + "\u0000"; + auth_str = auth_str + connection.pass; + return auth_str; + }; -Strophe.SASLAnonymous.test = function(connection) { - return connection.authcid === null; -}; + Strophe.Connection.prototype.mechanisms[Strophe.SASLPlain.prototype.name] = Strophe.SASLPlain; -Strophe.Connection.prototype.mechanisms[Strophe.SASLAnonymous.prototype.name] = Strophe.SASLAnonymous; + /** PrivateConstructor: SASLSHA1 + * SASL SCRAM SHA 1 authentication. + */ + Strophe.SASLSHA1 = function() {}; -/** PrivateConstructor: SASLPlain - * SASL Plain authentication. - */ -Strophe.SASLPlain = function() {}; + /* TEST: + * This is a simple example of a SCRAM-SHA-1 authentication exchange + * when the client doesn't support channel bindings (username 'user' and + * password 'pencil' are used): + * + * C: n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL + * S: r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92, + * i=4096 + * C: c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j, + * p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts= + * S: v=rmF9pqV8S7suAoZWja4dJRkFsKQ= + * + */ -Strophe.SASLPlain.prototype = new Strophe.SASLMechanism("PLAIN", true, 20); + Strophe.SASLSHA1.prototype = new Strophe.SASLMechanism("SCRAM-SHA-1", true, 40); -Strophe.SASLPlain.test = function(connection) { - return connection.authcid !== null; -}; + Strophe.SASLSHA1.test = function(connection) { + return connection.authcid !== null; + }; -Strophe.SASLPlain.prototype.onChallenge = function(connection) { - var auth_str = connection.authzid; - auth_str = auth_str + "\u0000"; - auth_str = auth_str + connection.authcid; - auth_str = auth_str + "\u0000"; - auth_str = auth_str + connection.pass; - return auth_str; -}; + Strophe.SASLSHA1.prototype.onChallenge = function(connection, challenge, test_cnonce) { + var cnonce = test_cnonce || MD5.hexdigest(Math.random() * 1234567890); + + var auth_str = "n=" + connection.authcid; + auth_str += ",r="; + auth_str += cnonce; + + connection._sasl_data.cnonce = cnonce; + connection._sasl_data["client-first-message-bare"] = auth_str; + + auth_str = "n,," + auth_str; + + this.onChallenge = function (connection, challenge) + { + var nonce, salt, iter, Hi, U, U_old, i, k; + var clientKey, serverKey, clientSignature; + var responseText = "c=biws,"; + var authMessage = connection._sasl_data["client-first-message-bare"] + "," + + challenge + ","; + var cnonce = connection._sasl_data.cnonce; + var attribMatch = /([a-z]+)=([^,]+)(,|$)/; + + while (challenge.match(attribMatch)) { + var matches = challenge.match(attribMatch); + challenge = challenge.replace(matches[0], ""); + switch (matches[1]) { + case "r": + nonce = matches[2]; + break; + case "s": + salt = matches[2]; + break; + case "i": + iter = matches[2]; + break; + } + } -Strophe.Connection.prototype.mechanisms[Strophe.SASLPlain.prototype.name] = Strophe.SASLPlain; + if (nonce.substr(0, cnonce.length) !== cnonce) { + connection._sasl_data = {}; + return connection._sasl_failure_cb(); + } -/** PrivateConstructor: SASLSHA1 - * SASL SCRAM SHA 1 authentication. - */ -Strophe.SASLSHA1 = function() {}; + responseText += "r=" + nonce; + authMessage += responseText; -/* TEST: - * This is a simple example of a SCRAM-SHA-1 authentication exchange - * when the client doesn't support channel bindings (username 'user' and - * password 'pencil' are used): - * - * C: n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL - * S: r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92, - * i=4096 - * C: c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j, - * p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts= - * S: v=rmF9pqV8S7suAoZWja4dJRkFsKQ= - * - */ + salt = Base64.decode(salt); + salt += "\x00\x00\x00\x01"; -Strophe.SASLSHA1.prototype = new Strophe.SASLMechanism("SCRAM-SHA-1", true, 40); - -Strophe.SASLSHA1.test = function(connection) { - return connection.authcid !== null; -}; - -Strophe.SASLSHA1.prototype.onChallenge = function(connection, challenge, test_cnonce) { - var cnonce = test_cnonce || MD5.hexdigest(Math.random() * 1234567890); - - var auth_str = "n=" + connection.authcid; - auth_str += ",r="; - auth_str += cnonce; - - connection._sasl_data.cnonce = cnonce; - connection._sasl_data["client-first-message-bare"] = auth_str; - - auth_str = "n,," + auth_str; - - this.onChallenge = function (connection, challenge) - { - var nonce, salt, iter, Hi, U, U_old, i, k; - var clientKey, serverKey, clientSignature; - var responseText = "c=biws,"; - var authMessage = connection._sasl_data["client-first-message-bare"] + "," + - challenge + ","; - var cnonce = connection._sasl_data.cnonce; - var attribMatch = /([a-z]+)=([^,]+)(,|$)/; - - while (challenge.match(attribMatch)) { - var matches = challenge.match(attribMatch); - challenge = challenge.replace(matches[0], ""); - switch (matches[1]) { - case "r": - nonce = matches[2]; - break; - case "s": - salt = matches[2]; - break; - case "i": - iter = matches[2]; - break; - } - } + Hi = U_old = SHA1.core_hmac_sha1(connection.pass, salt); + for (i = 1; i < iter; i++) { + U = SHA1.core_hmac_sha1(connection.pass, SHA1.binb2str(U_old)); + for (k = 0; k < 5; k++) { + Hi[k] ^= U[k]; + } + U_old = U; + } + Hi = SHA1.binb2str(Hi); - if (nonce.substr(0, cnonce.length) !== cnonce) { - connection._sasl_data = {}; - return connection._sasl_failure_cb(); - } + clientKey = SHA1.core_hmac_sha1(Hi, "Client Key"); + serverKey = SHA1.str_hmac_sha1(Hi, "Server Key"); + clientSignature = SHA1.core_hmac_sha1(SHA1.str_sha1(SHA1.binb2str(clientKey)), authMessage); + connection._sasl_data["server-signature"] = SHA1.b64_hmac_sha1(serverKey, authMessage); - responseText += "r=" + nonce; - authMessage += responseText; + for (k = 0; k < 5; k++) { + clientKey[k] ^= clientSignature[k]; + } - salt = Base64.decode(salt); - salt += "\x00\x00\x00\x01"; + responseText += ",p=" + Base64.encode(SHA1.binb2str(clientKey)); - Hi = U_old = core_hmac_sha1(connection.pass, salt); - for (i = 1; i < iter; i++) { - U = core_hmac_sha1(connection.pass, binb2str(U_old)); - for (k = 0; k < 5; k++) { - Hi[k] ^= U[k]; - } - U_old = U; - } - Hi = binb2str(Hi); + return responseText; + }.bind(this); - clientKey = core_hmac_sha1(Hi, "Client Key"); - serverKey = str_hmac_sha1(Hi, "Server Key"); - clientSignature = core_hmac_sha1(str_sha1(binb2str(clientKey)), authMessage); - connection._sasl_data["server-signature"] = b64_hmac_sha1(serverKey, authMessage); + return auth_str; + }; - for (k = 0; k < 5; k++) { - clientKey[k] ^= clientSignature[k]; - } + Strophe.Connection.prototype.mechanisms[Strophe.SASLSHA1.prototype.name] = Strophe.SASLSHA1; - responseText += ",p=" + Base64.encode(binb2str(clientKey)); + /** PrivateConstructor: SASLMD5 + * SASL DIGEST MD5 authentication. + */ + Strophe.SASLMD5 = function() {}; - return responseText; - }.bind(this); + Strophe.SASLMD5.prototype = new Strophe.SASLMechanism("DIGEST-MD5", false, 30); - return auth_str; -}; + Strophe.SASLMD5.test = function(connection) { + return connection.authcid !== null; + }; -Strophe.Connection.prototype.mechanisms[Strophe.SASLSHA1.prototype.name] = Strophe.SASLSHA1; + /** PrivateFunction: _quote + * _Private_ utility function to backslash escape and quote strings. + * + * Parameters: + * (String) str - The string to be quoted. + * + * Returns: + * quoted string + */ + Strophe.SASLMD5.prototype._quote = function (str) + { + return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; + //" end string workaround for emacs + }; + + + Strophe.SASLMD5.prototype.onChallenge = function(connection, challenge, test_cnonce) { + var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/; + var cnonce = test_cnonce || MD5.hexdigest("" + (Math.random() * 1234567890)); + var realm = ""; + var host = null; + var nonce = ""; + var qop = ""; + var matches; + + 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; + } + } -/** PrivateConstructor: SASLMD5 - * SASL DIGEST MD5 authentication. - */ -Strophe.SASLMD5 = function() {}; + var digest_uri = connection.servtype + "/" + connection.domain; + if (host !== null) { + digest_uri = digest_uri + "/" + host; + } -Strophe.SASLMD5.prototype = new Strophe.SASLMechanism("DIGEST-MD5", false, 30); + var A1 = MD5.hash(connection.authcid + + ":" + realm + ":" + this._connection.pass) + + ":" + nonce + ":" + cnonce; + var A2 = 'AUTHENTICATE:' + digest_uri; + + var responseText = ""; + responseText += 'charset=utf-8,'; + responseText += 'username=' + + this._quote(connection.authcid) + ','; + responseText += 'realm=' + this._quote(realm) + ','; + responseText += 'nonce=' + this._quote(nonce) + ','; + responseText += 'nc=00000001,'; + responseText += 'cnonce=' + this._quote(cnonce) + ','; + responseText += 'digest-uri=' + this._quote(digest_uri) + ','; + responseText += 'response=' + MD5.hexdigest(MD5.hexdigest(A1) + ":" + + nonce + ":00000001:" + + cnonce + ":auth:" + + MD5.hexdigest(A2)) + ","; + responseText += 'qop=auth'; + + this.onChallenge = function () { + return ""; + }.bind(this); + + return responseText; + }; -Strophe.SASLMD5.test = function(connection) { - return connection.authcid !== null; -}; + Strophe.Connection.prototype.mechanisms[Strophe.SASLMD5.prototype.name] = Strophe.SASLMD5; -/** PrivateFunction: _quote - * _Private_ utility function to backslash escape and quote strings. - * - * Parameters: - * (String) str - The string to be quoted. - * - * Returns: - * quoted string - */ -Strophe.SASLMD5.prototype._quote = function (str) - { - return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; - //" end string workaround for emacs - }; - - -Strophe.SASLMD5.prototype.onChallenge = function(connection, challenge, test_cnonce) { - var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/; - var cnonce = test_cnonce || MD5.hexdigest("" + (Math.random() * 1234567890)); - var realm = ""; - var host = null; - var nonce = ""; - var qop = ""; - var matches; - - 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 = connection.servtype + "/" + connection.domain; - if (host !== null) { - digest_uri = digest_uri + "/" + host; - } - - var A1 = MD5.hash(connection.authcid + - ":" + realm + ":" + this._connection.pass) + - ":" + nonce + ":" + cnonce; - var A2 = 'AUTHENTICATE:' + digest_uri; - - var responseText = ""; - responseText += 'charset=utf-8,'; - responseText += 'username=' + - this._quote(connection.authcid) + ','; - responseText += 'realm=' + this._quote(realm) + ','; - responseText += 'nonce=' + this._quote(nonce) + ','; - responseText += 'nc=00000001,'; - responseText += 'cnonce=' + this._quote(cnonce) + ','; - responseText += 'digest-uri=' + this._quote(digest_uri) + ','; - responseText += 'response=' + MD5.hexdigest(MD5.hexdigest(A1) + ":" + - nonce + ":00000001:" + - cnonce + ":auth:" + - MD5.hexdigest(A2)) + ","; - responseText += 'qop=auth'; - - this.onChallenge = function () - { - return ""; - }.bind(this); - - return responseText; -}; - -Strophe.Connection.prototype.mechanisms[Strophe.SASLMD5.prototype.name] = Strophe.SASLMD5; - -})(function () { - window.Strophe = arguments[0]; - window.$build = arguments[1]; - window.$msg = arguments[2]; - window.$iq = arguments[3]; - window.$pres = arguments[4]; -}); + return { + Strophe: Strophe, + $build: $build, + $msg: $msg, + $iq: $iq, + $pres: $pres + }; +})); diff --git a/src/md5.js b/src/md5.js index b752b449..aaa5c03d 100644 --- a/src/md5.js +++ b/src/md5.js @@ -11,11 +11,21 @@ * Everything that isn't used by Strophe has been stripped here! */ -var MD5 = (function () { +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(function () { + return factory(); + }); + } else { + // Browser globals + root.MD5 = factory(); + } +}(this, function (b) { /* - * Add integers, wrapping at 2^32. This uses 16-bit operations internally - * to work around bugs in some JS interpreters. - */ + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ var safe_add = function (x, y) { var lsw = (x & 0xFFFF) + (y & 0xFFFF); var msw = (x >> 16) + (y >> 16) + (lsw >> 16); @@ -23,15 +33,15 @@ var MD5 = (function () { }; /* - * Bitwise rotate a 32-bit number to the left. - */ + * Bitwise rotate a 32-bit number to the left. + */ var bit_rol = function (num, cnt) { return (num << cnt) | (num >>> (32 - cnt)); }; /* - * Convert a string to an array of little-endian words - */ + * Convert a string to an array of little-endian words + */ var str2binl = function (str) { var bin = []; for(var i = 0; i < str.length * 8; i += 8) @@ -42,8 +52,8 @@ var MD5 = (function () { }; /* - * Convert an array of little-endian words to a string - */ + * Convert an array of little-endian words to a string + */ var binl2str = function (bin) { var str = ""; for(var i = 0; i < bin.length * 32; i += 8) @@ -54,8 +64,8 @@ var MD5 = (function () { }; /* - * Convert an array of little-endian words to a hex string. - */ + * Convert an array of little-endian words to a hex string. + */ var binl2hex = function (binarray) { var hex_tab = "0123456789abcdef"; var str = ""; @@ -68,8 +78,8 @@ var MD5 = (function () { }; /* - * These functions implement the four basic operations the algorithm uses. - */ + * These functions implement the four basic operations the algorithm uses. + */ var md5_cmn = function (q, a, b, x, s, t) { return safe_add(bit_rol(safe_add(safe_add(a, q),safe_add(x, t)), s),b); }; @@ -91,8 +101,8 @@ var MD5 = (function () { }; /* - * Calculate the MD5 of an array of little-endian words, and a bit length - */ + * Calculate the MD5 of an array of little-endian words, and a bit length + */ var core_md5 = function (x, len) { /* append padding */ x[len >> 5] |= 0x80 << ((len) % 32); @@ -187,13 +197,12 @@ var MD5 = (function () { return [a, b, c, d]; }; - var obj = { /* - * These are the functions you'll usually want to call. - * They take string arguments and return either hex or base-64 encoded - * strings. - */ + * These are the functions you'll usually want to call. + * They take string arguments and return either hex or base-64 encoded + * strings. + */ hexdigest: function (s) { return binl2hex(core_md5(str2binl(s), s.length * 8)); }, @@ -202,6 +211,5 @@ var MD5 = (function () { return binl2str(core_md5(str2binl(s), s.length * 8)); } }; - return obj; -})(); +})); diff --git a/src/polyfill.js b/src/polyfill.js new file mode 100644 index 00000000..7dcbd63c --- /dev/null +++ b/src/polyfill.js @@ -0,0 +1,77 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(factory); + } else { + // Browser globals + factory(); + } +}(this, function () { + + /** PrivateFunction: Function.prototype.bind + * Bind a function to an instance. This is a polyfill for the ES5 bind method. + * which already exists in more modern browsers, but we provide it to support + * those that don't. + * + * Parameters: + * (Object) obj - The object that will become 'this' in the bound function. + * (Object) argN - An option argument that will be prepended to the + * arguments given for the function call + * + * Returns: + * The bound function. + */ + if (!Function.prototype.bind) { + Function.prototype.bind = function (obj /*, arg1, arg2, ... */) { + var func = this; + var _slice = Array.prototype.slice; + var _concat = Array.prototype.concat; + var _args = _slice.call(arguments, 1); + return function () { + return func.apply(obj ? obj : this, + _concat.call(_args, + _slice.call(arguments, 0))); + }; + }; + } + + /** PrivateFunction: Array.isArray + * This is a polyfill for the ES5 Array.isArray method. + */ + if (!Array.isArray) { + Array.isArray = function(arg) { + return Object.prototype.toString.call(arg) === '[object Array]'; + }; + } + + /** 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; + }; + } +})); diff --git a/src/sha1.js b/src/sha1.js index e629c483..d9c1f1e0 100644 --- a/src/sha1.js +++ b/src/sha1.js @@ -7,170 +7,167 @@ * See http://pajhome.org.uk/crypt/md5 for details. */ -/* Some functions and variables have been stripped for use with Strophe */ - -/* - * These are the functions you'll usually want to call - * They take string arguments and return either hex or base-64 encoded strings - */ -function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * 8));} -function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * 8));} -function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));} -function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));} - -/* - * Calculate the SHA-1 of an array of big-endian words, and a bit length - */ -function core_sha1(x, len) -{ - /* append padding */ - x[len >> 5] |= 0x80 << (24 - len % 32); - x[((len + 64 >> 9) << 4) + 15] = len; - - var w = new Array(80); - var a = 1732584193; - var b = -271733879; - var c = -1732584194; - var d = 271733878; - var e = -1009589776; +/* jshint undef: true, unused: true:, noarg: true, latedef: true */ +/* global define */ - var i, j, t, olda, oldb, oldc, oldd, olde; - for (i = 0; i < x.length; i += 16) - { - olda = a; - oldb = b; - oldc = c; - oldd = d; - olde = e; +/* Some functions and variables have been stripped for use with Strophe */ - for (j = 0; j < 80; j++) - { - if (j < 16) { w[j] = x[i + j]; } - else { w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); } - t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), - safe_add(safe_add(e, w[j]), sha1_kt(j))); - e = d; - d = c; - c = rol(b, 30); - b = a; - a = t; +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(function () { + return factory(); + }); + } else { + // Browser globals + root.SHA1 = factory(); + } +}(this, function () { + + /* + * Calculate the SHA-1 of an array of big-endian words, and a bit length + */ + function core_sha1(x, len) { + x[len >> 5] |= 0x80 << (24 - len % 32); /* append padding */ + x[((len + 64 >> 9) << 4) + 15] = len; + + var w = new Array(80); + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + var e = -1009589776; + + var i, j, t, olda, oldb, oldc, oldd, olde; + for (i = 0; i < x.length; i += 16) { + olda = a; + oldb = b; + oldc = c; + oldd = d; + olde = e; + for (j = 0; j < 80; j++) { + if (j < 16) { w[j] = x[i + j]; } + else { w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); } + t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), + safe_add(safe_add(e, w[j]), sha1_kt(j))); + e = d; + d = c; + c = rol(b, 30); + b = a; + a = t; + } + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + e = safe_add(e, olde); + } + return [a, b, c, d, e]; } - a = safe_add(a, olda); - b = safe_add(b, oldb); - c = safe_add(c, oldc); - d = safe_add(d, oldd); - e = safe_add(e, olde); - } - return [a, b, c, d, e]; -} - -/* - * Perform the appropriate triplet combination function for the current - * iteration - */ -function sha1_ft(t, b, c, d) -{ - if (t < 20) { return (b & c) | ((~b) & d); } - if (t < 40) { return b ^ c ^ d; } - if (t < 60) { return (b & c) | (b & d) | (c & d); } - return b ^ c ^ d; -} - -/* - * Determine the appropriate additive constant for the current iteration - */ -function sha1_kt(t) -{ - return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : - (t < 60) ? -1894007588 : -899497514; -} - -/* - * Calculate the HMAC-SHA1 of a key and some data - */ -function core_hmac_sha1(key, data) -{ - var bkey = str2binb(key); - if (bkey.length > 16) { bkey = core_sha1(bkey, key.length * 8); } + /* + * Perform the appropriate triplet combination function for the current + * iteration + */ + function sha1_ft(t, b, c, d) { + if (t < 20) { return (b & c) | ((~b) & d); } + if (t < 40) { return b ^ c ^ d; } + if (t < 60) { return (b & c) | (b & d) | (c & d); } + return b ^ c ^ d; + } - var ipad = new Array(16), opad = new Array(16); - for (var i = 0; i < 16; i++) - { - ipad[i] = bkey[i] ^ 0x36363636; - opad[i] = bkey[i] ^ 0x5C5C5C5C; - } + /* + * Determine the appropriate additive constant for the current iteration + */ + function sha1_kt(t) { + return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : + (t < 60) ? -1894007588 : -899497514; + } - var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * 8); - return core_sha1(opad.concat(hash), 512 + 160); -} + /* + * Calculate the HMAC-SHA1 of a key and some data + */ + function core_hmac_sha1(key, data) { + var bkey = str2binb(key); + if (bkey.length > 16) { bkey = core_sha1(bkey, key.length * 8); } + var ipad = new Array(16), opad = new Array(16); + for (var i = 0; i < 16; i++) { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * 8); + return core_sha1(opad.concat(hash), 512 + 160); + } -/* - * Add integers, wrapping at 2^32. This uses 16-bit operations internally - * to work around bugs in some JS interpreters. - */ -function safe_add(x, y) -{ - var lsw = (x & 0xFFFF) + (y & 0xFFFF); - var msw = (x >> 16) + (y >> 16) + (lsw >> 16); - return (msw << 16) | (lsw & 0xFFFF); -} + /* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ + function safe_add(x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + } -/* - * Bitwise rotate a 32-bit number to the left. - */ -function rol(num, cnt) -{ - return (num << cnt) | (num >>> (32 - cnt)); -} + /* + * Bitwise rotate a 32-bit number to the left. + */ + function rol(num, cnt) { + return (num << cnt) | (num >>> (32 - cnt)); + } -/* - * Convert an 8-bit or 16-bit string to an array of big-endian words - * In 8-bit function, characters >255 have their hi-byte silently ignored. - */ -function str2binb(str) -{ - var bin = []; - var mask = 255; - for (var i = 0; i < str.length * 8; i += 8) - { - bin[i>>5] |= (str.charCodeAt(i / 8) & mask) << (24 - i%32); - } - return bin; -} + /* + * Convert an 8-bit or 16-bit string to an array of big-endian words + * In 8-bit function, characters >255 have their hi-byte silently ignored. + */ + function str2binb(str) { + var bin = []; + var mask = 255; + for (var i = 0; i < str.length * 8; i += 8) { + bin[i>>5] |= (str.charCodeAt(i / 8) & mask) << (24 - i%32); + } + return bin; + } -/* - * Convert an array of big-endian words to a string - */ -function binb2str(bin) -{ - var str = ""; - var mask = 255; - for (var i = 0; i < bin.length * 32; i += 8) - { - str += String.fromCharCode((bin[i>>5] >>> (24 - i%32)) & mask); - } - return str; -} + /* + * Convert an array of big-endian words to a string + */ + function binb2str(bin) { + var str = ""; + var mask = 255; + for (var i = 0; i < bin.length * 32; i += 8) { + str += String.fromCharCode((bin[i>>5] >>> (24 - i%32)) & mask); + } + return str; + } -/* - * Convert an array of big-endian words to a base-64 string - */ -function binb2b64(binarray) -{ - var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - var str = ""; - var triplet, j; - for (var i = 0; i < binarray.length * 4; i += 3) - { - triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16) | - (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) | - ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF); - for (j = 0; j < 4; j++) - { - if (i * 8 + j * 6 > binarray.length * 32) { str += "="; } - else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); } + /* + * Convert an array of big-endian words to a base-64 string + */ + function binb2b64(binarray) { + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var str = ""; + var triplet, j; + for (var i = 0; i < binarray.length * 4; i += 3) { + triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16) | + (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) | + ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF); + for (j = 0; j < 4; j++) { + if (i * 8 + j * 6 > binarray.length * 32) { str += "="; } + else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); } + } + } + return str; } - } - return str; -} + + /* + * These are the functions you'll usually want to call + * They take string arguments and return either hex or base-64 encoded strings + */ + return { + b64_sha1: function (s) { return binb2b64(core_sha1(str2binb(s),s.length * 8)); }, + str_sha1: function (s) { return binb2str(core_sha1(str2binb(s),s.length * 8)); }, + b64_hmac_sha1: function (key, data){ return binb2b64(core_hmac_sha1(key, data)); }, + str_hmac_sha1: function (key, data){ return binb2str(core_hmac_sha1(key, data)); }, + }; +})); diff --git a/src/websocket.js b/src/websocket.js index 027dd0ce..e94ba4f0 100644 --- a/src/websocket.js +++ b/src/websocket.js @@ -6,525 +6,538 @@ */ /* jshint undef: true, unused: true:, noarg: true, latedef: true */ -/*global document, window, clearTimeout, WebSocket, - DOMParser, Strophe, $build */ - -/** Class: Strophe.WebSocket - * _Private_ helper class that handles WebSocket Connections - * - * The Strophe.WebSocket class is used internally by Strophe.Connection - * to encapsulate WebSocket sessions. It is not meant to be used from user's code. - */ - -/** File: websocket.js - * A JavaScript library to enable XMPP over Websocket in Strophejs. - * - * This file implements XMPP over WebSockets for Strophejs. - * If a Connection is established with a Websocket url (ws://...) - * Strophe will use WebSockets. - * For more information on XMPP-over WebSocket see this RFC draft: - * http://tools.ietf.org/html/draft-ietf-xmpp-websocket-00 - * - * WebSocket support implemented by Andreas Guth (andreas.guth@rwth-aachen.de) - */ - -/** PrivateConstructor: Strophe.Websocket - * Create and initialize a Strophe.WebSocket object. - * Currently only sets the connection Object. - * - * Parameters: - * (Strophe.Connection) connection - The Strophe.Connection that will use WebSockets. - * - * Returns: - * A new Strophe.WebSocket object. - */ -Strophe.Websocket = function(connection) { - this._conn = connection; - this.strip = "stream:stream"; - - var service = connection.service; - if (service.indexOf("ws:") !== 0 && service.indexOf("wss:") !== 0) { - // If the service is not an absolute URL, assume it is a path and put the absolute - // URL together from options, current URL and the path. - var new_service = ""; - - if (connection.options.protocol === "ws" && window.location.protocol !== "https:") { - new_service += "ws"; - } else { - new_service += "wss"; - } - - new_service += "://" + window.location.host; +/* global define, document, window, clearTimeout, WebSocket, DOMParser, Strophe, $build */ - if (service.indexOf("/") !== 0) { - new_service += window.location.pathname + service; - } else { - new_service += service; - } - - connection.service = new_service; +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['strophe-core'], function (wrapper) { + return factory(wrapper.Strophe); + }); + } else { + // Browser globals + return factory(Strophe); } -}; +}(this, function (Strophe) { -Strophe.Websocket.prototype = { - /** PrivateFunction: _buildStream - * _Private_ helper function to generate the start tag for WebSockets + /** Class: Strophe.WebSocket + * _Private_ helper class that handles WebSocket Connections * - * Returns: - * A Strophe.Builder with a element. + * The Strophe.WebSocket class is used internally by Strophe.Connection + * to encapsulate WebSocket sessions. It is not meant to be used from user's code. */ - _buildStream: function () - { - return $build("stream:stream", { - "to": this._conn.domain, - "xmlns": Strophe.NS.CLIENT, - "xmlns:stream": Strophe.NS.STREAM, - "version": '1.0' - }); - }, - /** PrivateFunction: _check_streamerror - * _Private_ checks a message for stream:error + /** File: websocket.js + * A JavaScript library to enable XMPP over Websocket in Strophejs. * - * Parameters: - * (Strophe.Request) bodyWrap - The received stanza. - * connectstatus - The ConnectStatus that will be set on error. - * Returns: - * true if there was a streamerror, false otherwise. - */ - _check_streamerror: function (bodyWrap, connectstatus) { - var errors = bodyWrap.getElementsByTagNameNS(Strophe.NS.STREAM, "error"); - if (errors.length === 0) { - return false; - } - var error = errors[0]; - - var condition = ""; - var text = ""; - - var ns = "urn:ietf:params:xml:ns:xmpp-streams"; - for (var i = 0; i < error.childNodes.length; i++) { - var e = error.childNodes[i]; - if (e.getAttribute("xmlns") !== ns) { - break; - } if (e.nodeName === "text") { - text = e.textContent; - } else { - condition = e.nodeName; - } - } - - var errorString = "WebSocket stream error: "; - - if (condition) { - errorString += condition; - } else { - errorString += "unknown"; - } - - if (text) { - errorString += " - " + condition; - } - - Strophe.error(errorString); - - // close the connection on stream_error - this._conn._changeConnectStatus(connectstatus, condition); - this._conn._doDisconnect(); - return true; - }, - - /** PrivateFunction: _reset - * Reset the connection. + * This file implements XMPP over WebSockets for Strophejs. + * If a Connection is established with a Websocket url (ws://...) + * Strophe will use WebSockets. + * For more information on XMPP-over WebSocket see this RFC draft: + * http://tools.ietf.org/html/draft-ietf-xmpp-websocket-00 * - * This function is called by the reset function of the Strophe Connection. - * Is not needed by WebSockets. + * WebSocket support implemented by Andreas Guth (andreas.guth@rwth-aachen.de) */ - _reset: function () - { - return; - }, - /** PrivateFunction: _connect - * _Private_ function called by Strophe.Connection.connect - * - * Creates a WebSocket for a connection and assigns Callbacks to it. - * Does nothing if there already is a WebSocket. - */ - _connect: function () { - // Ensure that there is no open WebSocket from a previous Connection. - this._closeSocket(); - - // Create the new WobSocket - this.socket = new WebSocket(this._conn.service, "xmpp"); - this.socket.onopen = this._onOpen.bind(this); - this.socket.onerror = this._onError.bind(this); - this.socket.onclose = this._onClose.bind(this); - this.socket.onmessage = this._connect_cb_wrapper.bind(this); - }, - - /** PrivateFunction: _connect_cb - * _Private_ function called by Strophe.Connection._connect_cb - * - * checks for stream:error + /** PrivateConstructor: Strophe.Websocket + * Create and initialize a Strophe.WebSocket object. + * Currently only sets the connection Object. * * Parameters: - * (Strophe.Request) bodyWrap - The received stanza. - */ - _connect_cb: function(bodyWrap) { - var error = this._check_streamerror(bodyWrap, Strophe.Status.CONNFAIL); - if (error) { - return Strophe.Status.CONNFAIL; - } - }, - - /** PrivateFunction: _handleStreamStart - * _Private_ function that checks the opening stream:stream tag for errors. + * (Strophe.Connection) connection - The Strophe.Connection that will use WebSockets. * - * Disconnects if there is an error and returns false, true otherwise. - * - * Parameters: - * (Node) message - Stanza containing the stream:stream. + * Returns: + * A new Strophe.WebSocket object. */ - _handleStreamStart: function(message) { - var error = false; - // Check for errors in the stream:stream tag - var ns = message.getAttribute("xmlns"); - if (typeof ns !== "string") { - error = "Missing xmlns in stream:stream"; - } else if (ns !== Strophe.NS.CLIENT) { - error = "Wrong xmlns in stream:stream: " + ns; - } + Strophe.Websocket = function(connection) { + this._conn = connection; + this.strip = "stream:stream"; + + var service = connection.service; + if (service.indexOf("ws:") !== 0 && service.indexOf("wss:") !== 0) { + // If the service is not an absolute URL, assume it is a path and put the absolute + // URL together from options, current URL and the path. + var new_service = ""; + + if (connection.options.protocol === "ws" && window.location.protocol !== "https:") { + new_service += "ws"; + } else { + new_service += "wss"; + } - var ns_stream = message.namespaceURI; - if (typeof ns_stream !== "string") { - error = "Missing xmlns:stream in stream:stream"; - } else if (ns_stream !== Strophe.NS.STREAM) { - error = "Wrong xmlns:stream in stream:stream: " + ns_stream; - } + new_service += "://" + window.location.host; - var ver = message.getAttribute("version"); - if (typeof ver !== "string") { - error = "Missing version in stream:stream"; - } else if (ver !== "1.0") { - error = "Wrong version in stream:stream: " + ver; - } + if (service.indexOf("/") !== 0) { + new_service += window.location.pathname + service; + } else { + new_service += service; + } - if (error) { - this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, error); - this._conn._doDisconnect(); - return false; + connection.service = new_service; } + }; + + Strophe.Websocket.prototype = { + /** PrivateFunction: _buildStream + * _Private_ helper function to generate the start tag for WebSockets + * + * Returns: + * A Strophe.Builder with a element. + */ + _buildStream: function () + { + return $build("stream:stream", { + "to": this._conn.domain, + "xmlns": Strophe.NS.CLIENT, + "xmlns:stream": Strophe.NS.STREAM, + "version": '1.0' + }); + }, + + /** PrivateFunction: _check_streamerror + * _Private_ checks a message for stream:error + * + * Parameters: + * (Strophe.Request) bodyWrap - The received stanza. + * connectstatus - The ConnectStatus that will be set on error. + * Returns: + * true if there was a streamerror, false otherwise. + */ + _check_streamerror: function (bodyWrap, connectstatus) { + var errors = bodyWrap.getElementsByTagNameNS(Strophe.NS.STREAM, "error"); + if (errors.length === 0) { + return false; + } + var error = errors[0]; + + var condition = ""; + var text = ""; + + var ns = "urn:ietf:params:xml:ns:xmpp-streams"; + for (var i = 0; i < error.childNodes.length; i++) { + var e = error.childNodes[i]; + if (e.getAttribute("xmlns") !== ns) { + break; + } if (e.nodeName === "text") { + text = e.textContent; + } else { + condition = e.nodeName; + } + } - return true; - }, - - /** PrivateFunction: _connect_cb_wrapper - * _Private_ function that handles the first connection messages. - * - * On receiving an opening stream tag this callback replaces itself with the real - * message handler. On receiving a stream error the connection is terminated. - */ - _connect_cb_wrapper: function(message) { - if (message.data.indexOf("\s*)*/, ""); - if (data === '') return; - - //Make the initial stream:stream selfclosing to parse it without a SAX parser. - data = message.data.replace(//, ""); + var errorString = "WebSocket stream error: "; - var streamStart = new DOMParser().parseFromString(data, "text/xml").documentElement; - this._conn.xmlInput(streamStart); - this._conn.rawInput(message.data); + if (condition) { + errorString += condition; + } else { + errorString += "unknown"; + } - //_handleStreamSteart will check for XML errors and disconnect on error - if (this._handleStreamStart(streamStart)) { + if (text) { + errorString += " - " + condition; + } - //_connect_cb will check for stream:error and disconnect on error - this._connect_cb(streamStart); + Strophe.error(errorString); - // ensure received stream:stream is NOT selfclosing and save it for following messages - this.streamStart = message.data.replace(/^$/, ""); - } - } else if (message.data === "") { - this._conn.rawInput(message.data); - this._conn.xmlInput(document.createElement("stream:stream")); - this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "Received closing stream"); + // close the connection on stream_error + this._conn._changeConnectStatus(connectstatus, condition); this._conn._doDisconnect(); + return true; + }, + + /** PrivateFunction: _reset + * Reset the connection. + * + * This function is called by the reset function of the Strophe Connection. + * Is not needed by WebSockets. + */ + _reset: function () + { return; - } else { - var string = this._streamWrap(message.data); - var elem = new DOMParser().parseFromString(string, "text/xml").documentElement; - this.socket.onmessage = this._onMessage.bind(this); - this._conn._connect_cb(elem, null, message.data); - } - }, + }, + + /** PrivateFunction: _connect + * _Private_ function called by Strophe.Connection.connect + * + * Creates a WebSocket for a connection and assigns Callbacks to it. + * Does nothing if there already is a WebSocket. + */ + _connect: function () { + // Ensure that there is no open WebSocket from a previous Connection. + this._closeSocket(); + + // Create the new WobSocket + this.socket = new WebSocket(this._conn.service, "xmpp"); + this.socket.onopen = this._onOpen.bind(this); + this.socket.onerror = this._onError.bind(this); + this.socket.onclose = this._onClose.bind(this); + this.socket.onmessage = this._connect_cb_wrapper.bind(this); + }, + + /** PrivateFunction: _connect_cb + * _Private_ function called by Strophe.Connection._connect_cb + * + * checks for stream:error + * + * Parameters: + * (Strophe.Request) bodyWrap - The received stanza. + */ + _connect_cb: function(bodyWrap) { + var error = this._check_streamerror(bodyWrap, Strophe.Status.CONNFAIL); + if (error) { + return Strophe.Status.CONNFAIL; + } + }, + + /** PrivateFunction: _handleStreamStart + * _Private_ function that checks the opening stream:stream tag for errors. + * + * Disconnects if there is an error and returns false, true otherwise. + * + * Parameters: + * (Node) message - Stanza containing the stream:stream. + */ + _handleStreamStart: function(message) { + var error = false; + // Check for errors in the stream:stream tag + var ns = message.getAttribute("xmlns"); + if (typeof ns !== "string") { + error = "Missing xmlns in stream:stream"; + } else if (ns !== Strophe.NS.CLIENT) { + error = "Wrong xmlns in stream:stream: " + ns; + } - /** PrivateFunction: _disconnect - * _Private_ function called by Strophe.Connection.disconnect - * - * Disconnects and sends a last stanza if one is given - * - * Parameters: - * (Request) pres - This stanza will be sent before disconnecting. - */ - _disconnect: function (pres) - { - if (this.socket.readyState !== WebSocket.CLOSED) { - if (pres) { - this._conn.send(pres); + var ns_stream = message.namespaceURI; + if (typeof ns_stream !== "string") { + error = "Missing xmlns:stream in stream:stream"; + } else if (ns_stream !== Strophe.NS.STREAM) { + error = "Wrong xmlns:stream in stream:stream: " + ns_stream; } - var close = ''; - this._conn.xmlOutput(document.createElement("stream:stream")); - this._conn.rawOutput(close); - try { - this.socket.send(close); - } catch (e) { - Strophe.info("Couldn't send closing stream tag."); + + var ver = message.getAttribute("version"); + if (typeof ver !== "string") { + error = "Missing version in stream:stream"; + } else if (ver !== "1.0") { + error = "Wrong version in stream:stream: " + ver; } - } - this._conn._doDisconnect(); - }, + if (error) { + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, error); + this._conn._doDisconnect(); + return false; + } - /** PrivateFunction: _doDisconnect - * _Private_ function to disconnect. - * - * Just closes the Socket for WebSockets - */ - _doDisconnect: function () - { - Strophe.info("WebSockets _doDisconnect was called"); - this._closeSocket(); - }, - - /** PrivateFunction _streamWrap - * _Private_ helper function to wrap a stanza in a tag. - * This is used so Strophe can process stanzas from WebSockets like BOSH - */ - _streamWrap: function (stanza) - { - return this.streamStart + stanza + ''; - }, + return true; + }, + /** PrivateFunction: _connect_cb_wrapper + * _Private_ function that handles the first connection messages. + * + * On receiving an opening stream tag this callback replaces itself with the real + * message handler. On receiving a stream error the connection is terminated. + */ + _connect_cb_wrapper: function(message) { + if (message.data.indexOf("\s*)*/, ""); + if (data === '') return; - /** PrivateFunction: _closeSocket - * _Private_ function to close the WebSocket. - * - * Closes the socket if it is still open and deletes it - */ - _closeSocket: function () - { - if (this.socket) { try { - this.socket.close(); - } catch (e) {} } - this.socket = null; - }, - - /** PrivateFunction: _emptyQueue - * _Private_ function to check if the message queue is empty. - * - * Returns: - * True, because WebSocket messages are send immediately after queueing. - */ - _emptyQueue: function () - { - return true; - }, + //Make the initial stream:stream selfclosing to parse it without a SAX parser. + data = message.data.replace(//, ""); - /** PrivateFunction: _onClose - * _Private_ function to handle websockets closing. - * - * Nothing to do here for WebSockets - */ - _onClose: function() { - if(this._conn.connected && !this._conn.disconnecting) { - Strophe.error("Websocket closed unexcectedly"); - this._conn._doDisconnect(); - } else { - Strophe.info("Websocket closed"); - } - }, + var streamStart = new DOMParser().parseFromString(data, "text/xml").documentElement; + this._conn.xmlInput(streamStart); + this._conn.rawInput(message.data); - /** PrivateFunction: _no_auth_received - * - * Called on stream start/restart when no stream:features - * has been received. - */ - _no_auth_received: function (_callback) - { - Strophe.error("Server did not send any auth methods"); - this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "Server did not send any auth methods"); - if (_callback) { - _callback = _callback.bind(this._conn); - _callback(); - } - this._conn._doDisconnect(); - }, + //_handleStreamSteart will check for XML errors and disconnect on error + if (this._handleStreamStart(streamStart)) { - /** PrivateFunction: _onDisconnectTimeout - * _Private_ timeout handler for handling non-graceful disconnection. - * - * This does nothing for WebSockets - */ - _onDisconnectTimeout: function () {}, + //_connect_cb will check for stream:error and disconnect on error + this._connect_cb(streamStart); - /** PrivateFunction: _onError - * _Private_ function to handle websockets errors. - * - * Parameters: - * (Object) error - The websocket error. - */ - _onError: function(error) { - Strophe.error("Websocket error " + error); - this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "The WebSocket connection could not be established was disconnected."); - this._disconnect(); - }, - - /** PrivateFunction: _onIdle - * _Private_ function called by Strophe.Connection._onIdle - * - * sends all queued stanzas - */ - _onIdle: function () { - var data = this._conn._data; - if (data.length > 0 && !this._conn.paused) { - for (var i = 0; i < data.length; i++) { - if (data[i] !== null) { - var stanza, rawStanza; - if (data[i] === "restart") { - stanza = this._buildStream(); - rawStanza = this._removeClosingTag(stanza); - stanza = stanza.tree(); - } else { - stanza = data[i]; - rawStanza = Strophe.serialize(stanza); - } - this._conn.xmlOutput(stanza); - this._conn.rawOutput(rawStanza); - this.socket.send(rawStanza); + // ensure received stream:stream is NOT selfclosing and save it for following messages + this.streamStart = message.data.replace(/^$/, ""); + } + } else if (message.data === "") { + this._conn.rawInput(message.data); + this._conn.xmlInput(document.createElement("stream:stream")); + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "Received closing stream"); + this._conn._doDisconnect(); + return; + } else { + var string = this._streamWrap(message.data); + var elem = new DOMParser().parseFromString(string, "text/xml").documentElement; + this.socket.onmessage = this._onMessage.bind(this); + this._conn._connect_cb(elem, null, message.data); + } + }, + + /** PrivateFunction: _disconnect + * _Private_ function called by Strophe.Connection.disconnect + * + * Disconnects and sends a last stanza if one is given + * + * Parameters: + * (Request) pres - This stanza will be sent before disconnecting. + */ + _disconnect: function (pres) + { + if (this.socket.readyState !== WebSocket.CLOSED) { + if (pres) { + this._conn.send(pres); + } + var close = ''; + this._conn.xmlOutput(document.createElement("stream:stream")); + this._conn.rawOutput(close); + try { + this.socket.send(close); + } catch (e) { + Strophe.info("Couldn't send closing stream tag."); } } - this._conn._data = []; - } - }, - /** PrivateFunction: _onMessage - * _Private_ function to handle websockets messages. - * - * This function parses each of the messages as if they are full documents. [TODO : We may actually want to use a SAX Push parser]. - * - * Since all XMPP traffic starts with "" - * The first stanza will always fail to be parsed... - * Addtionnaly, the seconds stanza will always be a with the stream NS defined in the previous stanza... so we need to 'force' the inclusion of the NS in this stanza! - * - * Parameters: - * (string) message - The websocket message. - */ - _onMessage: function(message) { - var elem, data; - // check for closing stream - if (message.data === "") { - var close = ""; - this._conn.rawInput(close); - this._conn.xmlInput(document.createElement("stream:stream")); - if (!this._conn.disconnecting) { + this._conn._doDisconnect(); + }, + + /** PrivateFunction: _doDisconnect + * _Private_ function to disconnect. + * + * Just closes the Socket for WebSockets + */ + _doDisconnect: function () + { + Strophe.info("WebSockets _doDisconnect was called"); + this._closeSocket(); + }, + + /** PrivateFunction _streamWrap + * _Private_ helper function to wrap a stanza in a tag. + * This is used so Strophe can process stanzas from WebSockets like BOSH + */ + _streamWrap: function (stanza) + { + return this.streamStart + stanza + ''; + }, + + + /** PrivateFunction: _closeSocket + * _Private_ function to close the WebSocket. + * + * Closes the socket if it is still open and deletes it + */ + _closeSocket: function () + { + if (this.socket) { try { + this.socket.close(); + } catch (e) {} } + this.socket = null; + }, + + /** PrivateFunction: _emptyQueue + * _Private_ function to check if the message queue is empty. + * + * Returns: + * True, because WebSocket messages are send immediately after queueing. + */ + _emptyQueue: function () + { + return true; + }, + + /** PrivateFunction: _onClose + * _Private_ function to handle websockets closing. + * + * Nothing to do here for WebSockets + */ + _onClose: function() { + if(this._conn.connected && !this._conn.disconnecting) { + Strophe.error("Websocket closed unexcectedly"); this._conn._doDisconnect(); + } else { + Strophe.info("Websocket closed"); } - return; - } else if (message.data.search("/, ""); - elem = new DOMParser().parseFromString(data, "text/xml").documentElement; - - if (!this._handleStreamStart(elem)) { + }, + + /** PrivateFunction: _no_auth_received + * + * Called on stream start/restart when no stream:features + * has been received. + */ + _no_auth_received: function (_callback) + { + Strophe.error("Server did not send any auth methods"); + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "Server did not send any auth methods"); + if (_callback) { + _callback = _callback.bind(this._conn); + _callback(); + } + this._conn._doDisconnect(); + }, + + /** PrivateFunction: _onDisconnectTimeout + * _Private_ timeout handler for handling non-graceful disconnection. + * + * This does nothing for WebSockets + */ + _onDisconnectTimeout: function () {}, + + /** PrivateFunction: _onError + * _Private_ function to handle websockets errors. + * + * Parameters: + * (Object) error - The websocket error. + */ + _onError: function(error) { + Strophe.error("Websocket error " + error); + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "The WebSocket connection could not be established was disconnected."); + this._disconnect(); + }, + + /** PrivateFunction: _onIdle + * _Private_ function called by Strophe.Connection._onIdle + * + * sends all queued stanzas + */ + _onIdle: function () { + var data = this._conn._data; + if (data.length > 0 && !this._conn.paused) { + for (var i = 0; i < data.length; i++) { + if (data[i] !== null) { + var stanza, rawStanza; + if (data[i] === "restart") { + stanza = this._buildStream(); + rawStanza = this._removeClosingTag(stanza); + stanza = stanza.tree(); + } else { + stanza = data[i]; + rawStanza = Strophe.serialize(stanza); + } + this._conn.xmlOutput(stanza); + this._conn.rawOutput(rawStanza); + this.socket.send(rawStanza); + } + } + this._conn._data = []; + } + }, + + /** PrivateFunction: _onMessage + * _Private_ function to handle websockets messages. + * + * This function parses each of the messages as if they are full documents. [TODO : We may actually want to use a SAX Push parser]. + * + * Since all XMPP traffic starts with "" + * The first stanza will always fail to be parsed... + * Addtionnaly, the seconds stanza will always be a with the stream NS defined in the previous stanza... so we need to 'force' the inclusion of the NS in this stanza! + * + * Parameters: + * (string) message - The websocket message. + */ + _onMessage: function(message) { + var elem, data; + // check for closing stream + if (message.data === "") { + var close = ""; + this._conn.rawInput(close); + this._conn.xmlInput(document.createElement("stream:stream")); + if (!this._conn.disconnecting) { + this._conn._doDisconnect(); + } return; + } else if (message.data.search("/, ""); + elem = new DOMParser().parseFromString(data, "text/xml").documentElement; + + if (!this._handleStreamStart(elem)) { + return; + } + } else { + data = this._streamWrap(message.data); + elem = new DOMParser().parseFromString(data, "text/xml").documentElement; } - } else { - data = this._streamWrap(message.data); - elem = new DOMParser().parseFromString(data, "text/xml").documentElement; - } - if (this._check_streamerror(elem, Strophe.Status.ERROR)) { - return; - } + if (this._check_streamerror(elem, Strophe.Status.ERROR)) { + return; + } - //handle unavailable presence stanza before disconnecting - if (this._conn.disconnecting && - elem.firstChild.nodeName === "presence" && - elem.firstChild.getAttribute("type") === "unavailable") { - this._conn.xmlInput(elem); - this._conn.rawInput(Strophe.serialize(elem)); - // if we are already disconnecting we will ignore the unavailable stanza and - // wait for the tag before we close the connection - return; + //handle unavailable presence stanza before disconnecting + if (this._conn.disconnecting && + elem.firstChild.nodeName === "presence" && + elem.firstChild.getAttribute("type") === "unavailable") { + this._conn.xmlInput(elem); + this._conn.rawInput(Strophe.serialize(elem)); + // if we are already disconnecting we will ignore the unavailable stanza and + // wait for the tag before we close the connection + return; + } + this._conn._dataRecv(elem, message.data); + }, + + /** PrivateFunction: _onOpen + * _Private_ function to handle websockets connection setup. + * + * The opening stream tag is sent here. + */ + _onOpen: function() { + Strophe.info("Websocket open"); + var start = this._buildStream(); + this._conn.xmlOutput(start.tree()); + + var startString = this._removeClosingTag(start); + this._conn.rawOutput(startString); + this.socket.send(startString); + }, + + /** PrivateFunction: _removeClosingTag + * _Private_ function to Make the first non-selfclosing + * + * Parameters: + * (Object) elem - The tag. + * + * Returns: + * The stream:stream tag as String + */ + _removeClosingTag: function(elem) { + var string = Strophe.serialize(elem); + string = string.replace(/<(stream:stream .*[^\/])\/>$/, "<$1>"); + return string; + }, + + /** PrivateFunction: _reqToData + * _Private_ function to get a stanza out of a request. + * + * WebSockets don't use requests, so the passed argument is just returned. + * + * Parameters: + * (Object) stanza - The stanza. + * + * Returns: + * The stanza that was passed. + */ + _reqToData: function (stanza) + { + return stanza; + }, + + /** PrivateFunction: _send + * _Private_ part of the Connection.send function for WebSocket + * + * Just flushes the messages that are in the queue + */ + _send: function () { + this._conn.flush(); + }, + + /** PrivateFunction: _sendRestart + * + * Send an xmpp:restart stanza. + */ + _sendRestart: function () + { + clearTimeout(this._conn._idleTimeout); + this._conn._onIdle.bind(this._conn)(); } - this._conn._dataRecv(elem, message.data); - }, - - /** PrivateFunction: _onOpen - * _Private_ function to handle websockets connection setup. - * - * The opening stream tag is sent here. - */ - _onOpen: function() { - Strophe.info("Websocket open"); - var start = this._buildStream(); - this._conn.xmlOutput(start.tree()); - - var startString = this._removeClosingTag(start); - this._conn.rawOutput(startString); - this.socket.send(startString); - }, - - /** PrivateFunction: _removeClosingTag - * _Private_ function to Make the first non-selfclosing - * - * Parameters: - * (Object) elem - The tag. - * - * Returns: - * The stream:stream tag as String - */ - _removeClosingTag: function(elem) { - var string = Strophe.serialize(elem); - string = string.replace(/<(stream:stream .*[^\/])\/>$/, "<$1>"); - return string; - }, - - /** PrivateFunction: _reqToData - * _Private_ function to get a stanza out of a request. - * - * WebSockets don't use requests, so the passed argument is just returned. - * - * Parameters: - * (Object) stanza - The stanza. - * - * Returns: - * The stanza that was passed. - */ - _reqToData: function (stanza) - { - return stanza; - }, - - /** PrivateFunction: _send - * _Private_ part of the Connection.send function for WebSocket - * - * Just flushes the messages that are in the queue - */ - _send: function () { - this._conn.flush(); - }, - - /** PrivateFunction: _sendRestart - * - * Send an xmpp:restart stanza. - */ - _sendRestart: function () - { - clearTimeout(this._conn._idleTimeout); - this._conn._onIdle.bind(this._conn)(); - } -}; + }; + return Strophe; +})); diff --git a/src/wrapper.js b/src/wrapper.js new file mode 100644 index 00000000..601001ef --- /dev/null +++ b/src/wrapper.js @@ -0,0 +1,7 @@ +define("strophe-full", [ + "strophe-core", + "strophe-bosh", + "strophe-websocket" +], function (wrapper) { + return wrapper; +}); From 074f38741920a353fa99c4439e3c3cac2562cbd6 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 18 Jan 2015 15:24:07 +0100 Subject: [PATCH 02/22] Create builds with Almond. --- Gruntfile.js | 42 ++++++++++++++++++++++-------------------- Makefile | 23 ++++++----------------- bower.json | 5 ++--- build.js | 7 +++++++ package.json | 6 ++++-- 5 files changed, 41 insertions(+), 42 deletions(-) create mode 100644 build.js diff --git a/Gruntfile.js b/Gruntfile.js index 1f3e3e3e..8d1109d7 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -19,7 +19,7 @@ module.exports = function(grunt){ "doc": ["<%= natural_docs.docs.output %>"], "prepare-release": ["strophejs-<%= pkg.version %>"], "release": ["strophejs-<%= pkg.version %>.zip", "strophejs-<%= pkg.version %>.tar.gz"], - "js": ["<%= concat.dist.dest %>", "strophe.min.js"] + "js": ["strophe.js", "strophe.min.js"] }, qunit: { @@ -32,24 +32,12 @@ module.exports = function(grunt){ } }, - concat: { - dist: { - src: ['src/base64.js', "src/sha1.js", "src/md5.js", "src/core.js", "src/bosh.js", "src/websocket.js" ], - dest: '<%= pkg.name %>' - }, - options: { - process: function(src){ - return src.replace('@VERSION@', pkg.version); - } - } - }, - copy: { "prepare-release": { files:[ { expand: true, - src:['<%= concat.dist.dest %>', 'strophe.min.js', 'LICENSE.txt', 'README.txt', + src:['', 'strophe.min.js', 'LICENSE.txt', 'README.txt', 'contrib/**', 'examples/**', 'plugins/**', 'tests/**', 'doc/**'], dest:"strophejs-<%= pkg.version %>" } @@ -58,7 +46,7 @@ module.exports = function(grunt){ "prepare-doc": { files:[ { - src:['<%= concat.dist.dest %>'], + src:['strophe.js'], dest:"<%= natural_docs.docs.inputs[0] %>" } ] @@ -85,13 +73,13 @@ module.exports = function(grunt){ banner: '/*! <%= pkg.name %> v<%= pkg.version %> - built on <%= grunt.template.today("dd-mm-yyyy") %> */\n' }, dist: { - files: { 'strophe.min.js': ['<%= concat.dist.dest %>'] } + files: { 'strophe.min.js': ['strophe.js'] } } }, watch: { files: ['<%= jshint.files %>'], - tasks: ['concat', 'uglify'] + tasks: ['build', 'uglify'] }, natural_docs: { @@ -116,7 +104,6 @@ module.exports = function(grunt){ grunt.loadNpmTasks("grunt-contrib-uglify"); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-watch'); - grunt.loadNpmTasks("grunt-contrib-concat"); grunt.loadNpmTasks("grunt-contrib-clean"); grunt.loadNpmTasks('grunt-shell'); grunt.loadNpmTasks('grunt-natural-docs'); @@ -124,10 +111,25 @@ module.exports = function(grunt){ grunt.loadNpmTasks('grunt-contrib-qunit'); grunt.registerTask("default", ["jshint", "min"]); - grunt.registerTask("min", ["concat", "uglify"]); + grunt.registerTask("min", ["build", "uglify"]); grunt.registerTask("prepare-release", ["copy:prepare-release"]); - grunt.registerTask("doc", ["concat", "copy:prepare-doc", "mkdir:prepare-doc", "natural_docs"]); + grunt.registerTask("doc", ["build", "copy:prepare-doc", "mkdir:prepare-doc", "natural_docs"]); grunt.registerTask("release", ["default", "doc", "copy:prepare-release", "shell:tar", "shell:zip"]); grunt.registerTask("all", ["release", "clean"]); + grunt.registerTask('build', 'Create a new build', function () { + var done = this.async(); + require('child_process').exec( + './node_modules/requirejs/bin/r.js -o build.js optimize=none out=strophe.js', + function (err, stdout, stderr) { + if (err) { + grunt.log.write('build failed with error code '+err.code); + grunt.log.write(stderr); + } + grunt.log.write(stdout); + done(); + } + ); + grunt.task.run('uglify'); + }); }; diff --git a/Makefile b/Makefile index 50a0844d..85dcbe33 100644 --- a/Makefile +++ b/Makefile @@ -11,9 +11,8 @@ NDPROJ_DIR = ndproj STROPHE = strophe.js STROPHE_MIN = strophe.min.js -all: normal min -normal: stamp-bower $(STROPHE) -min: stamp-bower $(STROPHE_MIN) +all: clean build +build: stamp-bower $(STROPHE) stamp-npm: package.json npm install @@ -25,13 +24,9 @@ stamp-bower: stamp-npm bower.json $(STROPHE): stamp-bower @@echo "Building" $(STROPHE) "..." - $(GRUNT) concat + $(GRUNT) build @@echo -$(STROPHE_MIN): $(STROPHE) - @@echo "Building" $(STROPHE_MIN) "..." - $(GRUNT) min - doc: @@echo "Building Strophe documentation..." @@if [ ! -d $(NDPROJ_DIR) ]; then mkdir $(NDPROJ_DIR); fi @@ -51,19 +46,13 @@ check:: stamp-bower normal $(PHANTOMJS) node_modules/qunit-phantomjs-runner/runner-list.js tests/strophe.html clean: - rm -f stamp-npm stamp-bower - rm -rf node_modules bower_components - @@echo "Cleaning" node_modules "..." - @@rm -rf node_modules - @@echo "Cleaning" $(STROPHE) "..." + @@rm -f stamp-npm stamp-bower + @@rm -rf node_modules bower_components @@rm -f $(STROPHE) - @@echo "Cleaning" $(STROPHE_MIN) "..." @@rm -f $(STROPHE_MIN) - @@echo "Cleaning minified plugins..." @@rm -f $(PLUGIN_FILES_MIN) - @@echo "Cleaning documentation..." @@rm -rf $(NDPROJ_DIR) $(DOC_DIR) $(DOC_TEMP) @@echo "Done." @@echo -.PHONY: all normal min doc release clean check +.PHONY: all doc release clean check diff --git a/bower.json b/bower.json index 8ba5e205..804ec536 100644 --- a/bower.json +++ b/bower.json @@ -25,11 +25,9 @@ "release_checklist.txt", "Makefile", "Gruntfile.js", - "component.json", "contrib", "examples", "tests", - "plugins", "src" ], "dependencies": { @@ -37,6 +35,7 @@ "sinon-qunit": "~2.0.0", "qunit": "~1.16.0", "jquery": "1.11.0", - "requirejs": "~2.1.15" + "requirejs": "~2.1.15", + "almond": "~0.3.0" } } diff --git a/build.js b/build.js new file mode 100644 index 00000000..9e9bb341 --- /dev/null +++ b/build.js @@ -0,0 +1,7 @@ +({ + baseUrl: ".", + name: "bower_components/almond/almond.js", + out: "strophe.min.js", + include: ['main'], + mainConfigFile: 'main.js' +}) diff --git a/package.json b/package.json index 8a68533e..86234323 100644 --- a/package.json +++ b/package.json @@ -61,12 +61,14 @@ "grunt-contrib-clean": "~0.5.0", "grunt-contrib-concat": "~0.3.0", "grunt-contrib-copy": "~0.5.0", - "grunt-contrib-jshint": "~0.8.0", + "grunt-contrib-jshint": "~0.10.0", "grunt-contrib-qunit": "^0.5.2", + "grunt-contrib-requirejs": "^0.4.4", "grunt-contrib-uglify": "~0.2.7", "grunt-contrib-watch": "~0.5.3", "grunt-mkdir": "~0.1.1", "grunt-natural-docs": "~0.1.1", - "grunt-shell": "~0.6.1" + "grunt-shell": "~0.6.1", + "requirejs": "~2.1.15" } } From feb245d83a7c9cfbbaca18968743c00d91aa20bb Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 18 Jan 2015 18:32:02 +0100 Subject: [PATCH 03/22] Expose more sha1 methods needed by Strophe. --- src/sha1.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sha1.js b/src/sha1.js index d9c1f1e0..c5651f9c 100644 --- a/src/sha1.js +++ b/src/sha1.js @@ -165,9 +165,11 @@ * They take string arguments and return either hex or base-64 encoded strings */ return { - b64_sha1: function (s) { return binb2b64(core_sha1(str2binb(s),s.length * 8)); }, - str_sha1: function (s) { return binb2str(core_sha1(str2binb(s),s.length * 8)); }, b64_hmac_sha1: function (key, data){ return binb2b64(core_hmac_sha1(key, data)); }, + b64_sha1: function (s) { return binb2b64(core_sha1(str2binb(s),s.length * 8)); }, + binb2str: binb2str, + core_hmac_sha1: core_hmac_sha1, str_hmac_sha1: function (key, data){ return binb2str(core_hmac_sha1(key, data)); }, + str_sha1: function (s) { return binb2str(core_sha1(str2binb(s),s.length * 8)); }, }; })); From e1e93b97e58bcfd4610493ead60a66dc1a782a73 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 18 Jan 2015 18:32:07 +0100 Subject: [PATCH 04/22] Load the individual models for tests. --- Makefile | 7 +++---- tests/main.js | 26 +++++++++++--------------- tests/strophe.html | 1 + tests/tests.js | 16 ++++++++++------ 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Makefile b/Makefile index 85dcbe33..ce5b60e8 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,6 @@ STROPHE = strophe.js STROPHE_MIN = strophe.min.js all: clean build -build: stamp-bower $(STROPHE) stamp-npm: package.json npm install @@ -22,7 +21,7 @@ stamp-bower: stamp-npm bower.json $(BOWER) install touch stamp-bower -$(STROPHE): stamp-bower +build: stamp-bower @@echo "Building" $(STROPHE) "..." $(GRUNT) build @@echo @@ -42,7 +41,7 @@ release: @@echo "Release created." @@echo -check:: stamp-bower normal +check:: $(PHANTOMJS) node_modules/qunit-phantomjs-runner/runner-list.js tests/strophe.html clean: @@ -55,4 +54,4 @@ clean: @@echo "Done." @@echo -.PHONY: all doc release clean check +.PHONY: all build doc release clean check diff --git a/tests/main.js b/tests/main.js index 30fbecae..73385a20 100644 --- a/tests/main.js +++ b/tests/main.js @@ -1,18 +1,14 @@ -require.config({ - baseUrl: "../", - paths: { - "jquery": "bower_components/jquery/dist/jquery", - "sinon": "bower_components/sinon/index", - "sinon-qunit": "bower_components/sinon-qunit/lib/sinon-qunit", - "strophe": "strophe", - "tests": "tests/tests" - }, - shim: { - 'sinon-qunit': { deps: ['sinon']}, - 'strophe': { exports: 'Strophe' }, - } -}); - +config.baseUrl = '../'; +config.paths.jquery = "bower_components/jquery/dist/jquery"; +config.paths.sinon = "bower_components/sinon/index"; +config.paths["sinon-qunit"] = "bower_components/sinon-qunit/lib/sinon-qunit"; +config.paths.strophe = "strophe"; +config.paths.tests = "tests/tests"; +config.shim = { + 'sinon-qunit': { deps: ['sinon']}, + 'strophe': { exports: 'Strophe' }, +}; +require.config(config); require(["tests"], function(tests) { tests.run(); QUnit.start(); diff --git a/tests/strophe.html b/tests/strophe.html index d3858839..c91f5d37 100644 --- a/tests/strophe.html +++ b/tests/strophe.html @@ -9,6 +9,7 @@ + Date: Tue, 27 Jan 2015 14:31:47 +0100 Subject: [PATCH 05/22] No need for a shim for strophe --- tests/main.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/main.js b/tests/main.js index 73385a20..31323fcf 100644 --- a/tests/main.js +++ b/tests/main.js @@ -5,8 +5,7 @@ config.paths["sinon-qunit"] = "bower_components/sinon-qunit/lib/sinon-qunit"; config.paths.strophe = "strophe"; config.paths.tests = "tests/tests"; config.shim = { - 'sinon-qunit': { deps: ['sinon']}, - 'strophe': { exports: 'Strophe' }, + 'sinon-qunit': { deps: ['sinon']} }; require.config(config); require(["tests"], function(tests) { From 8594700a51fe37746e4f923d51725a4dea5bf282 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Tue, 27 Jan 2015 15:08:40 +0100 Subject: [PATCH 06/22] Build.js doesn't build the minified file --- build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.js b/build.js index 9e9bb341..8334c59c 100644 --- a/build.js +++ b/build.js @@ -1,7 +1,7 @@ ({ baseUrl: ".", name: "bower_components/almond/almond.js", - out: "strophe.min.js", + out: "strophe.js", include: ['main'], mainConfigFile: 'main.js' }) From 09125dadce92810017aa4ff3b859d50615834ddf Mon Sep 17 00:00:00 2001 From: JC Brand Date: Tue, 27 Jan 2015 15:09:06 +0100 Subject: [PATCH 07/22] Also expose the crypto stuff --- src/core.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core.js b/src/core.js index f6ba6914..ea03d50a 100644 --- a/src/core.js +++ b/src/core.js @@ -3269,6 +3269,9 @@ $build: $build, $msg: $msg, $iq: $iq, - $pres: $pres + $pres: $pres, + SHA1: SHA1, + Base64: Base64, + MD5: MD5 }; })); From 3614aac3acc259fcce5f5867d105590225f51ad1 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 1 Feb 2015 00:48:52 +0100 Subject: [PATCH 08/22] Bugfix. Use indexOf method of array. --- src/core.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core.js b/src/core.js index ea03d50a..b83d6850 100644 --- a/src/core.js +++ b/src/core.js @@ -1295,7 +1295,7 @@ var elem_type = elem.getAttribute("type"); if (nsMatch && (!this.name || Strophe.isTagEqual(elem, this.name)) && - (!this.type || (Array.isArray(this.type) ? elem_type in this.type : elem_type == this.type)) && + (!this.type || (Array.isArray(this.type) ? this.type.indexOf(elem_type) != -1 : elem_type == this.type)) && (!this.id || elem.getAttribute("id") == this.id) && (!this.from || from == this.from)) { return true; From 0ba123b6aa5bbbd5eb951d84ba4af6459ce568bb Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 1 Feb 2015 19:02:46 +0100 Subject: [PATCH 09/22] strophe-full has been renamed to strophe --- examples/main.js | 2 +- main.js | 15 +++++++-------- src/wrapper.js | 2 +- tests/main.js | 1 - tests/tests.js | 4 ++-- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/examples/main.js b/examples/main.js index 233874c5..6ac8b1c8 100644 --- a/examples/main.js +++ b/examples/main.js @@ -1,7 +1,7 @@ config.baseUrl = '../'; require.config(config); if (typeof(require) === 'function') { - require(["jquery", "strophe-full", ], function($, wrapper) { + require(["jquery", "strophe", ], function($, wrapper) { Strophe = wrapper.Strophe; var BOSH_SERVICE = 'http://bosh.metajack.im:5280/xmpp-httpbind'; diff --git a/main.js b/main.js index 3afb9f88..64845433 100644 --- a/main.js +++ b/main.js @@ -18,26 +18,25 @@ require.config({ "strophe-base64": "src/base64", "strophe-bosh": "src/bosh", "strophe-core": "src/core", - "strophe-full": "src/wrapper", + "strophe": "src/wrapper", "strophe-md5": "src/md5", "strophe-sha1": "src/sha1", "strophe-websocket": "src/websocket", "strophe-polyfill": "src/polyfill", // Examples - "basic": "examples/basic", + "basic": "examples/basic", // Tests - "jquery": "bower_components/jquery/dist/jquery", - "sinon": "bower_components/sinon/index", - "sinon-qunit": "bower_components/sinon-qunit/lib/sinon-qunit", - "strophe": "src/strophe", - "tests": "tests/tests" + "jquery": "bower_components/jquery/dist/jquery", + "sinon": "bower_components/sinon/index", + "sinon-qunit": "bower_components/sinon-qunit/lib/sinon-qunit", + "tests": "tests/tests" } }); if (typeof(require) === 'function') { - require(["strophe-full"], function(Strophe) { + require(["strophe"], function(Strophe) { window.Strophe = Strophe; }); } diff --git a/src/wrapper.js b/src/wrapper.js index 601001ef..1a7c1f7d 100644 --- a/src/wrapper.js +++ b/src/wrapper.js @@ -1,4 +1,4 @@ -define("strophe-full", [ +define("strophe", [ "strophe-core", "strophe-bosh", "strophe-websocket" diff --git a/tests/main.js b/tests/main.js index 31323fcf..9acb2500 100644 --- a/tests/main.js +++ b/tests/main.js @@ -2,7 +2,6 @@ config.baseUrl = '../'; config.paths.jquery = "bower_components/jquery/dist/jquery"; config.paths.sinon = "bower_components/sinon/index"; config.paths["sinon-qunit"] = "bower_components/sinon-qunit/lib/sinon-qunit"; -config.paths.strophe = "strophe"; config.paths.tests = "tests/tests"; config.shim = { 'sinon-qunit': { deps: ['sinon']} diff --git a/tests/tests.js b/tests/tests.js index a6fb2f01..37184fd8 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -2,7 +2,7 @@ define([ 'jquery', 'sinon', 'sinon-qunit', - 'strophe-full' + 'strophe' ], function($, sinon, sinon_qunit, wrapper) { var run = function () { @@ -239,7 +239,7 @@ define([ notEqual(hand.isMatch(elem), true, "The handler should not match wrong stanza type"); hand = new Strophe.Handler(null, null, 'iq', ['error', 'result']); - notEqual(hand.isMatch(elem), true, "The handler should match if stanza type is in array of types"); + equal(hand.isMatch(elem), true, "The handler should match if stanza type is in array of types"); }); module("Misc"); From a405037eb6b66c931f7a583081d4770dbc401694 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 1 Feb 2015 19:18:58 +0100 Subject: [PATCH 10/22] Format code to allow easier merging from master. --- src/base64.js | 13 +- src/bosh.js | 1498 ++++++------ src/core.js | 5935 +++++++++++++++++++++++----------------------- src/md5.js | 38 +- src/sha1.js | 295 +-- src/websocket.js | 952 ++++---- 6 files changed, 4378 insertions(+), 4353 deletions(-) diff --git a/src/base64.js b/src/base64.js index 31980b55..1fe7c7af 100644 --- a/src/base64.js +++ b/src/base64.js @@ -14,11 +14,12 @@ } }(this, function () { var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var obj = { /** - * Encodes a string in base64 - * @param {String} input The string to encode in base64. - */ + * Encodes a string in base64 + * @param {String} input The string to encode in base64. + */ encode: function (input) { var output = ""; var chr1, chr2, chr3; @@ -50,9 +51,9 @@ }, /** - * Decodes a base64 string. - * @param {String} input The string to decode. - */ + * Decodes a base64 string. + * @param {String} input The string to decode. + */ decode: function (input) { var output = ""; var chr1, chr2, chr3; diff --git a/src/bosh.js b/src/bosh.js index 59197bab..8aabd2e7 100644 --- a/src/bosh.js +++ b/src/bosh.js @@ -23,841 +23,841 @@ } }(this, function (Strophe, $build) { - /** 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. - */ +/** 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: + * (XMLElement) elem - The XML 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 (elem, func, rid, sends) +{ + this.id = ++Strophe._requestId; + this.xmlData = elem; + this.data = Strophe.serialize(elem); + // 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(); +}; - /** PrivateConstructor: Strophe.Request - * Create and initialize a new Strophe.Request object. +Strophe.Request.prototype = { + /** PrivateFunction: getResponse + * Get a response from the underlying XMLHttpRequest. * - * Parameters: - * (XMLElement) elem - The XML 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. + * 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. */ - Strophe.Request = function (elem, func, rid, sends) + getResponse: function () { - this.id = ++Strophe._requestId; - this.xmlData = elem; - this.data = Strophe.serialize(elem); - // 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) { + 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; charset=utf-8"); - } - } else if (window.ActiveXObject) { - xhr = new ActiveXObject("Microsoft.XMLHTTP"); - } - - // use Function.bind() to prepend ourselves as an argument - xhr.onreadystatechange = this.func.bind(null, this); + return node; + }, - return xhr; + /** 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; charset=utf-8"); + } + } else if (window.ActiveXObject) { + xhr = new ActiveXObject("Microsoft.XMLHTTP"); } - }; - /** Class: Strophe.Bosh - * _Private_ helper class that handles BOSH Connections + // use Function.bind() to prepend ourselves as an argument + xhr.onreadystatechange = this.func.bind(null, this); + + return xhr; + } +}; + +/** Class: Strophe.Bosh + * _Private_ helper class that handles BOSH Connections + * + * The Strophe.Bosh class is used internally by Strophe.Connection + * to encapsulate BOSH sessions. It is not meant to be used from user's code. + */ + +/** File: bosh.js + * A JavaScript library to enable BOSH in Strophejs. + * + * 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. + */ + +/** PrivateConstructor: Strophe.Bosh + * Create and initialize a Strophe.Bosh object. + * + * Parameters: + * (Strophe.Connection) connection - The Strophe.Connection that will use BOSH. + * + * Returns: + * A new Strophe.Bosh object. + */ +Strophe.Bosh = function(connection) { + this._conn = connection; + /* request id for body tags */ + this.rid = Math.floor(Math.random() * 4294967295); + /* The current session ID. */ + this.sid = null; + + // default BOSH values + this.hold = 1; + this.wait = 60; + this.window = 5; + this.errors = 0; + + this._requests = []; +}; + +Strophe.Bosh.prototype = { + /** Variable: strip * - * The Strophe.Bosh class is used internally by Strophe.Connection - * to encapsulate BOSH sessions. It is not meant to be used from user's code. + * BOSH-Connections will have all stanzas wrapped in a tag when + * passed to or . + * To strip this tag, User code can set to "body": + * + * > Strophe.Bosh.prototype.strip = "body"; + * + * This will enable stripping of the body tag in both + * and . */ + strip: null, - /** File: bosh.js - * A JavaScript library to enable BOSH in Strophejs. + /** PrivateFunction: _buildBody + * _Private_ helper function to generate the wrapper for BOSH. * - * 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. + * Returns: + * A Strophe.Builder with a element. */ + _buildBody: function () + { + var bodyWrap = $build('body', { + rid: this.rid++, + xmlns: Strophe.NS.HTTPBIND + }); - /** PrivateConstructor: Strophe.Bosh - * Create and initialize a Strophe.Bosh object. - * - * Parameters: - * (Strophe.Connection) connection - The Strophe.Connection that will use BOSH. + if (this.sid !== null) { + bodyWrap.attrs({sid: this.sid}); + } + + return bodyWrap; + }, + + /** PrivateFunction: _reset + * Reset the connection. * - * Returns: - * A new Strophe.Bosh object. + * This function is called by the reset function of the Strophe Connection */ - Strophe.Bosh = function(connection) { - this._conn = connection; - /* request id for body tags */ + _reset: function () + { this.rid = Math.floor(Math.random() * 4294967295); - /* The current session ID. */ this.sid = null; + this.errors = 0; + }, - // default BOSH values - this.hold = 1; - this.wait = 60; - this.window = 5; + /** PrivateFunction: _connect + * _Private_ function that initializes the BOSH connection. + * + * Creates and sends the Request that initializes the BOSH connection. + */ + _connect: function (wait, hold, route) + { + this.wait = wait || this.wait; + this.hold = hold || this.hold; this.errors = 0; - this._requests = []; - }; + // build the body tag + var body = this._buildBody().attrs({ + to: this._conn.domain, + "xml:lang": "en", + wait: this.wait, + hold: this.hold, + content: "text/xml; charset=utf-8", + ver: "1.6", + "xmpp:version": "1.0", + "xmlns:xmpp": Strophe.NS.BOSH + }); - Strophe.Bosh.prototype = { - /** Variable: strip - * - * BOSH-Connections will have all stanzas wrapped in a tag when - * passed to or . - * To strip this tag, User code can set to "body": - * - * > Strophe.Bosh.prototype.strip = "body"; - * - * This will enable stripping of the body tag in both - * and . - */ - strip: null, - - /** 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(route){ + body.attrs({ + route: route }); + } - if (this.sid !== null) { - bodyWrap.attrs({sid: this.sid}); - } + var _connect_cb = this._conn._connect_cb; - return bodyWrap; - }, - - /** PrivateFunction: _reset - * Reset the connection. - * - * This function is called by the reset function of the Strophe Connection - */ - _reset: function () - { - this.rid = Math.floor(Math.random() * 4294967295); - this.sid = null; - this.errors = 0; - }, - - /** PrivateFunction: _connect - * _Private_ function that initializes the BOSH connection. - * - * Creates and sends the Request that initializes the BOSH connection. - */ - _connect: function (wait, hold, route) - { - this.wait = wait || this.wait; - this.hold = hold || this.hold; - this.errors = 0; - - // build the body tag - var body = this._buildBody().attrs({ - to: this._conn.domain, - "xml:lang": "en", - wait: this.wait, - hold: this.hold, - content: "text/xml; charset=utf-8", - ver: "1.6", - "xmpp:version": "1.0", - "xmlns:xmpp": Strophe.NS.BOSH - }); + this._requests.push( + new Strophe.Request(body.tree(), + this._onRequestStateChange.bind( + this, _connect_cb.bind(this._conn)), + body.tree().getAttribute("rid"))); + this._throttledRequestHandler(); + }, - if(route){ - body.attrs({ - route: route - }); + /** PrivateFunction: _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. + * (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. + */ + _attach: function (jid, sid, rid, callback, wait, hold, wind) + { + this._conn.jid = jid; + this.sid = sid; + this.rid = rid; + + this._conn.connect_callback = callback; + + this._conn.domain = Strophe.getDomainFromJid(this._conn.jid); + + this._conn.authenticated = true; + this._conn.connected = true; + + this.wait = wait || this.wait; + this.hold = hold || this.hold; + this.window = wind || this.window; + + this._conn._changeConnectStatus(Strophe.Status.ATTACHED, null); + }, + + /** PrivateFunction: _connect_cb + * _Private_ handler for initial connection request. + * + * This handler is used to process the Bosh-part of the initial request. + * Parameters: + * (Strophe.Request) bodyWrap - The received stanza. + */ + _connect_cb: function (bodyWrap) + { + var typ = bodyWrap.getAttribute("type"); + var cond, conflict; + if (typ !== null && typ == "terminate") { + // an error occurred + Strophe.error("BOSH-Connection failed: " + cond); + cond = bodyWrap.getAttribute("condition"); + conflict = bodyWrap.getElementsByTagName("conflict"); + if (cond !== null) { + if (cond == "remote-stream-error" && conflict.length > 0) { + cond = "conflict"; + } + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, cond); + } else { + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); } + this._conn._doDisconnect(); + return Strophe.Status.CONNFAIL; + } - var _connect_cb = this._conn._connect_cb; + // check to make sure we don't overwrite these if _connect_cb is + // called multiple times in the case of missing stream:features + if (!this.sid) { + this.sid = bodyWrap.getAttribute("sid"); + } + var wind = bodyWrap.getAttribute('requests'); + if (wind) { this.window = parseInt(wind, 10); } + var hold = bodyWrap.getAttribute('hold'); + if (hold) { this.hold = parseInt(hold, 10); } + var wait = bodyWrap.getAttribute('wait'); + if (wait) { this.wait = parseInt(wait, 10); } + }, + + /** PrivateFunction: _disconnect + * _Private_ part of Connection.disconnect for Bosh + * + * Parameters: + * (Request) pres - This stanza will be sent before disconnecting. + */ + _disconnect: function (pres) + { + this._sendTerminate(pres); + }, - this._requests.push( + /** PrivateFunction: _doDisconnect + * _Private_ function to disconnect. + * + * Resets the SID and RID. + */ + _doDisconnect: function () + { + this.sid = null; + this.rid = Math.floor(Math.random() * 4294967295); + }, + + /** PrivateFunction: _emptyQueue + * _Private_ function to check if the Request queue is empty. + * + * Returns: + * True, if there are no Requests queued, False otherwise. + */ + _emptyQueue: function () + { + return this._requests.length === 0; + }, + + /** 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._conn._onDisconnectTimeout(); + } + }, + + /** PrivateFunction: _no_auth_received + * + * Called on stream start/restart when no stream:features + * has been received and sends a blank poll request. + */ + _no_auth_received: function (_callback) + { + if (_callback) { + _callback = _callback.bind(this._conn); + } else { + _callback = this._conn._connect_cb.bind(this._conn); + } + var body = this._buildBody(); + this._requests.push( new Strophe.Request(body.tree(), - this._onRequestStateChange.bind( - this, _connect_cb.bind(this._conn)), - body.tree().getAttribute("rid"))); - this._throttledRequestHandler(); - }, - - /** PrivateFunction: _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. - * (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. - */ - _attach: function (jid, sid, rid, callback, wait, hold, wind) - { - this._conn.jid = jid; - this.sid = sid; - this.rid = rid; - - this._conn.connect_callback = callback; - - this._conn.domain = Strophe.getDomainFromJid(this._conn.jid); - - this._conn.authenticated = true; - this._conn.connected = true; - - this.wait = wait || this.wait; - this.hold = hold || this.hold; - this.window = wind || this.window; - - this._conn._changeConnectStatus(Strophe.Status.ATTACHED, null); - }, - - /** PrivateFunction: _connect_cb - * _Private_ handler for initial connection request. - * - * This handler is used to process the Bosh-part of the initial request. - * Parameters: - * (Strophe.Request) bodyWrap - The received stanza. - */ - _connect_cb: function (bodyWrap) - { - var typ = bodyWrap.getAttribute("type"); - var cond, conflict; - if (typ !== null && typ == "terminate") { - // an error occurred - Strophe.error("BOSH-Connection failed: " + cond); - cond = bodyWrap.getAttribute("condition"); - conflict = bodyWrap.getElementsByTagName("conflict"); - if (cond !== null) { - if (cond == "remote-stream-error" && conflict.length > 0) { - cond = "conflict"; + this._onRequestStateChange.bind( + this, _callback.bind(this._conn)), + body.tree().getAttribute("rid"))); + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _onDisconnectTimeout + * _Private_ timeout handler for handling non-graceful disconnection. + * + * Cancels all remaining Requests and clears the queue. + */ + _onDisconnectTimeout: function () + { + var req; + while (this._requests.length > 0) { + req = this._requests.pop(); + req.abort = true; + req.xhr.abort(); + // jslint complains, but this is fine. setting to empty func + // is necessary for IE6 + req.xhr.onreadystatechange = function () {}; // jshint ignore:line + } + }, + + /** PrivateFunction: _onIdle + * _Private_ handler called by Strophe.Connection._onIdle + * + * Sends all queued Requests or polls with empty Request if there are none. + */ + _onIdle: function () { + var data = this._conn._data; + + // if no requests are in progress, poll + if (this._conn.authenticated && this._requests.length === 0 && + data.length === 0 && !this._conn.disconnecting) { + Strophe.info("no requests during idle cycle, sending " + + "blank request"); + data.push(null); + } + + if (this._conn.paused) { + return; + } + + if (this._requests.length < 2 && data.length > 0) { + var body = this._buildBody(); + for (var i = 0; i < data.length; i++) { + if (data[i] !== null) { + if (data[i] === "restart") { + body.attrs({ + to: this._conn.domain, + "xml:lang": "en", + "xmpp:restart": "true", + "xmlns:xmpp": Strophe.NS.BOSH + }); + } else { + body.cnode(data[i]).up(); } - this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, cond); - } else { - this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); } - this._conn._doDisconnect(); - return Strophe.Status.CONNFAIL; - } - - // check to make sure we don't overwrite these if _connect_cb is - // called multiple times in the case of missing stream:features - if (!this.sid) { - this.sid = bodyWrap.getAttribute("sid"); - } - var wind = bodyWrap.getAttribute('requests'); - if (wind) { this.window = parseInt(wind, 10); } - var hold = bodyWrap.getAttribute('hold'); - if (hold) { this.hold = parseInt(hold, 10); } - var wait = bodyWrap.getAttribute('wait'); - if (wait) { this.wait = parseInt(wait, 10); } - }, - - /** PrivateFunction: _disconnect - * _Private_ part of Connection.disconnect for Bosh - * - * Parameters: - * (Request) pres - This stanza will be sent before disconnecting. - */ - _disconnect: function (pres) - { - this._sendTerminate(pres); - }, - - /** PrivateFunction: _doDisconnect - * _Private_ function to disconnect. - * - * Resets the SID and RID. - */ - _doDisconnect: function () - { - this.sid = null; - this.rid = Math.floor(Math.random() * 4294967295); - }, - - /** PrivateFunction: _emptyQueue - * _Private_ function to check if the Request queue is empty. - * - * Returns: - * True, if there are no Requests queued, False otherwise. - */ - _emptyQueue: function () - { - return this._requests.length === 0; - }, - - /** 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._conn._onDisconnectTimeout(); - } - }, - - /** PrivateFunction: _no_auth_received - * - * Called on stream start/restart when no stream:features - * has been received and sends a blank poll request. - */ - _no_auth_received: function (_callback) - { - if (_callback) { - _callback = _callback.bind(this._conn); - } else { - _callback = this._conn._connect_cb.bind(this._conn); } - var body = this._buildBody(); + delete this._conn._data; + this._conn._data = []; this._requests.push( - new Strophe.Request(body.tree(), - this._onRequestStateChange.bind( - this, _callback.bind(this._conn)), - body.tree().getAttribute("rid"))); + new Strophe.Request(body.tree(), + this._onRequestStateChange.bind( + this, this._conn._dataRecv.bind(this._conn)), + body.tree().getAttribute("rid"))); this._throttledRequestHandler(); - }, - - /** PrivateFunction: _onDisconnectTimeout - * _Private_ timeout handler for handling non-graceful disconnection. - * - * Cancels all remaining Requests and clears the queue. - */ - _onDisconnectTimeout: function () - { - var req; - while (this._requests.length > 0) { - req = this._requests.pop(); - req.abort = true; - req.xhr.abort(); - // jslint complains, but this is fine. setting to empty func - // is necessary for IE6 - req.xhr.onreadystatechange = function () {}; // jshint ignore:line - } - }, - - /** PrivateFunction: _onIdle - * _Private_ handler called by Strophe.Connection._onIdle - * - * Sends all queued Requests or polls with empty Request if there are none. - */ - _onIdle: function () { - var data = this._conn._data; - - // if no requests are in progress, poll - if (this._conn.authenticated && this._requests.length === 0 && - data.length === 0 && !this._conn.disconnecting) { - Strophe.info("no requests during idle cycle, sending " + - "blank request"); - data.push(null); - } + } - if (this._conn.paused) { - return; + if (this._requests.length > 0) { + var time_elapsed = this._requests[0].age(); + if (this._requests[0].dead !== null) { + if (this._requests[0].timeDead() > + Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)) { + this._throttledRequestHandler(); + } } - if (this._requests.length < 2 && data.length > 0) { - var body = this._buildBody(); - for (var i = 0; i < data.length; i++) { - if (data[i] !== null) { - if (data[i] === "restart") { - body.attrs({ - to: this._conn.domain, - "xml:lang": "en", - "xmpp:restart": "true", - "xmlns:xmpp": Strophe.NS.BOSH - }); - } else { - body.cnode(data[i]).up(); - } - } - } - delete this._conn._data; - this._conn._data = []; - this._requests.push( - new Strophe.Request(body.tree(), - this._onRequestStateChange.bind( - this, this._conn._dataRecv.bind(this._conn)), - body.tree().getAttribute("rid"))); + if (time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)) { + Strophe.warn("Request " + + this._requests[0].id + + " timed out, over " + Math.floor(Strophe.TIMEOUT * this.wait) + + " seconds since last activity"); this._throttledRequestHandler(); } + } + }, - if (this._requests.length > 0) { - var time_elapsed = this._requests[0].age(); - if (this._requests[0].dead !== null) { - if (this._requests[0].timeDead() > - Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)) { - this._throttledRequestHandler(); - } - } + /** 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 (time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)) { - Strophe.warn("Request " + - this._requests[0].id + - " timed out, over " + Math.floor(Strophe.TIMEOUT * this.wait) + - " seconds since last activity"); - this._throttledRequestHandler(); - } - } - }, - - /** 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; + } - 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 } - // request complete - var reqStatus; - if (req.xhr.readyState == 4) { + if (typeof(reqStatus) == "undefined") { 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; } + } - if (this.disconnecting) { - if (reqStatus >= 400) { - this._hitError(reqStatus); - return; - } - } + var reqIs0 = (this._requests[0] == req); + var reqIs1 = (this._requests[1] == req); - 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"); + } - 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() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait))) { + this._restartRequest(0); } - - // 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() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait))) { - 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._conn._changeConnectStatus(Strophe.Status.DISCONNECTING, - null); - this._conn._doDisconnect(); - } + // 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._conn._changeConnectStatus(Strophe.Status.DISCONNECTING, + null); + this._conn._doDisconnect(); } } - - if (!((reqStatus > 0 && reqStatus < 500) || - req.sends > 5)) { - this._throttledRequestHandler(); - } } - }, - - /** 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 self = this; - 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 (!((reqStatus > 0 && reqStatus < 500) || + req.sends > 5)) { + this._throttledRequestHandler(); } + } + }, - if (typeof(reqStatus) == "undefined") { - reqStatus = -1; - } + /** 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 self = this; + var req = this._requests[i]; + var reqStatus = -1; - // make sure we limit the number of retries - if (req.sends > this._conn.maxRetries) { - this._conn._onDisconnectTimeout(); - return; + try { + if (req.xhr.readyState == 4) { + reqStatus = req.xhr.status; } + } catch (e) { + Strophe.error("caught an error in _requests[" + i + + "], reqStatus: " + reqStatus); + } - var time_elapsed = req.age(); - var primaryTimeout = (!isNaN(time_elapsed) && - time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)); - var secondaryTimeout = (req.dead !== null && - req.timeDead() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)); - var requestCompletedWithServerError = (req.xhr.readyState == 4 && - (reqStatus < 1 || - reqStatus >= 500)); - if (primaryTimeout || secondaryTimeout || - requestCompletedWithServerError) { - if (secondaryTimeout) { - Strophe.error("Request " + - this._requests[i].id + - " timed out (secondary), restarting"); - } - req.abort = true; - req.xhr.abort(); - // setting to null fails on IE6, so set to empty function - req.xhr.onreadystatechange = function () {}; - this._requests[i] = new Strophe.Request(req.xmlData, - req.origFunc, - req.rid, - req.sends); - req = this._requests[i]; + if (typeof(reqStatus) == "undefined") { + reqStatus = -1; + } + + // make sure we limit the number of retries + if (req.sends > this._conn.maxRetries) { + this._conn._onDisconnectTimeout(); + return; + } + + var time_elapsed = req.age(); + var primaryTimeout = (!isNaN(time_elapsed) && + time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)); + var secondaryTimeout = (req.dead !== null && + req.timeDead() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)); + var requestCompletedWithServerError = (req.xhr.readyState == 4 && + (reqStatus < 1 || + reqStatus >= 500)); + if (primaryTimeout || secondaryTimeout || + requestCompletedWithServerError) { + if (secondaryTimeout) { + Strophe.error("Request " + + this._requests[i].id + + " timed out (secondary), restarting"); } + req.abort = true; + req.xhr.abort(); + // setting to null fails on IE6, so set to empty function + req.xhr.onreadystatechange = function () {}; + this._requests[i] = new Strophe.Request(req.xmlData, + req.origFunc, + req.rid, + req.sends); + req = this._requests[i]; + } - if (req.xhr.readyState === 0) { - Strophe.debug("request id " + req.id + - "." + req.sends + " posting"); - - try { - req.xhr.open("POST", this._conn.service, this._conn.options.sync ? false : true); - req.xhr.setRequestHeader("Content-Type", "text/xml; charset=utf-8"); - } catch (e2) { - Strophe.error("XHR open failed."); - if (!this._conn.connected) { - this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, - "bad-service"); - } - this._conn.disconnect(); - return; + if (req.xhr.readyState === 0) { + Strophe.debug("request id " + req.id + + "." + req.sends + " posting"); + + try { + req.xhr.open("POST", this._conn.service, this._conn.options.sync ? false : true); + req.xhr.setRequestHeader("Content-Type", "text/xml; charset=utf-8"); + } catch (e2) { + Strophe.error("XHR open failed."); + if (!this._conn.connected) { + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, + "bad-service"); } + this._conn.disconnect(); + return; + } - // Fires the XHR request -- may be invoked immediately - // or on a gradually expanding retry window for reconnects - var sendFunc = function () { - req.date = new Date(); - if (self._conn.options.customHeaders){ - var headers = self._conn.options.customHeaders; - for (var header in headers) { - if (headers.hasOwnProperty(header)) { - req.xhr.setRequestHeader(header, headers[header]); - } + // Fires the XHR request -- may be invoked immediately + // or on a gradually expanding retry window for reconnects + var sendFunc = function () { + req.date = new Date(); + if (self._conn.options.customHeaders){ + var headers = self._conn.options.customHeaders; + for (var header in headers) { + if (headers.hasOwnProperty(header)) { + req.xhr.setRequestHeader(header, headers[header]); } } - 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 creates a nicely - // expanding retry window - var backoff = Math.min(Math.floor(Strophe.TIMEOUT * this.wait), - Math.pow(req.sends, 3)) * 1000; - setTimeout(sendFunc, backoff); - } else { - sendFunc(); } + 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 creates a nicely + // expanding retry window + var backoff = Math.min(Math.floor(Strophe.TIMEOUT * this.wait), + Math.pow(req.sends, 3)) * 1000; + setTimeout(sendFunc, backoff); + } else { + sendFunc(); + } - req.sends++; + req.sends++; - if (this._conn.xmlOutput !== Strophe.Connection.prototype.xmlOutput) { - if (req.xmlData.nodeName === this.strip && req.xmlData.childNodes.length) { - this._conn.xmlOutput(req.xmlData.childNodes[0]); - } else { - this._conn.xmlOutput(req.xmlData); - } - } - if (this._conn.rawOutput !== Strophe.Connection.prototype.rawOutput) { - this._conn.rawOutput(req.data); + if (this._conn.xmlOutput !== Strophe.Connection.prototype.xmlOutput) { + if (req.xmlData.nodeName === this.strip && req.xmlData.childNodes.length) { + this._conn.xmlOutput(req.xmlData.childNodes[0]); + } else { + this._conn.xmlOutput(req.xmlData); } - } else { - Strophe.debug("_processRequest: " + - (i === 0 ? "first" : "second") + - " request has readyState of " + - req.xhr.readyState); } - }, - - /** 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); - } + if (this._conn.rawOutput !== Strophe.Connection.prototype.rawOutput) { + this._conn.rawOutput(req.data); } + } else { + Strophe.debug("_processRequest: " + + (i === 0 ? "first" : "second") + + " request has readyState of " + + req.xhr.readyState); + } + }, - // IE6 fails on setting to null, so set to empty function - req.xhr.onreadystatechange = function () {}; + /** 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"); - 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(); + var i; + for (i = this._requests.length - 1; i >= 0; i--) { + if (req == this._requests[i]) { + this._requests.splice(i, 1); } + } - this._processRequest(i); - }, - - /** PrivateFunction: _reqToData - * _Private_ function to get a stanza out of a request. - * - * Tries to extract a stanza out of a Request Object. - * When this fails the current connection will be disconnected. - * - * Parameters: - * (Object) req - The Request. - * - * Returns: - * The stanza that was passed. - */ - _reqToData: function (req) - { - try { - return req.getResponse(); - } catch (e) { - if (e != "parsererror") { throw e; } - this._conn.disconnect("strophe-parsererror"); - } - }, - - /** 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 (pres) - { - Strophe.info("_sendTerminate was called"); - var body = this._buildBody().attrs({type: "terminate"}); - - if (pres) { - body.cnode(pres.tree()); - } + // IE6 fails on setting to null, so set to empty function + req.xhr.onreadystatechange = function () {}; - var req = new Strophe.Request(body.tree(), - this._onRequestStateChange.bind( - this, this._conn._dataRecv.bind(this._conn)), - body.tree().getAttribute("rid")); + this._throttledRequestHandler(); + }, - this._requests.push(req); - this._throttledRequestHandler(); - }, - - /** PrivateFunction: _send - * _Private_ part of the Connection.send function for BOSH - * - * Just triggers the RequestHandler to send the messages that are in the queue - */ - _send: function () { - clearTimeout(this._conn._idleTimeout); - this._throttledRequestHandler(); - this._conn._idleTimeout = setTimeout(this._conn._onIdle.bind(this._conn), 100); - }, - - /** PrivateFunction: _sendRestart - * - * Send an xmpp:restart stanza. - */ - _sendRestart: function () - { - this._throttledRequestHandler(); - clearTimeout(this._conn._idleTimeout); - }, - - /** 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"); - } + /** 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(); + } - if (!this._requests || this._requests.length === 0) { - return; - } + this._processRequest(i); + }, - if (this._requests.length > 0) { - this._processRequest(0); - } + /** PrivateFunction: _reqToData + * _Private_ function to get a stanza out of a request. + * + * Tries to extract a stanza out of a Request Object. + * When this fails the current connection will be disconnected. + * + * Parameters: + * (Object) req - The Request. + * + * Returns: + * The stanza that was passed. + */ + _reqToData: function (req) + { + try { + return req.getResponse(); + } catch (e) { + if (e != "parsererror") { throw e; } + this._conn.disconnect("strophe-parsererror"); + } + }, - if (this._requests.length > 1 && - Math.abs(this._requests[0].rid - - this._requests[1].rid) < this.window) { - this._processRequest(1); - } + /** 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 (pres) + { + Strophe.info("_sendTerminate was called"); + var body = this._buildBody().attrs({type: "terminate"}); + + if (pres) { + body.cnode(pres.tree()); } - }; - return Strophe; + + var req = new Strophe.Request(body.tree(), + this._onRequestStateChange.bind( + this, this._conn._dataRecv.bind(this._conn)), + body.tree().getAttribute("rid")); + + this._requests.push(req); + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _send + * _Private_ part of the Connection.send function for BOSH + * + * Just triggers the RequestHandler to send the messages that are in the queue + */ + _send: function () { + clearTimeout(this._conn._idleTimeout); + this._throttledRequestHandler(); + this._conn._idleTimeout = setTimeout(this._conn._onIdle.bind(this._conn), 100); + }, + + /** PrivateFunction: _sendRestart + * + * Send an xmpp:restart stanza. + */ + _sendRestart: function () + { + this._throttledRequestHandler(); + clearTimeout(this._conn._idleTimeout); + }, + + /** 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) { + this._processRequest(1); + } + } +}; +return Strophe; })); diff --git a/src/core.js b/src/core.js index b83d6850..b4baf708 100644 --- a/src/core.js +++ b/src/core.js @@ -4,6 +4,7 @@ Copyright 2006-2008, OGG, LLC */ + /* jshint undef: true, unused: true:, noarg: true, latedef: true */ /*global define, document, window, setTimeout, clearTimeout, console, ActiveXObject, DOMParser */ @@ -42,3236 +43,3238 @@ } }(this, function (SHA1, Base64, MD5) { - /** 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. +var Strophe; + +/** 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 = { + /** Constant: VERSION + * The version of the Strophe library. Unreleased builds will have + * a version of head-HASH where HASH is a partial revision. + */ + VERSION: "@VERSION@", + + /** Constants: XMPP Namespace Constants + * Common namespace constants from the XMPP RFCs and XEPs. * - * Returns: - * A new Strophe.Builder object. + * 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.XHTML_IM - XHTML-IM namespace from XEP 71. + * NS.XHTML - XHTML body namespace from XEP 71. + */ + 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", + STANZAS: "urn:ietf:params:xml:ns:xmpp-stanzas", + XHTML_IM: "http://jabber.org/protocol/xhtml-im", + XHTML: "http://www.w3.org/1999/xhtml" + }, + + + /** Constants: XHTML_IM Namespace + * contains allowed tags, tag attributes, and css properties. + * Used in the createHtml function to filter incoming html into the allowed XHTML-IM subset. + * See http://xmpp.org/extensions/xep-0071.html#profile-summary for the list of recommended + * allowed tags and their attributes. */ - function $build(name, attrs) { return new Strophe.Builder(name, attrs); } + XHTML: { + tags: ['a','blockquote','br','cite','em','img','li','ol','p','span','strong','ul','body'], + attributes: { + 'a': ['href'], + 'blockquote': ['style'], + 'br': [], + 'cite': ['style'], + 'em': [], + 'img': ['src', 'alt', 'style', 'height', 'width'], + 'li': ['style'], + 'ol': ['style'], + 'p': ['style'], + 'span': ['style'], + 'strong': [], + 'ul': ['style'], + 'body': [] + }, + css: ['background-color','color','font-family','font-size','font-style','font-weight','margin-left','margin-right','text-align','text-decoration'], + validTag: function(tag) + { + for(var i = 0; i < Strophe.XHTML.tags.length; i++) { + if(tag == Strophe.XHTML.tags[i]) { + return true; + } + } + return false; + }, + validAttribute: function(tag, attribute) + { + if(typeof Strophe.XHTML.attributes[tag] !== 'undefined' && Strophe.XHTML.attributes[tag].length > 0) { + for(var i = 0; i < Strophe.XHTML.attributes[tag].length; i++) { + if(attribute == Strophe.XHTML.attributes[tag][i]) { + return true; + } + } + } + return false; + }, + validCSS: function(style) + { + for(var i = 0; i < Strophe.XHTML.css.length; i++) { + if(style == Strophe.XHTML.css[i]) { + return true; + } + } + return false; + } + }, - /** Function: $msg - * Create a Strophe.Builder with a element as the root. + /** Constants: Connection Status Constants + * Connection status constants for use by the connection handler + * callback. * - * Parmaeters: - * (Object) attrs - The element attributes in object notation. + * 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.ATTACHED - The connection has been attached + */ + Status: { + ERROR: 0, + CONNECTING: 1, + CONNFAIL: 2, + AUTHENTICATING: 3, + AUTHFAIL: 4, + CONNECTED: 5, + DISCONNECTED: 6, + DISCONNECTING: 7, + ATTACHED: 8 + }, + + /** Constants: Log Level Constants + * Logging level indicators. * - * Returns: - * A new Strophe.Builder object. + * LogLevel.DEBUG - Debug output + * LogLevel.INFO - Informational output + * LogLevel.WARN - Warnings + * LogLevel.ERROR - Errors + * LogLevel.FATAL - Fatal errors */ - function $msg(attrs) { return new Strophe.Builder("message", attrs); } - - /** Function: $iq - * Create a Strophe.Builder with an element as the root. + 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.FRAGMENT - XHTML fragment element. + */ + ElementType: { + NORMAL: 1, + TEXT: 3, + CDATA: 4, + FRAGMENT: 11 + }, + + /** 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 - Timeout multiplier. A waiting request will be considered + * failed after Math.floor(TIMEOUT * wait) seconds have elapsed. + * This defaults to 1.1, and with default wait, 66 seconds. + * SECONDARY_TIMEOUT - Secondary timeout multiplier. In cases where + * Strophe can detect early failure, it will consider the request + * failed if it doesn't return after + * Math.floor(SECONDARY_TIMEOUT * wait) seconds have elapsed. + * This defaults to 0.1, and with default wait, 6 seconds. + */ + TIMEOUT: 1.1, + SECONDARY_TIMEOUT: 0.1, + + /** Function: addNamespace + * This function is used to extend the current namespaces in + * Strophe.NS. It takes a key and a value with the key being the + * name of the new namespace, with its actual value. + * For example: + * Strophe.addNamespace('PUBSUB', "http://jabber.org/protocol/pubsub"); * * Parameters: - * (Object) attrs - The element attributes in object notation. + * (String) name - The name under which the namespace will be + * referenced under Strophe.NS + * (String) value - The actual namespace. + */ + addNamespace: function (name, value) + { + Strophe.NS[name] = value; + }, + + /** Function: forEachChild + * Map a function over some or all child elements of a given element. * - * Returns: - * A new Strophe.Builder object. + * 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. */ - function $iq(attrs) { return new Strophe.Builder("iq", attrs); } + 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: $pres - * Create a Strophe.Builder with a element as the root. + /** Function: isTagEqual + * Compare an element's tag name with a string. + * + * This function is case insensitive. * * Parameters: - * (Object) attrs - The element attributes in object notation. + * (XMLElement) el - A DOM element. + * (String) name - The element name. * * Returns: - * A new Strophe.Builder object. + * true if the element's tag name matches _el_, and false + * otherwise. + */ + isTagEqual: function (el, name) + { + return el.tagName == name; + }, + + /** PrivateVariable: _xmlGenerator + * _Private_ variable that caches a DOM document to + * generate elements. + */ + _xmlGenerator: null, + + /** PrivateFunction: _makeGenerator + * _Private_ function that creates a dummy XML DOM document to serve as + * an element and text node generator. */ - function $pres(attrs) { return new Strophe.Builder("presence", attrs); } + _makeGenerator: function () { + var doc; + + // IE9 does implement createDocument(); however, using it will cause the browser to leak memory on page unload. + // Here, we test for presence of createDocument() plus IE's proprietary documentMode attribute, which would be + // less than 10 in the case of IE9 and below. + if (document.implementation.createDocument === undefined || + document.implementation.createDocument && document.documentMode && document.documentMode < 10) { + doc = this._getIEXmlDom(); + doc.appendChild(doc.createElement('strophe')); + } else { + doc = document.implementation + .createDocument('jabber:client', 'strophe', null); + } - /** Class: Strophe - * An object container for all Strophe library functions. + return doc; + }, + + /** Function: xmlGenerator + * Get the DOM document to generate elements. * - * 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. + * Returns: + * The currently used DOM document. */ - var Strophe = { - /** Constant: VERSION - * The version of the Strophe library. Unreleased builds will have - * a version of head-HASH where HASH is a partial revision. - */ - VERSION: "@VERSION@", - - /** 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.XHTML_IM - XHTML-IM namespace from XEP 71. - * NS.XHTML - XHTML body namespace from XEP 71. - */ - 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", - STANZAS: "urn:ietf:params:xml:ns:xmpp-stanzas", - XHTML_IM: "http://jabber.org/protocol/xhtml-im", - XHTML: "http://www.w3.org/1999/xhtml" - }, - - - /** Constants: XHTML_IM Namespace - * contains allowed tags, tag attributes, and css properties. - * Used in the createHtml function to filter incoming html into the allowed XHTML-IM subset. - * See http://xmpp.org/extensions/xep-0071.html#profile-summary for the list of recommended - * allowed tags and their attributes. - */ - XHTML: { - tags: ['a','blockquote','br','cite','em','img','li','ol','p','span','strong','ul','body'], - attributes: { - 'a': ['href'], - 'blockquote': ['style'], - 'br': [], - 'cite': ['style'], - 'em': [], - 'img': ['src', 'alt', 'style', 'height', 'width'], - 'li': ['style'], - 'ol': ['style'], - 'p': ['style'], - 'span': ['style'], - 'strong': [], - 'ul': ['style'], - 'body': [] - }, - css: ['background-color','color','font-family','font-size','font-style','font-weight','margin-left','margin-right','text-align','text-decoration'], - validTag: function(tag) - { - for(var i = 0; i < Strophe.XHTML.tags.length; i++) { - if(tag == Strophe.XHTML.tags[i]) { - return true; - } - } - return false; - }, - validAttribute: function(tag, attribute) - { - if(typeof Strophe.XHTML.attributes[tag] !== 'undefined' && Strophe.XHTML.attributes[tag].length > 0) { - for(var i = 0; i < Strophe.XHTML.attributes[tag].length; i++) { - if(attribute == Strophe.XHTML.attributes[tag][i]) { - return true; - } - } - } - return false; - }, - validCSS: function(style) - { - for(var i = 0; i < Strophe.XHTML.css.length; i++) { - if(style == Strophe.XHTML.css[i]) { - return true; - } - } - return false; - } - }, - - /** 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.ATTACHED - The connection has been attached - */ - Status: { - ERROR: 0, - CONNECTING: 1, - CONNFAIL: 2, - AUTHENTICATING: 3, - AUTHFAIL: 4, - CONNECTED: 5, - DISCONNECTED: 6, - DISCONNECTING: 7, - ATTACHED: 8 - }, - - /** 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.FRAGMENT - XHTML fragment element. - */ - ElementType: { - NORMAL: 1, - TEXT: 3, - CDATA: 4, - FRAGMENT: 11 - }, - - /** 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 - Timeout multiplier. A waiting request will be considered - * failed after Math.floor(TIMEOUT * wait) seconds have elapsed. - * This defaults to 1.1, and with default wait, 66 seconds. - * SECONDARY_TIMEOUT - Secondary timeout multiplier. In cases where - * Strophe can detect early failure, it will consider the request - * failed if it doesn't return after - * Math.floor(SECONDARY_TIMEOUT * wait) seconds have elapsed. - * This defaults to 0.1, and with default wait, 6 seconds. - */ - TIMEOUT: 1.1, - SECONDARY_TIMEOUT: 0.1, - - /** Function: addNamespace - * This function is used to extend the current namespaces in - * Strophe.NS. It takes a key and a value with the key being the - * name of the new namespace, with its actual value. - * For example: - * Strophe.addNamespace('PUBSUB', "http://jabber.org/protocol/pubsub"); - * - * Parameters: - * (String) name - The name under which the namespace will be - * referenced under Strophe.NS - * (String) value - The actual namespace. - */ - addNamespace: function (name, value) - { - Strophe.NS[name] = value; - }, - - /** 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; + xmlGenerator: function () { + if (!Strophe._xmlGenerator) { + Strophe._xmlGenerator = Strophe._makeGenerator(); + } + return Strophe._xmlGenerator; + }, - 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); + /** PrivateFunction: _getIEXmlDom + * Gets IE xml doc object + * + * Returns: + * A Microsoft XML DOM Object + * See Also: + * http://msdn.microsoft.com/en-us/library/ms757837%28VS.85%29.aspx + */ + _getIEXmlDom : function() { + var doc = null; + var docStrings = [ + "Msxml2.DOMDocument.6.0", + "Msxml2.DOMDocument.5.0", + "Msxml2.DOMDocument.4.0", + "MSXML2.DOMDocument.3.0", + "MSXML2.DOMDocument", + "MSXML.DOMDocument", + "Microsoft.XMLDOM" + ]; + + for (var d = 0; d < docStrings.length; d++) { + if (doc === null) { + try { + doc = new ActiveXObject(docStrings[d]); + } catch (e) { + doc = null; } - } - }, - - /** 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 == name; - }, - - /** PrivateVariable: _xmlGenerator - * _Private_ variable that caches a DOM document to - * generate elements. - */ - _xmlGenerator: null, - - /** PrivateFunction: _makeGenerator - * _Private_ function that creates a dummy XML DOM document to serve as - * an element and text node generator. - */ - _makeGenerator: function () { - var doc; - - // IE9 does implement createDocument(); however, using it will cause the browser to leak memory on page unload. - // Here, we test for presence of createDocument() plus IE's proprietary documentMode attribute, which would be - // less than 10 in the case of IE9 and below. - if (document.implementation.createDocument === undefined || - document.implementation.createDocument && document.documentMode && document.documentMode < 10) { - doc = this._getIEXmlDom(); - doc.appendChild(doc.createElement('strophe')); } else { - doc = document.implementation - .createDocument('jabber:client', 'strophe', null); + break; } + } - return doc; - }, + return doc; + }, - /** Function: xmlGenerator - * Get the DOM document to generate elements. - * - * Returns: - * The currently used DOM document. - */ - xmlGenerator: function () { - if (!Strophe._xmlGenerator) { - Strophe._xmlGenerator = Strophe._makeGenerator(); - } - return Strophe._xmlGenerator; - }, - - /** PrivateFunction: _getIEXmlDom - * Gets IE xml doc object - * - * Returns: - * A Microsoft XML DOM Object - * See Also: - * http://msdn.microsoft.com/en-us/library/ms757837%28VS.85%29.aspx - */ - _getIEXmlDom : function() { - var doc = null; - var docStrings = [ - "Msxml2.DOMDocument.6.0", - "Msxml2.DOMDocument.5.0", - "Msxml2.DOMDocument.4.0", - "MSXML2.DOMDocument.3.0", - "MSXML2.DOMDocument", - "MSXML.DOMDocument", - "Microsoft.XMLDOM" - ]; - - for (var d = 0; d < docStrings.length; d++) { - if (doc === null) { - try { - doc = new ActiveXObject(docStrings[d]); - } catch (e) { - doc = null; + /** Function: xmlElement + * Create an XML DOM element. + * + * This function creates an XML DOM element correctly across all + * implementations. Note that these are not HTML DOM elements, which + * aren't appropriate for XMPP stanzas. + * + * Parameters: + * (String) name - The name for the element. + * (Array|Object) attrs - An optional array or object containing + * key/value pairs to use as element attributes. The object should + * be in the format {'key': 'value'} or {key: 'value'}. The array + * should have the format [['key1', 'value1'], ['key2', 'value2']]. + * (String) text - The text child data for the element. + * + * Returns: + * A new XML DOM element. + */ + xmlElement: function (name) + { + if (!name) { return null; } + + var node = Strophe.xmlGenerator().createElement(name); + + // FIXME: this should throw errors if args are the wrong type or + // there are more than two optional args + var a, i, k; + 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]); } - } else { - break; } - } - - return doc; - }, - - /** Function: xmlElement - * Create an XML DOM element. - * - * This function creates an XML DOM element correctly across all - * implementations. Note that these are not HTML DOM elements, which - * aren't appropriate for XMPP stanzas. - * - * Parameters: - * (String) name - The name for the element. - * (Array|Object) attrs - An optional array or object containing - * key/value pairs to use as element attributes. The object should - * be in the format {'key': 'value'} or {key: 'value'}. The array - * should have the format [['key1', 'value1'], ['key2', 'value2']]. - * (String) text - The text child data for the element. - * - * Returns: - * A new XML DOM element. - */ - xmlElement: function (name) - { - if (!name) { return null; } - - var node = Strophe.xmlGenerator().createElement(name); - - // FIXME: this should throw errors if args are the wrong type or - // there are more than two optional args - var a, i, k; - 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]); - } - } - } else if (typeof(arguments[a]) == "object") { - for (k in arguments[a]) { - if (arguments[a].hasOwnProperty(k)) { - node.setAttribute(k, arguments[a][k]); - } + } else if (typeof(arguments[a]) == "object") { + for (k in arguments[a]) { + if (arguments[a].hasOwnProperty(k)) { + node.setAttribute(k, arguments[a][k]); } } } + } - return node; - }, - - /* Function: xmlescape - * Excapes invalid xml characters. - * - * Parameters: - * (String) text - text to escape. - * - * Returns: - * Escaped text. - */ - xmlescape: function(text) - { - text = text.replace(/\&/g, "&"); - text = text.replace(//g, ">"); - text = text.replace(/'/g, "'"); - text = text.replace(/"/g, """); - return text; - }, - - /* Function: xmlunescape - * Unexcapes invalid xml characters. - * - * Parameters: - * (String) text - text to unescape. - * - * Returns: - * Unescaped text. - */ - xmlunescape: function(text) - { - text = text.replace(/\&/g, "&"); - text = text.replace(/</g, "<"); - text = text.replace(/>/g, ">"); - text = text.replace(/'/g, "'"); - text = text.replace(/"/g, "\""); - return text; - }, - - /** 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) - { - return Strophe.xmlGenerator().createTextNode(text); - }, - - /** Function: xmlHtmlNode - * Creates an XML DOM html node. - * - * Parameters: - * (String) html - The content of the html node. - * - * Returns: - * A new XML DOM text node. - */ - xmlHtmlNode: function (html) - { - var node; - //ensure text is escaped - if (window.DOMParser) { - var parser = new DOMParser(); - node = parser.parseFromString(html, "text/xml"); - } else { - node = new ActiveXObject("Microsoft.XMLDOM"); - node.async="false"; - node.loadXML(html); + return node; + }, + + /* Function: xmlescape + * Excapes invalid xml characters. + * + * Parameters: + * (String) text - text to escape. + * + * Returns: + * Escaped text. + */ + xmlescape: function(text) + { + text = text.replace(/\&/g, "&"); + text = text.replace(//g, ">"); + text = text.replace(/'/g, "'"); + text = text.replace(/"/g, """); + return text; + }, + + /* Function: xmlunescape + * Unexcapes invalid xml characters. + * + * Parameters: + * (String) text - text to unescape. + * + * Returns: + * Unescaped text. + */ + xmlunescape: function(text) + { + text = text.replace(/\&/g, "&"); + text = text.replace(/</g, "<"); + text = text.replace(/>/g, ">"); + text = text.replace(/'/g, "'"); + text = text.replace(/"/g, "\""); + return text; + }, + + /** 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) + { + return Strophe.xmlGenerator().createTextNode(text); + }, + + /** Function: xmlHtmlNode + * Creates an XML DOM html node. + * + * Parameters: + * (String) html - The content of the html node. + * + * Returns: + * A new XML DOM text node. + */ + xmlHtmlNode: function (html) + { + var node; + //ensure text is escaped + if (window.DOMParser) { + var parser = new DOMParser(); + node = parser.parseFromString(html, "text/xml"); + } else { + node = new ActiveXObject("Microsoft.XMLDOM"); + node.async="false"; + node.loadXML(html); + } + return node; + }, + + /** 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 node; - }, - - /** 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; + } + + return Strophe.xmlescape(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, + elem.attributes[i].value); } - for (var i = 0; i < elem.childNodes.length; i++) { - if (elem.childNodes[i].nodeType == Strophe.ElementType.TEXT) { - str += elem.childNodes[i].nodeValue; - } + for (i = 0; i < elem.childNodes.length; i++) { + el.appendChild(Strophe.copyElement(elem.childNodes[i])); } + } else if (elem.nodeType == Strophe.ElementType.TEXT) { + el = Strophe.xmlGenerator().createTextNode(elem.nodeValue); + } - return Strophe.xmlescape(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, - elem.attributes[i].value); - } + return el; + }, - for (i = 0; i < elem.childNodes.length; i++) { - el.appendChild(Strophe.copyElement(elem.childNodes[i])); - } - } else if (elem.nodeType == Strophe.ElementType.TEXT) { - el = Strophe.xmlGenerator().createTextNode(elem.nodeValue); - } - return el; - }, - - - /** Function: createHtml - * Copy an HTML DOM element into an XML DOM. - * - * This function copies a DOM element and all its descendants and returns - * the new copy. - * - * Parameters: - * (HTMLElement) elem - A DOM element. - * - * Returns: - * A new, copied DOM element tree. - */ - createHtml: function (elem) - { - var i, el, j, tag, attribute, value, css, cssAttrs, attr, cssName, cssValue; - if (elem.nodeType == Strophe.ElementType.NORMAL) { - tag = elem.nodeName; - if(Strophe.XHTML.validTag(tag)) { - try { - el = Strophe.xmlElement(tag); - for(i = 0; i < Strophe.XHTML.attributes[tag].length; i++) { - attribute = Strophe.XHTML.attributes[tag][i]; - value = elem.getAttribute(attribute); - if(typeof value == 'undefined' || value === null || value === '' || value === false || value === 0) { - continue; + /** Function: createHtml + * Copy an HTML DOM element into an XML DOM. + * + * This function copies a DOM element and all its descendants and returns + * the new copy. + * + * Parameters: + * (HTMLElement) elem - A DOM element. + * + * Returns: + * A new, copied DOM element tree. + */ + createHtml: function (elem) + { + var i, el, j, tag, attribute, value, css, cssAttrs, attr, cssName, cssValue; + if (elem.nodeType == Strophe.ElementType.NORMAL) { + tag = elem.nodeName; + if(Strophe.XHTML.validTag(tag)) { + try { + el = Strophe.xmlElement(tag); + for(i = 0; i < Strophe.XHTML.attributes[tag].length; i++) { + attribute = Strophe.XHTML.attributes[tag][i]; + value = elem.getAttribute(attribute); + if(typeof value == 'undefined' || value === null || value === '' || value === false || value === 0) { + continue; + } + if(attribute == 'style' && typeof value == 'object') { + if(typeof value.cssText != 'undefined') { + value = value.cssText; // we're dealing with IE, need to get CSS out } - if(attribute == 'style' && typeof value == 'object') { - if(typeof value.cssText != 'undefined') { - value = value.cssText; // we're dealing with IE, need to get CSS out + } + // filter out invalid css styles + if(attribute == 'style') { + css = []; + cssAttrs = value.split(';'); + for(j = 0; j < cssAttrs.length; j++) { + attr = cssAttrs[j].split(':'); + cssName = attr[0].replace(/^\s*/, "").replace(/\s*$/, "").toLowerCase(); + if(Strophe.XHTML.validCSS(cssName)) { + cssValue = attr[1].replace(/^\s*/, "").replace(/\s*$/, ""); + css.push(cssName + ': ' + cssValue); } } - // filter out invalid css styles - if(attribute == 'style') { - css = []; - cssAttrs = value.split(';'); - for(j = 0; j < cssAttrs.length; j++) { - attr = cssAttrs[j].split(':'); - cssName = attr[0].replace(/^\s*/, "").replace(/\s*$/, "").toLowerCase(); - if(Strophe.XHTML.validCSS(cssName)) { - cssValue = attr[1].replace(/^\s*/, "").replace(/\s*$/, ""); - css.push(cssName + ': ' + cssValue); - } - } - if(css.length > 0) { - value = css.join('; '); - el.setAttribute(attribute, value); - } - } else { + if(css.length > 0) { + value = css.join('; '); el.setAttribute(attribute, value); } + } else { + el.setAttribute(attribute, value); } - - for (i = 0; i < elem.childNodes.length; i++) { - el.appendChild(Strophe.createHtml(elem.childNodes[i])); - } - } catch(e) { // invalid elements - el = Strophe.xmlTextNode(''); } - } else { - el = Strophe.xmlGenerator().createDocumentFragment(); + for (i = 0; i < elem.childNodes.length; i++) { el.appendChild(Strophe.createHtml(elem.childNodes[i])); } + } catch(e) { // invalid elements + el = Strophe.xmlTextNode(''); } - } else if (elem.nodeType == Strophe.ElementType.FRAGMENT) { + } else { el = Strophe.xmlGenerator().createDocumentFragment(); for (i = 0; i < elem.childNodes.length; i++) { el.appendChild(Strophe.createHtml(elem.childNodes[i])); } - } else if (elem.nodeType == Strophe.ElementType.TEXT) { - el = Strophe.xmlTextNode(elem.nodeValue); - } - - return el; - }, - - /** Function: escapeNode - * Escape the node part (also called local part) of a JID. - * - * Parameters: - * (String) node - A node (or local part). - * - * Returns: - * An escaped node (or local part). - */ - escapeNode: function (node) - { - return node.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"); - }, - - /** Function: unescapeNode - * Unescape a node part (also called local part) of a JID. - * - * Parameters: - * (String) node - A node (or local part). - * - * Returns: - * An unescaped node (or local part). - */ - unescapeNode: function (node) - { - return node.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 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.getBareJidFromJid(jid); - if (bare.indexOf("@") < 0) { - return bare; - } else { - var parts = bare.split("@"); - parts.splice(0, 1); - return parts.join('@'); } - }, - - /** 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 = jid.split("/"); - if (s.length < 2) { return null; } - s.splice(0, 1); - return s.join('/'); - }, - - /** 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 jid ? jid.split("/")[0] : null; - }, - - /** 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. - */ - /* jshint ignore:start */ - log: function (level, msg) - { - return; - }, - /* jshint ignore:end */ - - /** 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; } - - if (typeof(elem.tree) === "function") { - elem = elem.tree(); - } - - 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 + - "='" + elem.attributes[i].value - .replace(/&/g, "&") - .replace(/\'/g, "'") - .replace(/>/g, ">") - .replace(/ 0) { - result += ">"; - for (i = 0; i < elem.childNodes.length; i++) { - child = elem.childNodes[i]; - switch( child.nodeType ){ - case Strophe.ElementType.NORMAL: - // normal element, so recurse - result += Strophe.serialize(child); - break; - case Strophe.ElementType.TEXT: - // text element to escape values - result += Strophe.xmlescape(child.nodeValue); - break; - case Strophe.ElementType.CDATA: - // cdata section so don't escape values - result += ""; - } - } - result += ""; - } else { - result += "/>"; + } else if (elem.nodeType == Strophe.ElementType.FRAGMENT) { + el = Strophe.xmlGenerator().createDocumentFragment(); + for (i = 0; i < elem.childNodes.length; i++) { + el.appendChild(Strophe.createHtml(elem.childNodes[i])); } + } else if (elem.nodeType == Strophe.ElementType.TEXT) { + el = Strophe.xmlTextNode(elem.nodeValue); + } - return result; - }, - - /** PrivateVariable: _requestId - * _Private_ variable that keeps track of the request ids for - * connections. - */ - _requestId: 0, + return el; + }, - /** PrivateVariable: Strophe.connectionPlugins - * _Private_ variable Used to store plugin names that need - * initialization on Strophe.Connection construction. - */ - _connectionPlugins: {}, - - /** Function: addConnectionPlugin - * Extends the Strophe.Connection object with the given plugin. - * - * Parameters: - * (String) name - The name of the extension. - * (Object) ptype - The plugin's prototype. - */ - addConnectionPlugin: function (name, ptype) - { - Strophe._connectionPlugins[name] = ptype; - } - }; - - /** 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'}); + /** Function: escapeNode + * Escape the node part (also called local part) of a JID. * * Parameters: - * (String) name - The name of the root element. - * (Object) attrs - The attributes for the root element in object notation. + * (String) node - A node (or local part). * * Returns: - * A new Strophe.Builder. + * An escaped node (or local part). */ - Strophe.Builder = function (name, attrs) + escapeNode: function (node) { - // Set correct namespace for jabber:client elements - if (name == "presence" || name == "message" || name == "iq") { - if (attrs && !attrs.xmlns) { - attrs.xmlns = Strophe.NS.CLIENT; - } else if (!attrs) { - attrs = {xmlns: Strophe.NS.CLIENT}; - } - } - - // Holds the tree being built. - this.nodeTree = Strophe.xmlElement(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) { - if (moreattrs.hasOwnProperty(k)) { - 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, - * unless text is provided. 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. - * (String) text - The text to add to the child. - * - * Returns: - * The Strophe.Builder object. - */ - c: function (name, attrs, text) - { - var child = Strophe.xmlElement(name, attrs, text); - this.node.appendChild(child); - if (!text) { - 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) - { - var impNode; - var xmlGen = Strophe.xmlGenerator(); - try { - impNode = (xmlGen.importNode !== undefined); - } - catch (e) { - impNode = false; - } - var newElem = impNode ? - xmlGen.importNode(elem, true) : - Strophe.copyElement(elem); - this.node.appendChild(newElem); - this.node = newElem; - 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; - }, - - /** Function: h - * Replace current element contents with the HTML passed in. - * - * This *does not* make the child the new current element - * - * Parameters: - * (String) html - The html to insert as contents of current element. - * - * Returns: - * The Strophe.Builder object. - */ - h: function (html) - { - var fragment = document.createElement('body'); - - // force the browser to try and fix any invalid HTML tags - fragment.innerHTML = html; - - // copy cleaned html into an xml dom - var xhtml = Strophe.createHtml(fragment); - - while(xhtml.childNodes.length > 0) { - this.node.appendChild(xhtml.childNodes[0]); - } - return this; - } - }; - - /** PrivateClass: Strophe.Handler - * _Private_ helper class for managing stanza handlers. + return node.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"); + }, + + /** Function: unescapeNode + * Unescape a node part (also called local part) of a JID. * - * 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. + * Parameters: + * (String) node - A node (or local part). * - * Users will not use Strophe.Handler objects directly, but instead they - * will use Strophe.Connection.addHandler() and - * Strophe.Connection.deleteHandler(). + * Returns: + * An unescaped node (or local part). */ - - /** PrivateConstructor: Strophe.Handler - * Create and initialize a new Strophe.Handler. + unescapeNode: function (node) + { + return node.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: - * (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. - * (Object) options - Handler options + * (String) jid - A JID. * * Returns: - * A new Strophe.Handler object. + * A String containing the node. */ - Strophe.Handler = function (handler, ns, name, type, id, from, options) + getNodeFromJid: function (jid) { - this.handler = handler; - this.ns = ns; - this.name = name; - this.type = type; - this.id = id; - this.options = options || {matchBare: false}; + if (jid.indexOf("@") < 0) { return null; } + return jid.split("@")[0]; + }, - // default matchBare to false if undefined - if (!this.options.matchBare) { - this.options.matchBare = false; - } - - if (this.options.matchBare) { - this.from = from ? Strophe.getBareJidFromJid(from) : null; - } else { - 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; - var from = null; + /** 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.getBareJidFromJid(jid); + if (bare.indexOf("@") < 0) { + return bare; + } else { + var parts = bare.split("@"); + parts.splice(0, 1); + return parts.join('@'); + } + }, - if (this.options.matchBare) { - from = Strophe.getBareJidFromJid(elem.getAttribute('from')); - } else { - from = elem.getAttribute('from'); + /** 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 = jid.split("/"); + if (s.length < 2) { return null; } + s.splice(0, 1); + return s.join('/'); + }, + + /** 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 jid ? jid.split("/")[0] : null; + }, + + /** 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. + */ + /* jshint ignore:start */ + log: function (level, msg) + { + return; + }, + /* jshint ignore:end */ + + /** 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; } + + if (typeof(elem.tree) === "function") { + elem = elem.tree(); + } + + 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 + + "='" + elem.attributes[i].value + .replace(/&/g, "&") + .replace(/\'/g, "'") + .replace(/>/g, ">") + .replace(/ 0) { + result += ">"; + for (i = 0; i < elem.childNodes.length; i++) { + child = elem.childNodes[i]; + switch( child.nodeType ){ + case Strophe.ElementType.NORMAL: + // normal element, so recurse + result += Strophe.serialize(child); + break; + case Strophe.ElementType.TEXT: + // text element to escape values + result += Strophe.xmlescape(child.nodeValue); + break; + case Strophe.ElementType.CDATA: + // cdata section so don't escape values + result += ""; + } } + result += ""; + } else { + result += "/>"; + } - nsMatch = false; - if (!this.ns) { - nsMatch = true; - } else { - var that = this; - Strophe.forEachChild(elem, null, function (elem) { - if (elem.getAttribute("xmlns") == that.ns) { - nsMatch = true; - } - }); + return result; + }, + + /** PrivateVariable: _requestId + * _Private_ variable that keeps track of the request ids for + * connections. + */ + _requestId: 0, + + /** PrivateVariable: Strophe.connectionPlugins + * _Private_ variable Used to store plugin names that need + * initialization on Strophe.Connection construction. + */ + _connectionPlugins: {}, + + /** Function: addConnectionPlugin + * Extends the Strophe.Connection object with the given plugin. + * + * Parameters: + * (String) name - The name of the extension. + * (Object) ptype - The plugin's prototype. + */ + addConnectionPlugin: function (name, ptype) + { + Strophe._connectionPlugins[name] = ptype; + } +}; + +/** 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) +{ + // Set correct namespace for jabber:client elements + if (name == "presence" || name == "message" || name == "iq") { + if (attrs && !attrs.xmlns) { + attrs.xmlns = Strophe.NS.CLIENT; + } else if (!attrs) { + attrs = {xmlns: Strophe.NS.CLIENT}; + } + } + + // Holds the tree being built. + this.nodeTree = Strophe.xmlElement(name, attrs); - nsMatch = nsMatch || elem.getAttribute("xmlns") == this.ns; + // 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) { + if (moreattrs.hasOwnProperty(k)) { + 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, + * unless text is provided. 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. + * (String) text - The text to add to the child. + * + * Returns: + * The Strophe.Builder object. + */ + c: function (name, attrs, text) + { + var child = Strophe.xmlElement(name, attrs, text); + this.node.appendChild(child); + if (!text) { + 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) + { + var impNode; + var xmlGen = Strophe.xmlGenerator(); + try { + impNode = (xmlGen.importNode !== undefined); + } + catch (e) { + impNode = false; + } + var newElem = impNode ? + xmlGen.importNode(elem, true) : + Strophe.copyElement(elem); + this.node.appendChild(newElem); + this.node = newElem; + 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; + }, + + /** Function: h + * Replace current element contents with the HTML passed in. + * + * This *does not* make the child the new current element + * + * Parameters: + * (String) html - The html to insert as contents of current element. + * + * Returns: + * The Strophe.Builder object. + */ + h: function (html) + { + var fragment = document.createElement('body'); + + // force the browser to try and fix any invalid HTML tags + fragment.innerHTML = html; - var elem_type = elem.getAttribute("type"); - if (nsMatch && - (!this.name || Strophe.isTagEqual(elem, this.name)) && - (!this.type || (Array.isArray(this.type) ? this.type.indexOf(elem_type) != -1 : elem_type == this.type)) && - (!this.id || elem.getAttribute("id") == this.id) && - (!this.from || from == this.from)) { - return true; + // copy cleaned html into an xml dom + var xhtml = Strophe.createHtml(fragment); + + while(xhtml.childNodes.length > 0) { + this.node.appendChild(xhtml.childNodes[0]); + } + return this; + } +}; + +/** 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. + * (Object) options - Handler options + * + * Returns: + * A new Strophe.Handler object. + */ +Strophe.Handler = function (handler, ns, name, type, id, from, options) +{ + this.handler = handler; + this.ns = ns; + this.name = name; + this.type = type; + this.id = id; + this.options = options || {matchBare: false}; + + // default matchBare to false if undefined + if (!this.options.matchBare) { + this.options.matchBare = false; + } + + if (this.options.matchBare) { + this.from = from ? Strophe.getBareJidFromJid(from) : null; + } else { + 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; + var from = null; + + if (this.options.matchBare) { + from = Strophe.getBareJidFromJid(elem.getAttribute('from')); + } else { + from = elem.getAttribute('from'); + } + + nsMatch = false; + if (!this.ns) { + nsMatch = true; + } else { + var that = this; + Strophe.forEachChild(elem, null, function (elem) { + if (elem.getAttribute("xmlns") == that.ns) { + nsMatch = true; + } + }); + + nsMatch = nsMatch || elem.getAttribute("xmlns") == this.ns; + } + + var elem_type = elem.getAttribute("type"); + if (nsMatch && + (!this.name || Strophe.isTagEqual(elem, this.name)) && + (!this.type || (Array.isArray(this.type) ? this.type.indexOf(elem_type) != -1 : elem_type == this.type)) && + (!this.id || elem.getAttribute("id") == this.id) && + (!this.from || 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: " + e.message + "\n" + e.stack); } - 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. + 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 +")}"; + } +}; + +/** Class: Strophe.Connection + * XMPP Connection manager. + * + * This 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, SASL SCRAM-SHA1 + * 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. + * + * The transport-protocol for this connection will be chosen automatically + * based on the given service parameter. URLs starting with "ws://" or + * "wss://" will use WebSockets, URLs starting with "http://", "https://" + * or without a protocol will use BOSH. + * + * To make Strophe connect to the current host you can leave out the protocol + * and host part and just pass the path, e.g. + * + * > var conn = new Strophe.Connection("/http-bind/"); + * + * WebSocket options: + * + * If you want to connect to the current host with a WebSocket connection you + * can tell Strophe to use WebSockets through a "protocol" attribute in the + * optional options parameter. Valid values are "ws" for WebSocket and "wss" + * for Secure WebSocket. + * So to connect to "wss://CURRENT_HOSTNAME/xmpp-websocket" you would call + * + * > var conn = new Strophe.Connection("/xmpp-websocket/", {protocol: "wss"}); + * + * Note that relative URLs _NOT_ starting with a "/" will also include the path + * of the current site. + * + * Also because downgrading security is not permitted by browsers, when using + * relative URLs both BOSH and WebSocket connections will use their secure + * variants if the current connection to the site is also secure (https). + * + * BOSH options: + * + * by adding "sync" to the options, you can control if requests will + * be made synchronously or not. The default behaviour is asynchronous. + * If you want to make requests synchronous, make "sync" evaluate to true: + * > var conn = new Strophe.Connection("/http-bind/", {sync: true}); + * You can also toggle this on an already established connection: + * > conn.options.sync = true; + * + * + * Parameters: + * (String) service - The BOSH or WebSocket service URL. + * (Object) options - A hash of configuration options + * + * Returns: + * A new Strophe.Connection object. + */ +Strophe.Connection = function (service, options) +{ + // The service URL + this.service = service; + + // Configuration options + this.options = options || {}; + var proto = this.options.protocol || ""; + + // Select protocal based on service or options + if (service.indexOf("ws:") === 0 || service.indexOf("wss:") === 0 || + proto.indexOf("ws") === 0) { + this._proto = new Strophe.Websocket(this); + } else { + this._proto = new Strophe.Bosh(this); + } + /* The connected JID. */ + this.jid = ""; + /* the JIDs domain */ + this.domain = null; + /* stream:features */ + this.features = null; + + // SASL + this._sasl_data = {}; + this.do_session = false; + this.do_bind = false; + + // handler lists + this.timedHandlers = []; + this.handlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + + this._authentication = {}; + this._idleTimeout = null; + this._disconnectTimeout = null; + + this.do_authentication = true; + this.authenticated = false; + this.disconnecting = false; + this.connected = false; + + this.paused = false; + + this._data = []; + this._uniqueId = 0; + + this._sasl_success_handler = null; + this._sasl_failure_handler = null; + this._sasl_challenge_handler = null; + + // Max retries before disconnecting + this.maxRetries = 5; + + // setup onIdle callback every 1/10th of a second + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + + // initialize plugins + for (var k in Strophe._connectionPlugins) { + if (Strophe._connectionPlugins.hasOwnProperty(k)) { + var ptype = Strophe._connectionPlugins[k]; + // jslint complaints about the below line, but this is fine + var F = function () {}; // jshint ignore:line + F.prototype = ptype; + this[k] = new F(); + this[k].init(this); + } + } +}; + +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._proto._reset(); + + // SASL + this.do_session = false; + this.do_bind = false; + + // handler lists + this.timedHandlers = []; + this.handlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + this._authentication = {}; + + this.authenticated = false; + this.disconnecting = false; + this.connected = false; + + this._data = []; + this._requests = []; + this._uniqueId = 0; + }, + + /** 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 + * BOSH-Connections 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'. + * + * The Parameters _wait_, _hold_ and _route_ are optional and only relevant + * for BOSH connections. Please see XEP 124 for a more detailed explanation + * of the optional parameters. + * + * 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. + * (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). + * (String) route - The optional route value. + */ + connect: function (jid, pass, callback, wait, hold, route) + { + this.jid = jid; + /** Variable: authzid + * Authorization identity. */ - 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: " + e.message + "\n" + e.stack); - } + this.authzid = Strophe.getBareJidFromJid(this.jid); + /** Variable: authcid + * Authentication identity (User name). + */ + this.authcid = Strophe.getNodeFromJid(this.jid); + /** Variable: pass + * Authentication identity (User password). + */ + this.pass = pass; + /** Variable: servtype + * Digest MD5 compatibility. + */ + this.servtype = "xmpp"; + this.connect_callback = callback; + this.disconnecting = false; + this.connected = false; + this.authenticated = false; - throw e; - } + // parse jid for domain + this.domain = Strophe.getDomainFromJid(this.jid); - return result; - }, + this._changeConnectStatus(Strophe.Status.CONNECTING, null); - /** 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 + ")}"; - } - }; + this._proto._connect(wait, hold, route); + }, - /** 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. + /** Function: attach + * Attach to an already created and authenticated BOSH session. * - * 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. + * 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: - * (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. + * (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. + * (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. */ - Strophe.TimedHandler = function (period, handler) + attach: function (jid, sid, rid, callback, wait, hold, wind) { - this.period = period; - this.handler = handler; + this._proto._attach(jid, sid, rid, callback, wait, hold, wind); + }, - 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 +")}"; - } - }; - - /** Class: Strophe.Connection - * XMPP Connection manager. + /** Function: xmlInput + * User overrideable function that receives XML data coming into the + * connection. * - * This 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, SASL SCRAM-SHA1 - * and legacy authentication. + * The default function does nothing. User code can override this with + * > Strophe.Connection.xmlInput = function (elem) { + * > (user code) + * > }; * - * 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. + * Due to limitations of current Browsers' XML-Parsers the opening and closing + * tag for WebSocket-Connoctions will be passed as selfclosing here. * - * 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. + * BOSH-Connections will have all stanzas wrapped in a tag. See + * if you want to strip this tag. * - * To send data to the connection, use send(). + * Parameters: + * (XMLElement) elem - The XML data received by the connection. */ + /* jshint unused:false */ + xmlInput: function (elem) + { + return; + }, + /* jshint unused:true */ - /** Constructor: Strophe.Connection - * Create and initialize a Strophe.Connection object. + /** Function: xmlOutput + * User overrideable function that receives XML data sent to the + * connection. * - * The transport-protocol for this connection will be chosen automatically - * based on the given service parameter. URLs starting with "ws://" or - * "wss://" will use WebSockets, URLs starting with "http://", "https://" - * or without a protocol will use BOSH. + * The default function does nothing. User code can override this with + * > Strophe.Connection.xmlOutput = function (elem) { + * > (user code) + * > }; * - * To make Strophe connect to the current host you can leave out the protocol - * and host part and just pass the path, e.g. + * Due to limitations of current Browsers' XML-Parsers the opening and closing + * tag for WebSocket-Connoctions will be passed as selfclosing here. * - * > var conn = new Strophe.Connection("/http-bind/"); + * BOSH-Connections will have all stanzas wrapped in a tag. See + * if you want to strip this tag. * - * WebSocket options: - * - * If you want to connect to the current host with a WebSocket connection you - * can tell Strophe to use WebSockets through a "protocol" attribute in the - * optional options parameter. Valid values are "ws" for WebSocket and "wss" - * for Secure WebSocket. - * So to connect to "wss://CURRENT_HOSTNAME/xmpp-websocket" you would call - * - * > var conn = new Strophe.Connection("/xmpp-websocket/", {protocol: "wss"}); + * Parameters: + * (XMLElement) elem - The XMLdata sent by the connection. + */ + /* jshint unused:false */ + xmlOutput: function (elem) + { + return; + }, + /* jshint unused:true */ + + /** Function: rawInput + * User overrideable function that receives raw data coming into the + * connection. * - * Note that relative URLs _NOT_ starting with a "/" will also include the path - * of the current site. + * The default function does nothing. User code can override this with + * > Strophe.Connection.rawInput = function (data) { + * > (user code) + * > }; * - * Also because downgrading security is not permitted by browsers, when using - * relative URLs both BOSH and WebSocket connections will use their secure - * variants if the current connection to the site is also secure (https). + * Parameters: + * (String) data - The data received by the connection. + */ + /* jshint unused:false */ + rawInput: function (data) + { + return; + }, + /* jshint unused:true */ + + /** Function: rawOutput + * User overrideable function that receives raw data sent to the + * connection. * - * BOSH options: + * The default function does nothing. User code can override this with + * > Strophe.Connection.rawOutput = function (data) { + * > (user code) + * > }; * - * by adding "sync" to the options, you can control if requests will - * be made synchronously or not. The default behaviour is asynchronous. - * If you want to make requests synchronous, make "sync" evaluate to true: - * > var conn = new Strophe.Connection("/http-bind/", {sync: true}); - * You can also toggle this on an already established connection: - * > conn.options.sync = true; + * Parameters: + * (String) data - The data sent by the connection. + */ + /* jshint unused:false */ + rawOutput: function (data) + { + return; + }, + /* jshint unused:true */ + + /** 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: - * (String) service - The BOSH or WebSocket service URL. - * (Object) options - A hash of configuration options - * - * Returns: - * A new Strophe.Connection object. + * (XMLElement | + * [XMLElement] | + * Strophe.Builder) elem - The stanza to send. */ - Strophe.Connection = function (service, options) + send: function (elem) { - // The service URL - this.service = service; - - // Configuration options - this.options = options || {}; - var proto = this.options.protocol || ""; - - // Select protocal based on service or options - if (service.indexOf("ws:") === 0 || service.indexOf("wss:") === 0 || - proto.indexOf("ws") === 0) { - this._proto = new Strophe.Websocket(this); + if (elem === null) { return ; } + if (typeof(elem.sort) === "function") { + for (var i = 0; i < elem.length; i++) { + this._queueData(elem[i]); + } + } else if (typeof(elem.tree) === "function") { + this._queueData(elem.tree()); } else { - this._proto = new Strophe.Bosh(this); + this._queueData(elem); } - /* The connected JID. */ - this.jid = ""; - /* the JIDs domain */ - this.domain = null; - /* stream:features */ - this.features = null; - - // SASL - this._sasl_data = {}; - this.do_session = false; - this.do_bind = false; - - // handler lists - this.timedHandlers = []; - this.handlers = []; - this.removeTimeds = []; - this.removeHandlers = []; - this.addTimeds = []; - this.addHandlers = []; - - this._authentication = {}; - this._idleTimeout = null; - this._disconnectTimeout = null; - - this.do_authentication = true; - this.authenticated = false; - this.disconnecting = false; - this.connected = false; - this.paused = false; + this._proto._send(); + }, - this._data = []; - this._uniqueId = 0; + /** Function: flush + * Immediately send any pending outgoing data. + * + * Normally send() queues outgoing data until the next idle period + * (100ms), which optimizes network use in the common cases when + * several send()s are called in succession. flush() can be used to + * immediately send all pending data. + */ + flush: function () + { + // cancel the pending idle period and run the idle function + // immediately + clearTimeout(this._idleTimeout); + this._onIdle(); + }, + + /** Function: sendIQ + * Helper function to send IQ stanzas. + * + * Parameters: + * (XMLElement) elem - The stanza to send. + * (Function) callback - The callback function for a successful request. + * (Function) errback - The callback function for a failed or timed + * out request. On timeout, the stanza will be null. + * (Integer) timeout - The time specified in milliseconds for a + * timeout to occur. + * + * Returns: + * The id used to send the IQ. + */ + sendIQ: function(elem, callback, errback, timeout) { + var timeoutHandler = null; + var that = this; + + if (typeof(elem.tree) === "function") { + elem = elem.tree(); + } + var id = elem.getAttribute('id'); - this._sasl_success_handler = null; - this._sasl_failure_handler = null; - this._sasl_challenge_handler = null; + // inject id if not found + if (!id) { + id = this.getUniqueId("sendIQ"); + elem.setAttribute("id", id); + } - // Max retries before disconnecting - this.maxRetries = 5; + var expectedFrom = elem.getAttribute("to"); + var fulljid = this.jid; - // setup onIdle callback every 1/10th of a second - this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + var handler = this.addHandler(function (stanza) { + // remove timeout handler if there is one + if (timeoutHandler) { + that.deleteTimedHandler(timeoutHandler); + } - // initialize plugins - for (var k in Strophe._connectionPlugins) { - if (Strophe._connectionPlugins.hasOwnProperty(k)) { - var ptype = Strophe._connectionPlugins[k]; - // jslint complaints about the below line, but this is fine - var F = function () {}; // jshint ignore:line - F.prototype = ptype; - this[k] = new F(); - this[k].init(this); + var acceptable = false; + var from = stanza.getAttribute("from"); + if (from === expectedFrom || + (expectedFrom === null && + (from === Strophe.getBareJidFromJid(fulljid) || + from === Strophe.getDomainFromJid(fulljid) || + from === fulljid))) { + acceptable = true; } - } - }; - 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._proto._reset(); - - // SASL - this.do_session = false; - this.do_bind = false; - - // handler lists - this.timedHandlers = []; - this.handlers = []; - this.removeTimeds = []; - this.removeHandlers = []; - this.addTimeds = []; - this.addHandlers = []; - this._authentication = {}; - - this.authenticated = false; - this.disconnecting = false; - this.connected = false; - - this._data = []; - this._requests = []; - this._uniqueId = 0; - }, - - /** 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 - * BOSH-Connections 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 + ""; + if (!acceptable) { + throw { + name: "StropheError", + message: "Got answer to IQ from wrong jid:" + from + + "\nExpected jid: " + expectedFrom + }; } - }, - - /** 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'. - * - * The Parameters _wait_, _hold_ and _route_ are optional and only relevant - * for BOSH connections. Please see XEP 124 for a more detailed explanation - * of the optional parameters. - * - * 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. - * (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). - * (String) route - The optional route value. - */ - connect: function (jid, pass, callback, wait, hold, route) - { - this.jid = jid; - /** Variable: authzid - * Authorization identity. - */ - this.authzid = Strophe.getBareJidFromJid(this.jid); - /** Variable: authcid - * Authentication identity (User name). - */ - this.authcid = Strophe.getNodeFromJid(this.jid); - /** Variable: pass - * Authentication identity (User password). - */ - this.pass = pass; - /** Variable: servtype - * Digest MD5 compatibility. - */ - this.servtype = "xmpp"; - this.connect_callback = callback; - this.disconnecting = false; - this.connected = false; - this.authenticated = false; - - // parse jid for domain - this.domain = Strophe.getDomainFromJid(this.jid); - - this._changeConnectStatus(Strophe.Status.CONNECTING, null); - - this._proto._connect(wait, hold, route); - }, - - /** 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. - * (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. - */ - attach: function (jid, sid, rid, callback, wait, hold, wind) - { - this._proto._attach(jid, sid, rid, callback, wait, hold, wind); - }, - - /** Function: xmlInput - * User overrideable function that receives XML data coming into the - * connection. - * - * The default function does nothing. User code can override this with - * > Strophe.Connection.xmlInput = function (elem) { - * > (user code) - * > }; - * - * Due to limitations of current Browsers' XML-Parsers the opening and closing - * tag for WebSocket-Connoctions will be passed as selfclosing here. - * - * BOSH-Connections will have all stanzas wrapped in a tag. See - * if you want to strip this tag. - * - * Parameters: - * (XMLElement) elem - The XML data received by the connection. - */ - /* jshint unused:false */ - xmlInput: function (elem) - { - return; - }, - /* jshint unused:true */ - - /** Function: xmlOutput - * User overrideable function that receives XML data sent to the - * connection. - * - * The default function does nothing. User code can override this with - * > Strophe.Connection.xmlOutput = function (elem) { - * > (user code) - * > }; - * - * Due to limitations of current Browsers' XML-Parsers the opening and closing - * tag for WebSocket-Connoctions will be passed as selfclosing here. - * - * BOSH-Connections will have all stanzas wrapped in a tag. See - * if you want to strip this tag. - * - * Parameters: - * (XMLElement) elem - The XMLdata sent by the connection. - */ - /* jshint unused:false */ - xmlOutput: function (elem) - { - return; - }, - /* jshint unused: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. - */ - /* jshint unused:false */ - rawInput: function (data) - { - return; - }, - /* jshint unused:true */ - - /** 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. - */ - /* jshint unused:false */ - rawOutput: function (data) - { - return; - }, - /* jshint unused:true */ - - /** 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 | - * [XMLElement] | - * Strophe.Builder) elem - The stanza to send. - */ - send: function (elem) - { - if (elem === null) { return ; } - if (typeof(elem.sort) === "function") { - for (var i = 0; i < elem.length; i++) { - this._queueData(elem[i]); + + var iqtype = stanza.getAttribute('type'); + if (iqtype == 'result') { + if (callback) { + callback(stanza); + } + } else if (iqtype == 'error') { + if (errback) { + errback(stanza); } - } else if (typeof(elem.tree) === "function") { - this._queueData(elem.tree()); } else { - this._queueData(elem); + throw { + name: "StropheError", + message: "Got bad IQ type of " + iqtype + }; } + }, null, 'iq', ['error', 'result'], id); - this._proto._send(); - }, + // if timeout specified, setup timeout handler. + if (timeout) { + timeoutHandler = this.addTimedHandler(timeout, function () { + // get rid of normal handler + that.deleteHandler(handler); + // call errback on timeout with null stanza + if (errback) { + errback(null); + } + return false; + }); + } + this.send(elem); + return id; + }, - /** Function: flush - * Immediately send any pending outgoing data. - * - * Normally send() queues outgoing data until the next idle period - * (100ms), which optimizes network use in the common cases when - * several send()s are called in succession. flush() can be used to - * immediately send all pending data. - */ - flush: function () - { - // cancel the pending idle period and run the idle function - // immediately - clearTimeout(this._idleTimeout); - this._onIdle(); - }, - - /** Function: sendIQ - * Helper function to send IQ stanzas. - * - * Parameters: - * (XMLElement) elem - The stanza to send. - * (Function) callback - The callback function for a successful request. - * (Function) errback - The callback function for a failed or timed - * out request. On timeout, the stanza will be null. - * (Integer) timeout - The time specified in milliseconds for a - * timeout to occur. - * - * Returns: - * The id used to send the IQ. - */ - sendIQ: function(elem, callback, errback, timeout) { - var timeoutHandler = null; - var that = this; + /** PrivateFunction: _queueData + * Queue outgoing data for later sending. Also ensures that the data + * is a DOMElement. + */ + _queueData: function (element) { + if (element === null || + !element.tagName || + !element.childNodes) { + throw { + name: "StropheError", + message: "Cannot queue non-DOMElement." + }; + } - if (typeof(elem.tree) === "function") { - elem = elem.tree(); - } - var id = elem.getAttribute('id'); + this._data.push(element); + }, - // inject id if not found - if (!id) { - id = this.getUniqueId("sendIQ"); - elem.setAttribute("id", id); - } + /** PrivateFunction: _sendRestart + * Send an xmpp:restart stanza. + */ + _sendRestart: function () + { + this._data.push("restart"); - var expectedFrom = elem.getAttribute("to"); - var fulljid = this.jid; + this._proto._sendRestart(); - var handler = this.addHandler(function (stanza) { - // remove timeout handler if there is one - if (timeoutHandler) { - that.deleteTimedHandler(timeoutHandler); - } + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + }, - var acceptable = false; - var from = stanza.getAttribute("from"); - if (from === expectedFrom || - (expectedFrom === null && - (from === Strophe.getBareJidFromJid(fulljid) || - from === Strophe.getDomainFromJid(fulljid) || - from === fulljid))) { - acceptable = true; - } + /** 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; + }, - if (!acceptable) { - throw { - name: "StropheError", - message: "Got answer to IQ from wrong jid:" + from + - "\nExpected jid: " + expectedFrom - }; - } + /** 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); + }, - var iqtype = stanza.getAttribute('type'); - if (iqtype == 'result') { - if (callback) { - callback(stanza); - } - } else if (iqtype == 'error') { - if (errback) { - errback(stanza); + /** 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 options argument contains handler matching flags that affect how + * matches are determined. Currently the only flag is matchBare (a + * boolean). When matchBare is true, the from parameter and the from + * attribute on the stanza will be matched as bare JIDs instead of + * full JIDs. To use this, pass {matchBare: true} as the value of + * options. The default value for matchBare is false. + * + * 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. + * (String) options - The handler options + * + * Returns: + * A reference to the handler that can be used to remove it. + */ + addHandler: function (handler, ns, name, type, id, from, options) + { + var hand = new Strophe.Handler(handler, ns, name, type, id, from, options); + 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); + // If a handler is being deleted while it is being added, + // prevent it from getting added + var i = this.addHandlers.indexOf(handRef); + if (i >= 0) { + this.addHandlers.splice(i, 1); + } + }, + + /** 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. + * + * Parameters: + * (String) reason - The reason the disconnect is occuring. + */ + disconnect: function (reason) + { + this._changeConnectStatus(Strophe.Status.DISCONNECTING, reason); + + Strophe.info("Disconnect was called because: " + reason); + if (this.connected) { + var pres = false; + this.disconnecting = true; + if (this.authenticated) { + pres = $pres({ + xmlns: Strophe.NS.CLIENT, + type: 'unavailable' + }); + } + // setup timeout handler + this._disconnectTimeout = this._addSysTimedHandler( + 3000, this._onDisconnectTimeout.bind(this)); + this._proto._disconnect(pres); + } + }, + + /** PrivateFunction: _changeConnectStatus + * _Private_ helper function that makes sure plugins and the user's + * callback are notified of connection status changes. + * + * Parameters: + * (Integer) status - the new connection status, one of the values + * in Strophe.Status + * (String) condition - the error condition or null + */ + _changeConnectStatus: function (status, condition) + { + // notify all plugins listening for status changes + for (var k in Strophe._connectionPlugins) { + if (Strophe._connectionPlugins.hasOwnProperty(k)) { + var plugin = this[k]; + if (plugin.statusChanged) { + try { + plugin.statusChanged(status, condition); + } catch (err) { + Strophe.error("" + k + " plugin caused an exception " + + "changing status: " + err); } - } else { - throw { - name: "StropheError", - message: "Got bad IQ type of " + iqtype - }; } - }, null, 'iq', ['error', 'result'], id); - - // if timeout specified, setup timeout handler. - if (timeout) { - timeoutHandler = this.addTimedHandler(timeout, function () { - // get rid of normal handler - that.deleteHandler(handler); - // call errback on timeout with null stanza - if (errback) { - errback(null); - } - return false; - }); } - this.send(elem); - return id; - }, + } - /** PrivateFunction: _queueData - * Queue outgoing data for later sending. Also ensures that the data - * is a DOMElement. - */ - _queueData: function (element) { - if (element === null || - !element.tagName || - !element.childNodes) { - throw { - name: "StropheError", - message: "Cannot queue non-DOMElement." - }; + // notify the user's callback + if (this.connect_callback) { + try { + this.connect_callback(status, condition); + } catch (e) { + Strophe.error("User connection callback caused an " + + "exception: " + e); } + } + }, - this._data.push(element); - }, + /** 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 () + { + if (typeof this._idleTimeout == "number") { + clearTimeout(this._idleTimeout); + } - /** PrivateFunction: _sendRestart - * Send an xmpp:restart stanza. - */ - _sendRestart: function () - { - this._data.push("restart"); + // Cancel Disconnect Timeout + if (this._disconnectTimeout !== null) { + this.deleteTimedHandler(this._disconnectTimeout); + this._disconnectTimeout = null; + } - this._proto._sendRestart(); + Strophe.info("_doDisconnect was called"); + this._proto._doDisconnect(); - 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 options argument contains handler matching flags that affect how - * matches are determined. Currently the only flag is matchBare (a - * boolean). When matchBare is true, the from parameter and the from - * attribute on the stanza will be matched as bare JIDs instead of - * full JIDs. To use this, pass {matchBare: true} as the value of - * options. The default value for matchBare is false. - * - * 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. - * (String) options - The handler options - * - * Returns: - * A reference to the handler that can be used to remove it. - */ - addHandler: function (handler, ns, name, type, id, from, options) - { - var hand = new Strophe.Handler(handler, ns, name, type, id, from, options); - 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); - // If a handler is being deleted while it is being added, - // prevent it from getting added - var i = this.addHandlers.indexOf(handRef); - if (i >= 0) { - this.addHandlers.splice(i, 1); - } - }, - - /** 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. - * - * Parameters: - * (String) reason - The reason the disconnect is occuring. - */ - disconnect: function (reason) - { - this._changeConnectStatus(Strophe.Status.DISCONNECTING, reason); - - Strophe.info("Disconnect was called because: " + reason); - if (this.connected) { - var pres = false; - this.disconnecting = true; - if (this.authenticated) { - pres = $pres({ - xmlns: Strophe.NS.CLIENT, - type: 'unavailable' - }); - } - // setup timeout handler - this._disconnectTimeout = this._addSysTimedHandler( - 3000, this._onDisconnectTimeout.bind(this)); - this._proto._disconnect(pres); - } - }, - - /** PrivateFunction: _changeConnectStatus - * _Private_ helper function that makes sure plugins and the user's - * callback are notified of connection status changes. - * - * Parameters: - * (Integer) status - the new connection status, one of the values - * in Strophe.Status - * (String) condition - the error condition or null - */ - _changeConnectStatus: function (status, condition) - { - // notify all plugins listening for status changes - for (var k in Strophe._connectionPlugins) { - if (Strophe._connectionPlugins.hasOwnProperty(k)) { - var plugin = this[k]; - if (plugin.statusChanged) { - try { - plugin.statusChanged(status, condition); - } catch (err) { - Strophe.error("" + k + " plugin caused an exception " + - "changing status: " + err); - } - } - } - } + this.authenticated = false; + this.disconnecting = false; - // notify the user's callback - if (this.connect_callback) { - try { - this.connect_callback(status, condition); - } catch (e) { - Strophe.error("User connection callback caused an " + - "exception: " + e); - } - } - }, + // delete handlers + this.handlers = []; + this.timedHandlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; - /** 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 () - { - if (typeof this._idleTimeout == "number") { - clearTimeout(this._idleTimeout); - } + // tell the parent we disconnected + this._changeConnectStatus(Strophe.Status.DISCONNECTED, null); + this.connected = false; + }, - // Cancel Disconnect Timeout - if (this._disconnectTimeout !== null) { - this.deleteTimedHandler(this._disconnectTimeout); - this._disconnectTimeout = null; - } + /** 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. + * (string) req - The stanza a raw string (optiona). + */ + _dataRecv: function (req, raw) + { + Strophe.info("_dataRecv called"); + var elem = this._proto._reqToData(req); + if (elem === null) { return; } - Strophe.info("_doDisconnect was called"); - this._proto._doDisconnect(); - - this.authenticated = false; - this.disconnecting = false; - - // delete handlers - this.handlers = []; - this.timedHandlers = []; - this.removeTimeds = []; - this.removeHandlers = []; - this.addTimeds = []; - this.addHandlers = []; - - // tell the parent we disconnected - this._changeConnectStatus(Strophe.Status.DISCONNECTED, null); - this.connected = false; - }, - - /** 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. - * (string) req - The stanza a raw string (optiona). - */ - _dataRecv: function (req, raw) - { - Strophe.info("_dataRecv called"); - var elem = this._proto._reqToData(req); - if (elem === null) { return; } - - if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) { - if (elem.nodeName === this._proto.strip && elem.childNodes.length) { - this.xmlInput(elem.childNodes[0]); - } else { - this.xmlInput(elem); - } + if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) { + if (elem.nodeName === this._proto.strip && elem.childNodes.length) { + this.xmlInput(elem.childNodes[0]); + } else { + this.xmlInput(elem); } - if (this.rawInput !== Strophe.Connection.prototype.rawInput) { - if (raw) { - this.rawInput(raw); - } else { - this.rawInput(Strophe.serialize(elem)); - } + } + if (this.rawInput !== Strophe.Connection.prototype.rawInput) { + if (raw) { + this.rawInput(raw); + } else { + this.rawInput(Strophe.serialize(elem)); } + } - // 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); - } + // 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()); - } + // add handlers scheduled for addition + while (this.addHandlers.length > 0) { + this.handlers.push(this.addHandlers.pop()); + } - // handle graceful disconnect - if (this.disconnecting && this._proto._emptyQueue()) { - this._doDisconnect(); + // handle graceful disconnect + if (this.disconnecting && this._proto._emptyQueue()) { + this._doDisconnect(); + return; + } + + var type = elem.getAttribute("type"); + var cond, conflict; + if (type !== null && type == "terminate") { + // Don't process stanzas that come in after disconnect + if (this.disconnecting) { return; } - var type = elem.getAttribute("type"); - var cond, conflict; - if (type !== null && type == "terminate") { - // Don't process stanzas that come in after disconnect - if (this.disconnecting) { - return; - } - - // 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._changeConnectStatus(Strophe.Status.CONNFAIL, cond); - } else { - this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); + // 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._doDisconnect(); - return; + this._changeConnectStatus(Strophe.Status.CONNFAIL, cond); + } else { + this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); } + this._doDisconnect(); + return; + } - // send each incoming stanza through the handler chain - var that = this; - Strophe.forEachChild(elem, null, function (child) { - var i, newList; - // process handlers - newList = that.handlers; - that.handlers = []; - for (i = 0; i < newList.length; i++) { - var hand = newList[i]; - // encapsulate 'handler.run' not to lose the whole handler list if - // one of the handlers throws an exception - try { - if (hand.isMatch(child) && - (that.authenticated || !hand.user)) { - if (hand.run(child)) { - that.handlers.push(hand); - } - } else { + // send each incoming stanza through the handler chain + var that = this; + Strophe.forEachChild(elem, null, function (child) { + var i, newList; + // process handlers + newList = that.handlers; + that.handlers = []; + for (i = 0; i < newList.length; i++) { + var hand = newList[i]; + // encapsulate 'handler.run' not to lose the whole handler list if + // one of the handlers throws an exception + try { + if (hand.isMatch(child) && + (that.authenticated || !hand.user)) { + if (hand.run(child)) { that.handlers.push(hand); } - } catch(e) { - // if the handler throws an exception, we consider it as false - Strophe.warn('Removing Strophe handlers due to uncaught exception: ' + e.message); + } else { + that.handlers.push(hand); } + } catch(e) { + // if the handler throws an exception, we consider it as false + Strophe.warn('Removing Strophe handlers due to uncaught exception: ' + e.message); } - }); - }, + } + }); + }, - /** Attribute: mechanisms - * SASL Mechanisms available for Conncection. - */ - mechanisms: {}, - - /** 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. - * (Function) _callback - low level (xmpp) connect callback function. - * Useful for plugins with their own xmpp connect callback (when their) - * want to do something special). - */ - _connect_cb: function (req, _callback, raw) - { - Strophe.info("_connect_cb was called"); + /** Attribute: mechanisms + * SASL Mechanisms available for Conncection. + */ + mechanisms: {}, + + /** 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. + * (Function) _callback - low level (xmpp) connect callback function. + * Useful for plugins with their own xmpp connect callback (when their) + * want to do something special). + */ + _connect_cb: function (req, _callback, raw) + { + Strophe.info("_connect_cb was called"); - this.connected = true; + this.connected = true; - var bodyWrap = this._proto._reqToData(req); - if (!bodyWrap) { return; } + var bodyWrap = this._proto._reqToData(req); + if (!bodyWrap) { return; } - if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) { - if (bodyWrap.nodeName === this._proto.strip && bodyWrap.childNodes.length) { - this.xmlInput(bodyWrap.childNodes[0]); - } else { - this.xmlInput(bodyWrap); - } + if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) { + if (bodyWrap.nodeName === this._proto.strip && bodyWrap.childNodes.length) { + this.xmlInput(bodyWrap.childNodes[0]); + } else { + this.xmlInput(bodyWrap); } - if (this.rawInput !== Strophe.Connection.prototype.rawInput) { - if (raw) { - this.rawInput(raw); - } else { - this.rawInput(Strophe.serialize(bodyWrap)); - } + } + if (this.rawInput !== Strophe.Connection.prototype.rawInput) { + if (raw) { + this.rawInput(raw); + } else { + this.rawInput(Strophe.serialize(bodyWrap)); } + } - var conncheck = this._proto._connect_cb(bodyWrap); - if (conncheck === Strophe.Status.CONNFAIL) { - return; - } + var conncheck = this._proto._connect_cb(bodyWrap); + if (conncheck === Strophe.Status.CONNFAIL) { + return; + } - this._authentication.sasl_scram_sha1 = false; - this._authentication.sasl_plain = false; - this._authentication.sasl_digest_md5 = false; - this._authentication.sasl_anonymous = false; + this._authentication.sasl_scram_sha1 = false; + this._authentication.sasl_plain = false; + this._authentication.sasl_digest_md5 = false; + this._authentication.sasl_anonymous = false; - this._authentication.legacy_auth = false; + this._authentication.legacy_auth = false; - // Check for the stream:features tag - var hasFeatures = bodyWrap.getElementsByTagNameNS(Strophe.NS.STREAM, "features").length > 0; - var mechanisms = bodyWrap.getElementsByTagName("mechanism"); - var matched = []; - var i, mech, found_authentication = false; - if (!hasFeatures) { - this._proto._no_auth_received(_callback); - return; - } - if (mechanisms.length > 0) { - for (i = 0; i < mechanisms.length; i++) { - mech = Strophe.getText(mechanisms[i]); - if (this.mechanisms[mech]) matched.push(this.mechanisms[mech]); - } - } - this._authentication.legacy_auth = - bodyWrap.getElementsByTagName("auth").length > 0; - found_authentication = this._authentication.legacy_auth || - matched.length > 0; - if (!found_authentication) { - this._proto._no_auth_received(_callback); - return; - } - if (this.do_authentication !== false) - this.authenticate(matched); - }, - - /** Function: authenticate - * Set up authentication - * - * Contiunues the initial connection request by setting up authentication - * handlers and start the authentication process. - * - * SASL authentication will be attempted if available, otherwise - * the code will fall back to legacy authentication. - * - */ - authenticate: function (matched) - { - var i; - // Sorting matched mechanisms according to priority. - for (i = 0; i < matched.length - 1; ++i) { - var higher = i; - for (var j = i + 1; j < matched.length; ++j) { - if (matched[j].prototype.priority > matched[higher].prototype.priority) { - higher = j; - } - } - if (higher != i) { - var swap = matched[i]; - matched[i] = matched[higher]; - matched[higher] = swap; + // Check for the stream:features tag + var hasFeatures = bodyWrap.getElementsByTagNameNS(Strophe.NS.STREAM, "features").length > 0; + var mechanisms = bodyWrap.getElementsByTagName("mechanism"); + var matched = []; + var i, mech, found_authentication = false; + if (!hasFeatures) { + this._proto._no_auth_received(_callback); + return; + } + if (mechanisms.length > 0) { + for (i = 0; i < mechanisms.length; i++) { + mech = Strophe.getText(mechanisms[i]); + if (this.mechanisms[mech]) matched.push(this.mechanisms[mech]); } - } + } + this._authentication.legacy_auth = + bodyWrap.getElementsByTagName("auth").length > 0; + found_authentication = this._authentication.legacy_auth || + matched.length > 0; + if (!found_authentication) { + this._proto._no_auth_received(_callback); + return; + } + if (this.do_authentication !== false) + this.authenticate(matched); + }, - // run each mechanism - var mechanism_found = false; - for (i = 0; i < matched.length; ++i) { - if (!matched[i].test(this)) continue; - - 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._sasl_challenge_handler = this._addSysHandler( - this._sasl_challenge_cb.bind(this), null, - "challenge", null, null); - - this._sasl_mechanism = new matched[i](); - this._sasl_mechanism.onStart(this); - - var request_auth_exchange = $build("auth", { - xmlns: Strophe.NS.SASL, - mechanism: this._sasl_mechanism.name - }); + /** Function: authenticate + * Set up authentication + * + * Contiunues the initial connection request by setting up authentication + * handlers and start the authentication process. + * + * SASL authentication will be attempted if available, otherwise + * the code will fall back to legacy authentication. + * + */ + authenticate: function (matched) + { + var i; + // Sorting matched mechanisms according to priority. + for (i = 0; i < matched.length - 1; ++i) { + var higher = i; + for (var j = i + 1; j < matched.length; ++j) { + if (matched[j].prototype.priority > matched[higher].prototype.priority) { + higher = j; + } + } + if (higher != i) { + var swap = matched[i]; + matched[i] = matched[higher]; + matched[higher] = swap; + } + } - if (this._sasl_mechanism.isClientFirst) { - var response = this._sasl_mechanism.onChallenge(this, null); - request_auth_exchange.t(Base64.encode(response)); - } + // run each mechanism + var mechanism_found = false; + for (i = 0; i < matched.length; ++i) { + if (!matched[i].test(this)) continue; + + 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._sasl_challenge_handler = this._addSysHandler( + this._sasl_challenge_cb.bind(this), null, + "challenge", null, null); + + this._sasl_mechanism = new matched[i](); + this._sasl_mechanism.onStart(this); + + var request_auth_exchange = $build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: this._sasl_mechanism.name + }); - this.send(request_auth_exchange.tree()); + if (this._sasl_mechanism.isClientFirst) { + var response = this._sasl_mechanism.onChallenge(this, null); + request_auth_exchange.t(Base64.encode(response)); + } - mechanism_found = true; - break; - } + this.send(request_auth_exchange.tree()); - if (!mechanism_found) { - // if none of the mechanism worked - if (Strophe.getNodeFromJid(this.jid) === null) { - // we don't have a node, which is required for non-anonymous - // client connections - this._changeConnectStatus(Strophe.Status.CONNFAIL, - 'x-strophe-bad-non-anon-jid'); - this.disconnect('x-strophe-bad-non-anon-jid'); - } else { - // fall back to legacy authentication - this._changeConnectStatus(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()); - } - } + mechanism_found = true; + break; + } - }, + if (!mechanism_found) { + // if none of the mechanism worked + if (Strophe.getNodeFromJid(this.jid) === null) { + // we don't have a node, which is required for non-anonymous + // client connections + this._changeConnectStatus(Strophe.Status.CONNFAIL, + 'x-strophe-bad-non-anon-jid'); + this.disconnect('x-strophe-bad-non-anon-jid'); + } else { + // fall back to legacy authentication + this._changeConnectStatus(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()); + } + } - _sasl_challenge_cb: function(elem) { - var challenge = Base64.decode(Strophe.getText(elem)); - var response = this._sasl_mechanism.onChallenge(this, challenge); + }, - var stanza = $build('response', { - xmlns: Strophe.NS.SASL - }); - if (response !== "") { - stanza.t(Base64.encode(response)); - } - this.send(stanza.tree()); - - return true; - }, - - /** 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. - */ - /* jshint unused:false */ - _auth1_cb: function (elem) - { - // build plaintext auth iq - var iq = $iq({type: "set", id: "_auth_2"}) - .c('query', {xmlns: Strophe.NS.AUTH}) - .c('username', {}).t(Strophe.getNodeFromJid(this.jid)) - .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)); + _sasl_challenge_cb: function(elem) { + var challenge = Base64.decode(Strophe.getText(elem)); + var response = this._sasl_mechanism.onChallenge(this, challenge); - this._addSysHandler(this._auth2_cb.bind(this), null, - null, null, "_auth_2"); + var stanza = $build('response', { + xmlns: Strophe.NS.SASL + }); + if (response !== "") { + stanza.t(Base64.encode(response)); + } + this.send(stanza.tree()); - this.send(iq.tree()); + return true; + }, - return false; - }, - /* jshint unused:true */ - - /** 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) - { - if (this._sasl_data["server-signature"]) { - var serverSignature; - var success = Base64.decode(Strophe.getText(elem)); - var attribMatch = /([a-z]+)=([^,]+)(,|$)/; - var matches = success.match(attribMatch); - if (matches[1] == "v") { - serverSignature = matches[2]; - } + /** 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. + */ + /* jshint unused:false */ + _auth1_cb: function (elem) + { + // build plaintext auth iq + var iq = $iq({type: "set", id: "_auth_2"}) + .c('query', {xmlns: Strophe.NS.AUTH}) + .c('username', {}).t(Strophe.getNodeFromJid(this.jid)) + .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)); - if (serverSignature != this._sasl_data["server-signature"]) { - // 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._sasl_data = {}; - return this._sasl_failure_cb(null); - } - } + this._addSysHandler(this._auth2_cb.bind(this), null, + null, null, "_auth_2"); - Strophe.info("SASL authentication succeeded."); + this.send(iq.tree()); - if(this._sasl_mechanism) - this._sasl_mechanism.onSuccess(); + return false; + }, + /* jshint unused:true */ - // remove old handlers - this.deleteHandler(this._sasl_failure_handler); - this._sasl_failure_handler = null; - if (this._sasl_challenge_handler) { + /** 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) + { + if (this._sasl_data["server-signature"]) { + var serverSignature; + var success = Base64.decode(Strophe.getText(elem)); + var attribMatch = /([a-z]+)=([^,]+)(,|$)/; + var matches = success.match(attribMatch); + if (matches[1] == "v") { + serverSignature = matches[2]; + } + + if (serverSignature != this._sasl_data["server-signature"]) { + // 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._sasl_data = {}; + return this._sasl_failure_cb(null); } + } - this._addSysHandler(this._sasl_auth1_cb.bind(this), null, - "stream:features", null, null); + Strophe.info("SASL authentication succeeded."); - // we must send an xmpp:restart now - this._sendRestart(); + if(this._sasl_mechanism) + this._sasl_mechanism.onSuccess(); - 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) - { - // save stream:features for future usage - this.features = elem; + // 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; + } - var i, child; + this._addSysHandler(this._sasl_auth1_cb.bind(this), null, + "stream:features", null, null); - for (i = 0; i < elem.childNodes.length; i++) { - child = elem.childNodes[i]; - if (child.nodeName == 'bind') { - this.do_bind = true; - } + // we must send an xmpp:restart now + this._sendRestart(); - if (child.nodeName == 'session') { - this.do_session = true; - } - } + return false; + }, - if (!this.do_bind) { - this._changeConnectStatus(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()); - } - } + /** 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) + { + // save stream:features for future usage + this.features = elem; - 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."); - var conflict = elem.getElementsByTagName("conflict"), condition; - if (conflict.length > 0) { - condition = 'conflict'; - } - this._changeConnectStatus(Strophe.Status.AUTHFAIL, condition); - return false; - } + var i, child; - // 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 { - this.authenticated = true; - this._changeConnectStatus(Strophe.Status.CONNECTED, null); - } - } - } else { - Strophe.info("SASL binding failed."); - this._changeConnectStatus(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._changeConnectStatus(Strophe.Status.CONNECTED, null); - } else if (elem.getAttribute("type") == "error") { - Strophe.info("Session creation failed."); - this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); - return false; + for (i = 0; i < elem.childNodes.length; i++) { + child = elem.childNodes[i]; + if (child.nodeName == 'bind') { + this.do_bind = true; } - return false; - }, - - /** PrivateFunction: _sasl_failure_cb - * _Private_ handler for SASL authentication failure. - * - * Parameters: - * (XMLElement) elem - The matching stanza. - * - * Returns: - * false to remove the handler. - */ - /* jshint unused:false */ - _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; + if (child.nodeName == 'session') { + this.do_session = true; } + } - if(this._sasl_mechanism) - this._sasl_mechanism.onFailure(); + if (!this.do_bind) { this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); return false; - }, - /* jshint unused:true */ - - /** 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._changeConnectStatus(Strophe.Status.CONNECTED, null); - } else if (elem.getAttribute("type") == "error") { - this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); - this.disconnect('authentication failed'); + } 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: _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"); - - this._proto._onDisconnectTimeout(); - - // 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; - - // add timed handlers scheduled for addition - // NOTE: we add before remove in the case a timed handler is - // added and then deleted before the next _onIdle() call. - while (this.addTimeds.length > 0) { - this.timedHandlers.push(this.addTimeds.pop()); - } + return false; + }, - // 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); - } + /** 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."); + var conflict = elem.getElementsByTagName("conflict"), condition; + if (conflict.length > 0) { + condition = 'conflict'; } + this._changeConnectStatus(Strophe.Status.AUTHFAIL, condition); + return false; + } - // 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); - } + // 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 { + this.authenticated = true; + this._changeConnectStatus(Strophe.Status.CONNECTED, null); } } - this.timedHandlers = newList; + } else { + Strophe.info("SASL binding failed."); + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } + }, - clearTimeout(this._idleTimeout); + /** 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._changeConnectStatus(Strophe.Status.CONNECTED, null); + } else if (elem.getAttribute("type") == "error") { + Strophe.info("Session creation failed."); + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } - this._proto._onIdle(); + return false; + }, - // reactivate the timer only if connected - if (this.connected) { - this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); - } + /** PrivateFunction: _sasl_failure_cb + * _Private_ handler for SASL authentication failure. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + /* jshint unused:false */ + _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; } - }; - /** Class: Strophe.SASLMechanism - * - * encapsulates SASL authentication mechanisms. + if(this._sasl_mechanism) + this._sasl_mechanism.onFailure(); + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + }, + /* jshint unused:true */ + + /** PrivateFunction: _auth2_cb + * _Private_ handler to finish legacy authentication. * - * User code may override the priority for each mechanism or disable it completely. - * See for information about changing priority and for informatian on - * how to disable a mechanism. + * This handler is called when the result from the jabber:iq:auth + * stanza is returned. * - * By default, all mechanisms are enabled and the priorities are + * Parameters: + * (XMLElement) elem - The stanza that triggered the callback. * - * SCRAM-SHA1 - 40 - * DIGEST-MD5 - 30 - * Plain - 20 + * Returns: + * false to remove the handler. */ + _auth2_cb: function (elem) + { + if (elem.getAttribute("type") == "result") { + this.authenticated = true; + this._changeConnectStatus(Strophe.Status.CONNECTED, null); + } else if (elem.getAttribute("type") == "error") { + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + this.disconnect('authentication failed'); + } + + return false; + }, - /** - * PrivateConstructor: Strophe.SASLMechanism - * SASL auth mechanism abstraction. + /** 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: - * (String) name - SASL Mechanism name. - * (Boolean) isClientFirst - If client should send response first without challenge. - * (Number) priority - Priority. + * (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: - * A new Strophe.SASLMechanism object. - */ - Strophe.SASLMechanism = function(name, isClientFirst, priority) { - /** PrivateVariable: name - * Mechanism name. - */ - this.name = name; - /** PrivateVariable: isClientFirst - * If client sends response without initial server challenge. - */ - this.isClientFirst = isClientFirst; - /** Variable: priority - * Determines which is chosen for authentication (Higher is better). - * Users may override this to prioritize mechanisms differently. - * - * In the default configuration the priorities are - * - * SCRAM-SHA1 - 40 - * DIGEST-MD5 - 30 - * Plain - 20 - * - * Example: (This will cause Strophe to choose the mechanism that the server sent first) - * - * > Strophe.SASLMD5.priority = Strophe.SASLSHA1.priority; - * - * See for a list of available mechanisms. - * - */ - this.priority = priority; - }; - - Strophe.SASLMechanism.prototype = { - /** - * Function: test - * Checks if mechanism able to run. - * To disable a mechanism, make this return false; - * - * To disable plain authentication run - * > Strophe.SASLPlain.test = function() { - * > return false; - * > } - * - * See for a list of available mechanisms. - * - * Parameters: - * (Strophe.Connection) connection - Target Connection. - * - * Returns: - * (Boolean) If mechanism was able to run. - */ - /* jshint unused:false */ - test: function(connection) { - return true; - }, - /* jshint unused:true */ - - /** PrivateFunction: onStart - * Called before starting mechanism on some connection. - * - * Parameters: - * (Strophe.Connection) connection - Target Connection. - */ - onStart: function(connection) - { - this._connection = connection; - }, - - /** PrivateFunction: onChallenge - * Called by protocol implementation on incoming challenge. If client is - * first (isClientFirst == true) challenge will be null on the first call. - * - * Parameters: - * (Strophe.Connection) connection - Target Connection. - * (String) challenge - current challenge to handle. - * - * Returns: - * (String) Mechanism response. - */ - /* jshint unused:false */ - onChallenge: function(connection, challenge) { - throw new Error("You should implement challenge handling!"); - }, - /* jshint unused:true */ - - /** PrivateFunction: onFailure - * Protocol informs mechanism implementation about SASL failure. - */ - onFailure: function() { - this._connection = null; - }, - - /** PrivateFunction: onSuccess - * Protocol informs mechanism implementation about SASL success. - */ - onSuccess: function() { - this._connection = null; - } - }; + * false to remove the handler. + */ + _onDisconnectTimeout: function () + { + Strophe.info("_onDisconnectTimeout was called"); - /** Constants: SASL mechanisms - * Available authentication mechanisms - * - * Strophe.SASLAnonymous - SASL Anonymous authentication. - * Strophe.SASLPlain - SASL Plain authentication. - * Strophe.SASLMD5 - SASL Digest-MD5 authentication - * Strophe.SASLSHA1 - SASL SCRAM-SHA1 authentication - */ + this._proto._onDisconnectTimeout(); - // Building SASL callbacks + // actually disconnect + this._doDisconnect(); - /** PrivateConstructor: SASLAnonymous - * SASL Anonymous authentication. - */ - Strophe.SASLAnonymous = function() {}; + return false; + }, - Strophe.SASLAnonymous.prototype = new Strophe.SASLMechanism("ANONYMOUS", false, 10); + /** 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; - Strophe.SASLAnonymous.test = function(connection) { - return connection.authcid === null; - }; + // add timed handlers scheduled for addition + // NOTE: we add before remove in the case a timed handler is + // added and then deleted before the next _onIdle() call. + while (this.addTimeds.length > 0) { + this.timedHandlers.push(this.addTimeds.pop()); + } - Strophe.Connection.prototype.mechanisms[Strophe.SASLAnonymous.prototype.name] = Strophe.SASLAnonymous; + // 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); + } + } - /** PrivateConstructor: SASLPlain - * SASL Plain authentication. - */ - Strophe.SASLPlain = function() {}; + // 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; - Strophe.SASLPlain.prototype = new Strophe.SASLMechanism("PLAIN", true, 20); + clearTimeout(this._idleTimeout); - Strophe.SASLPlain.test = function(connection) { - return connection.authcid !== null; - }; + this._proto._onIdle(); - Strophe.SASLPlain.prototype.onChallenge = function(connection) { - var auth_str = connection.authzid; - auth_str = auth_str + "\u0000"; - auth_str = auth_str + connection.authcid; - auth_str = auth_str + "\u0000"; - auth_str = auth_str + connection.pass; - return auth_str; - }; + // reactivate the timer only if connected + if (this.connected) { + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + } + } +}; - Strophe.Connection.prototype.mechanisms[Strophe.SASLPlain.prototype.name] = Strophe.SASLPlain; +/** Class: Strophe.SASLMechanism + * + * encapsulates SASL authentication mechanisms. + * + * User code may override the priority for each mechanism or disable it completely. + * See for information about changing priority and for informatian on + * how to disable a mechanism. + * + * By default, all mechanisms are enabled and the priorities are + * + * SCRAM-SHA1 - 40 + * DIGEST-MD5 - 30 + * Plain - 20 + */ - /** PrivateConstructor: SASLSHA1 - * SASL SCRAM SHA 1 authentication. - */ - Strophe.SASLSHA1 = function() {}; +/** + * PrivateConstructor: Strophe.SASLMechanism + * SASL auth mechanism abstraction. + * + * Parameters: + * (String) name - SASL Mechanism name. + * (Boolean) isClientFirst - If client should send response first without challenge. + * (Number) priority - Priority. + * + * Returns: + * A new Strophe.SASLMechanism object. + */ +Strophe.SASLMechanism = function(name, isClientFirst, priority) { + /** PrivateVariable: name + * Mechanism name. + */ + this.name = name; + /** PrivateVariable: isClientFirst + * If client sends response without initial server challenge. + */ + this.isClientFirst = isClientFirst; + /** Variable: priority + * Determines which is chosen for authentication (Higher is better). + * Users may override this to prioritize mechanisms differently. + * + * In the default configuration the priorities are + * + * SCRAM-SHA1 - 40 + * DIGEST-MD5 - 30 + * Plain - 20 + * + * Example: (This will cause Strophe to choose the mechanism that the server sent first) + * + * > Strophe.SASLMD5.priority = Strophe.SASLSHA1.priority; + * + * See for a list of available mechanisms. + * + */ + this.priority = priority; +}; + +Strophe.SASLMechanism.prototype = { + /** + * Function: test + * Checks if mechanism able to run. + * To disable a mechanism, make this return false; + * + * To disable plain authentication run + * > Strophe.SASLPlain.test = function() { + * > return false; + * > } + * + * See for a list of available mechanisms. + * + * Parameters: + * (Strophe.Connection) connection - Target Connection. + * + * Returns: + * (Boolean) If mechanism was able to run. + */ + /* jshint unused:false */ + test: function(connection) { + return true; + }, + /* jshint unused:true */ + + /** PrivateFunction: onStart + * Called before starting mechanism on some connection. + * + * Parameters: + * (Strophe.Connection) connection - Target Connection. + */ + onStart: function(connection) + { + this._connection = connection; + }, + + /** PrivateFunction: onChallenge + * Called by protocol implementation on incoming challenge. If client is + * first (isClientFirst == true) challenge will be null on the first call. + * + * Parameters: + * (Strophe.Connection) connection - Target Connection. + * (String) challenge - current challenge to handle. + * + * Returns: + * (String) Mechanism response. + */ + /* jshint unused:false */ + onChallenge: function(connection, challenge) { + throw new Error("You should implement challenge handling!"); + }, + /* jshint unused:true */ + + /** PrivateFunction: onFailure + * Protocol informs mechanism implementation about SASL failure. + */ + onFailure: function() { + this._connection = null; + }, + + /** PrivateFunction: onSuccess + * Protocol informs mechanism implementation about SASL success. + */ + onSuccess: function() { + this._connection = null; + } +}; + + /** Constants: SASL mechanisms + * Available authentication mechanisms + * + * Strophe.SASLAnonymous - SASL Anonymous authentication. + * Strophe.SASLPlain - SASL Plain authentication. + * Strophe.SASLMD5 - SASL Digest-MD5 authentication + * Strophe.SASLSHA1 - SASL SCRAM-SHA1 authentication + */ + +// Building SASL callbacks + +/** PrivateConstructor: SASLAnonymous + * SASL Anonymous authentication. + */ +Strophe.SASLAnonymous = function() {}; - /* TEST: - * This is a simple example of a SCRAM-SHA-1 authentication exchange - * when the client doesn't support channel bindings (username 'user' and - * password 'pencil' are used): - * - * C: n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL - * S: r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92, - * i=4096 - * C: c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j, - * p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts= - * S: v=rmF9pqV8S7suAoZWja4dJRkFsKQ= - * - */ +Strophe.SASLAnonymous.prototype = new Strophe.SASLMechanism("ANONYMOUS", false, 10); - Strophe.SASLSHA1.prototype = new Strophe.SASLMechanism("SCRAM-SHA-1", true, 40); +Strophe.SASLAnonymous.test = function(connection) { + return connection.authcid === null; +}; - Strophe.SASLSHA1.test = function(connection) { - return connection.authcid !== null; - }; +Strophe.Connection.prototype.mechanisms[Strophe.SASLAnonymous.prototype.name] = Strophe.SASLAnonymous; - Strophe.SASLSHA1.prototype.onChallenge = function(connection, challenge, test_cnonce) { - var cnonce = test_cnonce || MD5.hexdigest(Math.random() * 1234567890); +/** PrivateConstructor: SASLPlain + * SASL Plain authentication. + */ +Strophe.SASLPlain = function() {}; - var auth_str = "n=" + connection.authcid; - auth_str += ",r="; - auth_str += cnonce; +Strophe.SASLPlain.prototype = new Strophe.SASLMechanism("PLAIN", true, 20); - connection._sasl_data.cnonce = cnonce; - connection._sasl_data["client-first-message-bare"] = auth_str; +Strophe.SASLPlain.test = function(connection) { + return connection.authcid !== null; +}; - auth_str = "n,," + auth_str; +Strophe.SASLPlain.prototype.onChallenge = function(connection) { + var auth_str = connection.authzid; + auth_str = auth_str + "\u0000"; + auth_str = auth_str + connection.authcid; + auth_str = auth_str + "\u0000"; + auth_str = auth_str + connection.pass; + return auth_str; +}; - this.onChallenge = function (connection, challenge) - { - var nonce, salt, iter, Hi, U, U_old, i, k; - var clientKey, serverKey, clientSignature; - var responseText = "c=biws,"; - var authMessage = connection._sasl_data["client-first-message-bare"] + "," + - challenge + ","; - var cnonce = connection._sasl_data.cnonce; - var attribMatch = /([a-z]+)=([^,]+)(,|$)/; +Strophe.Connection.prototype.mechanisms[Strophe.SASLPlain.prototype.name] = Strophe.SASLPlain; - while (challenge.match(attribMatch)) { - var matches = challenge.match(attribMatch); - challenge = challenge.replace(matches[0], ""); - switch (matches[1]) { - case "r": - nonce = matches[2]; - break; - case "s": - salt = matches[2]; - break; - case "i": - iter = matches[2]; - break; - } - } +/** PrivateConstructor: SASLSHA1 + * SASL SCRAM SHA 1 authentication. + */ +Strophe.SASLSHA1 = function() {}; - if (nonce.substr(0, cnonce.length) !== cnonce) { - connection._sasl_data = {}; - return connection._sasl_failure_cb(); - } +/* TEST: + * This is a simple example of a SCRAM-SHA-1 authentication exchange + * when the client doesn't support channel bindings (username 'user' and + * password 'pencil' are used): + * + * C: n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL + * S: r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92, + * i=4096 + * C: c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j, + * p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts= + * S: v=rmF9pqV8S7suAoZWja4dJRkFsKQ= + * + */ - responseText += "r=" + nonce; - authMessage += responseText; +Strophe.SASLSHA1.prototype = new Strophe.SASLMechanism("SCRAM-SHA-1", true, 40); + +Strophe.SASLSHA1.test = function(connection) { + return connection.authcid !== null; +}; + +Strophe.SASLSHA1.prototype.onChallenge = function(connection, challenge, test_cnonce) { + var cnonce = test_cnonce || MD5.hexdigest(Math.random() * 1234567890); + + var auth_str = "n=" + connection.authcid; + auth_str += ",r="; + auth_str += cnonce; + + connection._sasl_data.cnonce = cnonce; + connection._sasl_data["client-first-message-bare"] = auth_str; + + auth_str = "n,," + auth_str; + + this.onChallenge = function (connection, challenge) + { + var nonce, salt, iter, Hi, U, U_old, i, k; + var clientKey, serverKey, clientSignature; + var responseText = "c=biws,"; + var authMessage = connection._sasl_data["client-first-message-bare"] + "," + + challenge + ","; + var cnonce = connection._sasl_data.cnonce; + var attribMatch = /([a-z]+)=([^,]+)(,|$)/; + + while (challenge.match(attribMatch)) { + var matches = challenge.match(attribMatch); + challenge = challenge.replace(matches[0], ""); + switch (matches[1]) { + case "r": + nonce = matches[2]; + break; + case "s": + salt = matches[2]; + break; + case "i": + iter = matches[2]; + break; + } + } - salt = Base64.decode(salt); - salt += "\x00\x00\x00\x01"; + if (nonce.substr(0, cnonce.length) !== cnonce) { + connection._sasl_data = {}; + return connection._sasl_failure_cb(); + } - Hi = U_old = SHA1.core_hmac_sha1(connection.pass, salt); - for (i = 1; i < iter; i++) { - U = SHA1.core_hmac_sha1(connection.pass, SHA1.binb2str(U_old)); - for (k = 0; k < 5; k++) { - Hi[k] ^= U[k]; - } - U_old = U; - } - Hi = SHA1.binb2str(Hi); + responseText += "r=" + nonce; + authMessage += responseText; - clientKey = SHA1.core_hmac_sha1(Hi, "Client Key"); - serverKey = SHA1.str_hmac_sha1(Hi, "Server Key"); - clientSignature = SHA1.core_hmac_sha1(SHA1.str_sha1(SHA1.binb2str(clientKey)), authMessage); - connection._sasl_data["server-signature"] = SHA1.b64_hmac_sha1(serverKey, authMessage); + salt = Base64.decode(salt); + salt += "\x00\x00\x00\x01"; - for (k = 0; k < 5; k++) { - clientKey[k] ^= clientSignature[k]; - } + Hi = U_old = SHA1.core_hmac_sha1(connection.pass, salt); + for (i = 1; i < iter; i++) { + U = SHA1.core_hmac_sha1(connection.pass, SHA1.binb2str(U_old)); + for (k = 0; k < 5; k++) { + Hi[k] ^= U[k]; + } + U_old = U; + } + Hi = SHA1.binb2str(Hi); - responseText += ",p=" + Base64.encode(SHA1.binb2str(clientKey)); + clientKey = SHA1.core_hmac_sha1(Hi, "Client Key"); + serverKey = SHA1.str_hmac_sha1(Hi, "Server Key"); + clientSignature = SHA1.core_hmac_sha1(SHA1.str_sha1(SHA1.binb2str(clientKey)), authMessage); + connection._sasl_data["server-signature"] = SHA1.b64_hmac_sha1(serverKey, authMessage); - return responseText; - }.bind(this); + for (k = 0; k < 5; k++) { + clientKey[k] ^= clientSignature[k]; + } - return auth_str; - }; + responseText += ",p=" + Base64.encode(SHA1.binb2str(clientKey)); - Strophe.Connection.prototype.mechanisms[Strophe.SASLSHA1.prototype.name] = Strophe.SASLSHA1; + return responseText; + }.bind(this); - /** PrivateConstructor: SASLMD5 - * SASL DIGEST MD5 authentication. - */ - Strophe.SASLMD5 = function() {}; + return auth_str; +}; - Strophe.SASLMD5.prototype = new Strophe.SASLMechanism("DIGEST-MD5", false, 30); +Strophe.Connection.prototype.mechanisms[Strophe.SASLSHA1.prototype.name] = Strophe.SASLSHA1; - Strophe.SASLMD5.test = function(connection) { - return connection.authcid !== null; - }; +/** PrivateConstructor: SASLMD5 + * SASL DIGEST MD5 authentication. + */ +Strophe.SASLMD5 = function() {}; - /** PrivateFunction: _quote - * _Private_ utility function to backslash escape and quote strings. - * - * Parameters: - * (String) str - The string to be quoted. - * - * Returns: - * quoted string - */ - Strophe.SASLMD5.prototype._quote = function (str) - { - return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; - //" end string workaround for emacs - }; - - - Strophe.SASLMD5.prototype.onChallenge = function(connection, challenge, test_cnonce) { - var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/; - var cnonce = test_cnonce || MD5.hexdigest("" + (Math.random() * 1234567890)); - var realm = ""; - var host = null; - var nonce = ""; - var qop = ""; - var matches; - - 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; - } - } +Strophe.SASLMD5.prototype = new Strophe.SASLMechanism("DIGEST-MD5", false, 30); - var digest_uri = connection.servtype + "/" + connection.domain; - if (host !== null) { - digest_uri = digest_uri + "/" + host; - } +Strophe.SASLMD5.test = function(connection) { + return connection.authcid !== null; +}; - var A1 = MD5.hash(connection.authcid + - ":" + realm + ":" + this._connection.pass) + - ":" + nonce + ":" + cnonce; - var A2 = 'AUTHENTICATE:' + digest_uri; - - var responseText = ""; - responseText += 'charset=utf-8,'; - responseText += 'username=' + - this._quote(connection.authcid) + ','; - responseText += 'realm=' + this._quote(realm) + ','; - responseText += 'nonce=' + this._quote(nonce) + ','; - responseText += 'nc=00000001,'; - responseText += 'cnonce=' + this._quote(cnonce) + ','; - responseText += 'digest-uri=' + this._quote(digest_uri) + ','; - responseText += 'response=' + MD5.hexdigest(MD5.hexdigest(A1) + ":" + - nonce + ":00000001:" + - cnonce + ":auth:" + - MD5.hexdigest(A2)) + ","; - responseText += 'qop=auth'; - - this.onChallenge = function () { - return ""; - }.bind(this); - - return responseText; - }; - - Strophe.Connection.prototype.mechanisms[Strophe.SASLMD5.prototype.name] = Strophe.SASLMD5; - - return { - Strophe: Strophe, - $build: $build, - $msg: $msg, - $iq: $iq, - $pres: $pres, - SHA1: SHA1, - Base64: Base64, - MD5: MD5 - }; +/** PrivateFunction: _quote + * _Private_ utility function to backslash escape and quote strings. + * + * Parameters: + * (String) str - The string to be quoted. + * + * Returns: + * quoted string + */ +Strophe.SASLMD5.prototype._quote = function (str) + { + return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; + //" end string workaround for emacs + }; + + +Strophe.SASLMD5.prototype.onChallenge = function(connection, challenge, test_cnonce) { + var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/; + var cnonce = test_cnonce || MD5.hexdigest("" + (Math.random() * 1234567890)); + var realm = ""; + var host = null; + var nonce = ""; + var qop = ""; + var matches; + + 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 = connection.servtype + "/" + connection.domain; + if (host !== null) { + digest_uri = digest_uri + "/" + host; + } + + var A1 = MD5.hash(connection.authcid + + ":" + realm + ":" + this._connection.pass) + + ":" + nonce + ":" + cnonce; + var A2 = 'AUTHENTICATE:' + digest_uri; + + var responseText = ""; + responseText += 'charset=utf-8,'; + responseText += 'username=' + + this._quote(connection.authcid) + ','; + responseText += 'realm=' + this._quote(realm) + ','; + responseText += 'nonce=' + this._quote(nonce) + ','; + responseText += 'nc=00000001,'; + responseText += 'cnonce=' + this._quote(cnonce) + ','; + responseText += 'digest-uri=' + this._quote(digest_uri) + ','; + responseText += 'response=' + MD5.hexdigest(MD5.hexdigest(A1) + ":" + + nonce + ":00000001:" + + cnonce + ":auth:" + + MD5.hexdigest(A2)) + ","; + responseText += 'qop=auth'; + + this.onChallenge = function () { + return ""; + }.bind(this); + + return responseText; +}; + +Strophe.Connection.prototype.mechanisms[Strophe.SASLMD5.prototype.name] = Strophe.SASLMD5; + +return { + Strophe: Strophe, + $build: $build, + $msg: $msg, + $iq: $iq, + $pres: $pres, + SHA1: SHA1, + Base64: Base64, + MD5: MD5 +}; })); diff --git a/src/md5.js b/src/md5.js index aaa5c03d..9680c399 100644 --- a/src/md5.js +++ b/src/md5.js @@ -23,9 +23,9 @@ } }(this, function (b) { /* - * Add integers, wrapping at 2^32. This uses 16-bit operations internally - * to work around bugs in some JS interpreters. - */ + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ var safe_add = function (x, y) { var lsw = (x & 0xFFFF) + (y & 0xFFFF); var msw = (x >> 16) + (y >> 16) + (lsw >> 16); @@ -33,15 +33,15 @@ }; /* - * Bitwise rotate a 32-bit number to the left. - */ + * Bitwise rotate a 32-bit number to the left. + */ var bit_rol = function (num, cnt) { return (num << cnt) | (num >>> (32 - cnt)); }; /* - * Convert a string to an array of little-endian words - */ + * Convert a string to an array of little-endian words + */ var str2binl = function (str) { var bin = []; for(var i = 0; i < str.length * 8; i += 8) @@ -52,8 +52,8 @@ }; /* - * Convert an array of little-endian words to a string - */ + * Convert an array of little-endian words to a string + */ var binl2str = function (bin) { var str = ""; for(var i = 0; i < bin.length * 32; i += 8) @@ -64,8 +64,8 @@ }; /* - * Convert an array of little-endian words to a hex string. - */ + * Convert an array of little-endian words to a hex string. + */ var binl2hex = function (binarray) { var hex_tab = "0123456789abcdef"; var str = ""; @@ -78,8 +78,8 @@ }; /* - * These functions implement the four basic operations the algorithm uses. - */ + * These functions implement the four basic operations the algorithm uses. + */ var md5_cmn = function (q, a, b, x, s, t) { return safe_add(bit_rol(safe_add(safe_add(a, q),safe_add(x, t)), s),b); }; @@ -101,8 +101,8 @@ }; /* - * Calculate the MD5 of an array of little-endian words, and a bit length - */ + * Calculate the MD5 of an array of little-endian words, and a bit length + */ var core_md5 = function (x, len) { /* append padding */ x[len >> 5] |= 0x80 << ((len) % 32); @@ -199,10 +199,10 @@ var obj = { /* - * These are the functions you'll usually want to call. - * They take string arguments and return either hex or base-64 encoded - * strings. - */ + * These are the functions you'll usually want to call. + * They take string arguments and return either hex or base-64 encoded + * strings. + */ hexdigest: function (s) { return binl2hex(core_md5(str2binl(s), s.length * 8)); }, diff --git a/src/sha1.js b/src/sha1.js index c5651f9c..ef43a13c 100644 --- a/src/sha1.js +++ b/src/sha1.js @@ -24,152 +24,173 @@ } }(this, function () { - /* - * Calculate the SHA-1 of an array of big-endian words, and a bit length - */ - function core_sha1(x, len) { - x[len >> 5] |= 0x80 << (24 - len % 32); /* append padding */ - x[((len + 64 >> 9) << 4) + 15] = len; - - var w = new Array(80); - var a = 1732584193; - var b = -271733879; - var c = -1732584194; - var d = 271733878; - var e = -1009589776; - - var i, j, t, olda, oldb, oldc, oldd, olde; - for (i = 0; i < x.length; i += 16) { - olda = a; - oldb = b; - oldc = c; - oldd = d; - olde = e; - for (j = 0; j < 80; j++) { - if (j < 16) { w[j] = x[i + j]; } - else { w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); } - t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), - safe_add(safe_add(e, w[j]), sha1_kt(j))); - e = d; - d = c; - c = rol(b, 30); - b = a; - a = t; - } - a = safe_add(a, olda); - b = safe_add(b, oldb); - c = safe_add(c, oldc); - d = safe_add(d, oldd); - e = safe_add(e, olde); - } - return [a, b, c, d, e]; - } +/* + * Calculate the SHA-1 of an array of big-endian words, and a bit length + */ +function core_sha1(x, len) +{ + /* append padding */ + x[len >> 5] |= 0x80 << (24 - len % 32); + x[((len + 64 >> 9) << 4) + 15] = len; - /* - * Perform the appropriate triplet combination function for the current - * iteration - */ - function sha1_ft(t, b, c, d) { - if (t < 20) { return (b & c) | ((~b) & d); } - if (t < 40) { return b ^ c ^ d; } - if (t < 60) { return (b & c) | (b & d) | (c & d); } - return b ^ c ^ d; - } + var w = new Array(80); + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + var e = -1009589776; - /* - * Determine the appropriate additive constant for the current iteration - */ - function sha1_kt(t) { - return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : - (t < 60) ? -1894007588 : -899497514; - } + var i, j, t, olda, oldb, oldc, oldd, olde; + for (i = 0; i < x.length; i += 16) + { + olda = a; + oldb = b; + oldc = c; + oldd = d; + olde = e; - /* - * Calculate the HMAC-SHA1 of a key and some data - */ - function core_hmac_sha1(key, data) { - var bkey = str2binb(key); - if (bkey.length > 16) { bkey = core_sha1(bkey, key.length * 8); } - var ipad = new Array(16), opad = new Array(16); - for (var i = 0; i < 16; i++) { - ipad[i] = bkey[i] ^ 0x36363636; - opad[i] = bkey[i] ^ 0x5C5C5C5C; - } - var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * 8); - return core_sha1(opad.concat(hash), 512 + 160); + for (j = 0; j < 80; j++) + { + if (j < 16) { w[j] = x[i + j]; } + else { w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); } + t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), + safe_add(safe_add(e, w[j]), sha1_kt(j))); + e = d; + d = c; + c = rol(b, 30); + b = a; + a = t; } - /* - * Add integers, wrapping at 2^32. This uses 16-bit operations internally - * to work around bugs in some JS interpreters. - */ - function safe_add(x, y) { - var lsw = (x & 0xFFFF) + (y & 0xFFFF); - var msw = (x >> 16) + (y >> 16) + (lsw >> 16); - return (msw << 16) | (lsw & 0xFFFF); - } + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + e = safe_add(e, olde); + } + return [a, b, c, d, e]; +} - /* - * Bitwise rotate a 32-bit number to the left. - */ - function rol(num, cnt) { - return (num << cnt) | (num >>> (32 - cnt)); - } +/* + * Perform the appropriate triplet combination function for the current + * iteration + */ +function sha1_ft(t, b, c, d) +{ + if (t < 20) { return (b & c) | ((~b) & d); } + if (t < 40) { return b ^ c ^ d; } + if (t < 60) { return (b & c) | (b & d) | (c & d); } + return b ^ c ^ d; +} - /* - * Convert an 8-bit or 16-bit string to an array of big-endian words - * In 8-bit function, characters >255 have their hi-byte silently ignored. - */ - function str2binb(str) { - var bin = []; - var mask = 255; - for (var i = 0; i < str.length * 8; i += 8) { - bin[i>>5] |= (str.charCodeAt(i / 8) & mask) << (24 - i%32); - } - return bin; - } +/* + * Determine the appropriate additive constant for the current iteration + */ +function sha1_kt(t) +{ + return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : + (t < 60) ? -1894007588 : -899497514; +} - /* - * Convert an array of big-endian words to a string - */ - function binb2str(bin) { - var str = ""; - var mask = 255; - for (var i = 0; i < bin.length * 32; i += 8) { - str += String.fromCharCode((bin[i>>5] >>> (24 - i%32)) & mask); - } - return str; - } +/* + * Calculate the HMAC-SHA1 of a key and some data + */ +function core_hmac_sha1(key, data) +{ + var bkey = str2binb(key); + if (bkey.length > 16) { bkey = core_sha1(bkey, key.length * 8); } + + var ipad = new Array(16), opad = new Array(16); + for (var i = 0; i < 16; i++) + { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * 8); + return core_sha1(opad.concat(hash), 512 + 160); +} - /* - * Convert an array of big-endian words to a base-64 string - */ - function binb2b64(binarray) { - var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - var str = ""; - var triplet, j; - for (var i = 0; i < binarray.length * 4; i += 3) { - triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16) | - (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) | - ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF); - for (j = 0; j < 4; j++) { - if (i * 8 + j * 6 > binarray.length * 32) { str += "="; } - else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); } - } - } - return str; +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ +function safe_add(x, y) +{ + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); +} + +/* + * Bitwise rotate a 32-bit number to the left. + */ +function rol(num, cnt) +{ + return (num << cnt) | (num >>> (32 - cnt)); +} + +/* + * Convert an 8-bit or 16-bit string to an array of big-endian words + * In 8-bit function, characters >255 have their hi-byte silently ignored. + */ +function str2binb(str) +{ + var bin = []; + var mask = 255; + for (var i = 0; i < str.length * 8; i += 8) + { + bin[i>>5] |= (str.charCodeAt(i / 8) & mask) << (24 - i%32); + } + return bin; +} + +/* + * Convert an array of big-endian words to a string + */ +function binb2str(bin) +{ + var str = ""; + var mask = 255; + for (var i = 0; i < bin.length * 32; i += 8) + { + str += String.fromCharCode((bin[i>>5] >>> (24 - i%32)) & mask); + } + return str; +} + +/* + * Convert an array of big-endian words to a base-64 string + */ +function binb2b64(binarray) +{ + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var str = ""; + var triplet, j; + for (var i = 0; i < binarray.length * 4; i += 3) + { + triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16) | + (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) | + ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF); + for (j = 0; j < 4; j++) + { + if (i * 8 + j * 6 > binarray.length * 32) { str += "="; } + else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); } } + } + return str; +} - /* - * These are the functions you'll usually want to call - * They take string arguments and return either hex or base-64 encoded strings - */ - return { - b64_hmac_sha1: function (key, data){ return binb2b64(core_hmac_sha1(key, data)); }, - b64_sha1: function (s) { return binb2b64(core_sha1(str2binb(s),s.length * 8)); }, - binb2str: binb2str, - core_hmac_sha1: core_hmac_sha1, - str_hmac_sha1: function (key, data){ return binb2str(core_hmac_sha1(key, data)); }, - str_sha1: function (s) { return binb2str(core_sha1(str2binb(s),s.length * 8)); }, - }; +/* + * These are the functions you'll usually want to call + * They take string arguments and return either hex or base-64 encoded strings + */ +return { + b64_hmac_sha1: function (key, data){ return binb2b64(core_hmac_sha1(key, data)); }, + b64_sha1: function (s) { return binb2b64(core_sha1(str2binb(s),s.length * 8)); }, + binb2str: binb2str, + core_hmac_sha1: core_hmac_sha1, + str_hmac_sha1: function (key, data){ return binb2str(core_hmac_sha1(key, data)); }, + str_sha1: function (s) { return binb2str(core_sha1(str2binb(s),s.length * 8)); }, +}; })); diff --git a/src/websocket.js b/src/websocket.js index e94ba4f0..d92953e4 100644 --- a/src/websocket.js +++ b/src/websocket.js @@ -20,524 +20,524 @@ } }(this, function (Strophe) { - /** Class: Strophe.WebSocket - * _Private_ helper class that handles WebSocket Connections - * - * The Strophe.WebSocket class is used internally by Strophe.Connection - * to encapsulate WebSocket sessions. It is not meant to be used from user's code. - */ +/** Class: Strophe.WebSocket + * _Private_ helper class that handles WebSocket Connections + * + * The Strophe.WebSocket class is used internally by Strophe.Connection + * to encapsulate WebSocket sessions. It is not meant to be used from user's code. + */ + +/** File: websocket.js + * A JavaScript library to enable XMPP over Websocket in Strophejs. + * + * This file implements XMPP over WebSockets for Strophejs. + * If a Connection is established with a Websocket url (ws://...) + * Strophe will use WebSockets. + * For more information on XMPP-over WebSocket see this RFC draft: + * http://tools.ietf.org/html/draft-ietf-xmpp-websocket-00 + * + * WebSocket support implemented by Andreas Guth (andreas.guth@rwth-aachen.de) + */ + +/** PrivateConstructor: Strophe.Websocket + * Create and initialize a Strophe.WebSocket object. + * Currently only sets the connection Object. + * + * Parameters: + * (Strophe.Connection) connection - The Strophe.Connection that will use WebSockets. + * + * Returns: + * A new Strophe.WebSocket object. + */ +Strophe.Websocket = function(connection) { + this._conn = connection; + this.strip = "stream:stream"; + + var service = connection.service; + if (service.indexOf("ws:") !== 0 && service.indexOf("wss:") !== 0) { + // If the service is not an absolute URL, assume it is a path and put the absolute + // URL together from options, current URL and the path. + var new_service = ""; + + if (connection.options.protocol === "ws" && window.location.protocol !== "https:") { + new_service += "ws"; + } else { + new_service += "wss"; + } - /** File: websocket.js - * A JavaScript library to enable XMPP over Websocket in Strophejs. - * - * This file implements XMPP over WebSockets for Strophejs. - * If a Connection is established with a Websocket url (ws://...) - * Strophe will use WebSockets. - * For more information on XMPP-over WebSocket see this RFC draft: - * http://tools.ietf.org/html/draft-ietf-xmpp-websocket-00 + new_service += "://" + window.location.host; + + if (service.indexOf("/") !== 0) { + new_service += window.location.pathname + service; + } else { + new_service += service; + } + + connection.service = new_service; + } +}; + +Strophe.Websocket.prototype = { + /** PrivateFunction: _buildStream + * _Private_ helper function to generate the start tag for WebSockets * - * WebSocket support implemented by Andreas Guth (andreas.guth@rwth-aachen.de) + * Returns: + * A Strophe.Builder with a element. */ + _buildStream: function () + { + return $build("stream:stream", { + "to": this._conn.domain, + "xmlns": Strophe.NS.CLIENT, + "xmlns:stream": Strophe.NS.STREAM, + "version": '1.0' + }); + }, - /** PrivateConstructor: Strophe.Websocket - * Create and initialize a Strophe.WebSocket object. - * Currently only sets the connection Object. + /** PrivateFunction: _check_streamerror + * _Private_ checks a message for stream:error * * Parameters: - * (Strophe.Connection) connection - The Strophe.Connection that will use WebSockets. - * + * (Strophe.Request) bodyWrap - The received stanza. + * connectstatus - The ConnectStatus that will be set on error. * Returns: - * A new Strophe.WebSocket object. + * true if there was a streamerror, false otherwise. */ - Strophe.Websocket = function(connection) { - this._conn = connection; - this.strip = "stream:stream"; - - var service = connection.service; - if (service.indexOf("ws:") !== 0 && service.indexOf("wss:") !== 0) { - // If the service is not an absolute URL, assume it is a path and put the absolute - // URL together from options, current URL and the path. - var new_service = ""; - - if (connection.options.protocol === "ws" && window.location.protocol !== "https:") { - new_service += "ws"; + _check_streamerror: function (bodyWrap, connectstatus) { + var errors = bodyWrap.getElementsByTagNameNS(Strophe.NS.STREAM, "error"); + if (errors.length === 0) { + return false; + } + var error = errors[0]; + + var condition = ""; + var text = ""; + + var ns = "urn:ietf:params:xml:ns:xmpp-streams"; + for (var i = 0; i < error.childNodes.length; i++) { + var e = error.childNodes[i]; + if (e.getAttribute("xmlns") !== ns) { + break; + } if (e.nodeName === "text") { + text = e.textContent; } else { - new_service += "wss"; + condition = e.nodeName; } + } - new_service += "://" + window.location.host; + var errorString = "WebSocket stream error: "; - if (service.indexOf("/") !== 0) { - new_service += window.location.pathname + service; - } else { - new_service += service; - } + if (condition) { + errorString += condition; + } else { + errorString += "unknown"; + } - connection.service = new_service; + if (text) { + errorString += " - " + condition; } - }; - - Strophe.Websocket.prototype = { - /** PrivateFunction: _buildStream - * _Private_ helper function to generate the start tag for WebSockets - * - * Returns: - * A Strophe.Builder with a element. - */ - _buildStream: function () - { - return $build("stream:stream", { - "to": this._conn.domain, - "xmlns": Strophe.NS.CLIENT, - "xmlns:stream": Strophe.NS.STREAM, - "version": '1.0' - }); - }, - - /** PrivateFunction: _check_streamerror - * _Private_ checks a message for stream:error - * - * Parameters: - * (Strophe.Request) bodyWrap - The received stanza. - * connectstatus - The ConnectStatus that will be set on error. - * Returns: - * true if there was a streamerror, false otherwise. - */ - _check_streamerror: function (bodyWrap, connectstatus) { - var errors = bodyWrap.getElementsByTagNameNS(Strophe.NS.STREAM, "error"); - if (errors.length === 0) { - return false; - } - var error = errors[0]; - - var condition = ""; - var text = ""; - - var ns = "urn:ietf:params:xml:ns:xmpp-streams"; - for (var i = 0; i < error.childNodes.length; i++) { - var e = error.childNodes[i]; - if (e.getAttribute("xmlns") !== ns) { - break; - } if (e.nodeName === "text") { - text = e.textContent; - } else { - condition = e.nodeName; - } - } - var errorString = "WebSocket stream error: "; + Strophe.error(errorString); - if (condition) { - errorString += condition; - } else { - errorString += "unknown"; - } + // close the connection on stream_error + this._conn._changeConnectStatus(connectstatus, condition); + this._conn._doDisconnect(); + return true; + }, - if (text) { - errorString += " - " + condition; - } + /** PrivateFunction: _reset + * Reset the connection. + * + * This function is called by the reset function of the Strophe Connection. + * Is not needed by WebSockets. + */ + _reset: function () + { + return; + }, - Strophe.error(errorString); + /** PrivateFunction: _connect + * _Private_ function called by Strophe.Connection.connect + * + * Creates a WebSocket for a connection and assigns Callbacks to it. + * Does nothing if there already is a WebSocket. + */ + _connect: function () { + // Ensure that there is no open WebSocket from a previous Connection. + this._closeSocket(); + + // Create the new WobSocket + this.socket = new WebSocket(this._conn.service, "xmpp"); + this.socket.onopen = this._onOpen.bind(this); + this.socket.onerror = this._onError.bind(this); + this.socket.onclose = this._onClose.bind(this); + this.socket.onmessage = this._connect_cb_wrapper.bind(this); + }, + + /** PrivateFunction: _connect_cb + * _Private_ function called by Strophe.Connection._connect_cb + * + * checks for stream:error + * + * Parameters: + * (Strophe.Request) bodyWrap - The received stanza. + */ + _connect_cb: function(bodyWrap) { + var error = this._check_streamerror(bodyWrap, Strophe.Status.CONNFAIL); + if (error) { + return Strophe.Status.CONNFAIL; + } + }, - // close the connection on stream_error - this._conn._changeConnectStatus(connectstatus, condition); - this._conn._doDisconnect(); - return true; - }, - - /** PrivateFunction: _reset - * Reset the connection. - * - * This function is called by the reset function of the Strophe Connection. - * Is not needed by WebSockets. - */ - _reset: function () - { - return; - }, - - /** PrivateFunction: _connect - * _Private_ function called by Strophe.Connection.connect - * - * Creates a WebSocket for a connection and assigns Callbacks to it. - * Does nothing if there already is a WebSocket. - */ - _connect: function () { - // Ensure that there is no open WebSocket from a previous Connection. - this._closeSocket(); - - // Create the new WobSocket - this.socket = new WebSocket(this._conn.service, "xmpp"); - this.socket.onopen = this._onOpen.bind(this); - this.socket.onerror = this._onError.bind(this); - this.socket.onclose = this._onClose.bind(this); - this.socket.onmessage = this._connect_cb_wrapper.bind(this); - }, - - /** PrivateFunction: _connect_cb - * _Private_ function called by Strophe.Connection._connect_cb - * - * checks for stream:error - * - * Parameters: - * (Strophe.Request) bodyWrap - The received stanza. - */ - _connect_cb: function(bodyWrap) { - var error = this._check_streamerror(bodyWrap, Strophe.Status.CONNFAIL); - if (error) { - return Strophe.Status.CONNFAIL; - } - }, - - /** PrivateFunction: _handleStreamStart - * _Private_ function that checks the opening stream:stream tag for errors. - * - * Disconnects if there is an error and returns false, true otherwise. - * - * Parameters: - * (Node) message - Stanza containing the stream:stream. - */ - _handleStreamStart: function(message) { - var error = false; - // Check for errors in the stream:stream tag - var ns = message.getAttribute("xmlns"); - if (typeof ns !== "string") { - error = "Missing xmlns in stream:stream"; - } else if (ns !== Strophe.NS.CLIENT) { - error = "Wrong xmlns in stream:stream: " + ns; - } + /** PrivateFunction: _handleStreamStart + * _Private_ function that checks the opening stream:stream tag for errors. + * + * Disconnects if there is an error and returns false, true otherwise. + * + * Parameters: + * (Node) message - Stanza containing the stream:stream. + */ + _handleStreamStart: function(message) { + var error = false; + // Check for errors in the stream:stream tag + var ns = message.getAttribute("xmlns"); + if (typeof ns !== "string") { + error = "Missing xmlns in stream:stream"; + } else if (ns !== Strophe.NS.CLIENT) { + error = "Wrong xmlns in stream:stream: " + ns; + } - var ns_stream = message.namespaceURI; - if (typeof ns_stream !== "string") { - error = "Missing xmlns:stream in stream:stream"; - } else if (ns_stream !== Strophe.NS.STREAM) { - error = "Wrong xmlns:stream in stream:stream: " + ns_stream; - } + var ns_stream = message.namespaceURI; + if (typeof ns_stream !== "string") { + error = "Missing xmlns:stream in stream:stream"; + } else if (ns_stream !== Strophe.NS.STREAM) { + error = "Wrong xmlns:stream in stream:stream: " + ns_stream; + } - var ver = message.getAttribute("version"); - if (typeof ver !== "string") { - error = "Missing version in stream:stream"; - } else if (ver !== "1.0") { - error = "Wrong version in stream:stream: " + ver; - } + var ver = message.getAttribute("version"); + if (typeof ver !== "string") { + error = "Missing version in stream:stream"; + } else if (ver !== "1.0") { + error = "Wrong version in stream:stream: " + ver; + } - if (error) { - this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, error); - this._conn._doDisconnect(); - return false; - } + if (error) { + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, error); + this._conn._doDisconnect(); + return false; + } - return true; - }, + return true; + }, - /** PrivateFunction: _connect_cb_wrapper - * _Private_ function that handles the first connection messages. - * - * On receiving an opening stream tag this callback replaces itself with the real - * message handler. On receiving a stream error the connection is terminated. - */ - _connect_cb_wrapper: function(message) { - if (message.data.indexOf("\s*)*/, ""); - if (data === '') return; + /** PrivateFunction: _connect_cb_wrapper + * _Private_ function that handles the first connection messages. + * + * On receiving an opening stream tag this callback replaces itself with the real + * message handler. On receiving a stream error the connection is terminated. + */ + _connect_cb_wrapper: function(message) { + if (message.data.indexOf("\s*)*/, ""); + if (data === '') return; - //Make the initial stream:stream selfclosing to parse it without a SAX parser. - data = message.data.replace(//, ""); + //Make the initial stream:stream selfclosing to parse it without a SAX parser. + data = message.data.replace(//, ""); - var streamStart = new DOMParser().parseFromString(data, "text/xml").documentElement; - this._conn.xmlInput(streamStart); - this._conn.rawInput(message.data); + var streamStart = new DOMParser().parseFromString(data, "text/xml").documentElement; + this._conn.xmlInput(streamStart); + this._conn.rawInput(message.data); - //_handleStreamSteart will check for XML errors and disconnect on error - if (this._handleStreamStart(streamStart)) { + //_handleStreamSteart will check for XML errors and disconnect on error + if (this._handleStreamStart(streamStart)) { - //_connect_cb will check for stream:error and disconnect on error - this._connect_cb(streamStart); + //_connect_cb will check for stream:error and disconnect on error + this._connect_cb(streamStart); - // ensure received stream:stream is NOT selfclosing and save it for following messages - this.streamStart = message.data.replace(/^$/, ""); - } - } else if (message.data === "") { - this._conn.rawInput(message.data); - this._conn.xmlInput(document.createElement("stream:stream")); - this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "Received closing stream"); - this._conn._doDisconnect(); - return; - } else { - var string = this._streamWrap(message.data); - var elem = new DOMParser().parseFromString(string, "text/xml").documentElement; - this.socket.onmessage = this._onMessage.bind(this); - this._conn._connect_cb(elem, null, message.data); + // ensure received stream:stream is NOT selfclosing and save it for following messages + this.streamStart = message.data.replace(/^$/, ""); } - }, - - /** PrivateFunction: _disconnect - * _Private_ function called by Strophe.Connection.disconnect - * - * Disconnects and sends a last stanza if one is given - * - * Parameters: - * (Request) pres - This stanza will be sent before disconnecting. - */ - _disconnect: function (pres) - { - if (this.socket.readyState !== WebSocket.CLOSED) { - if (pres) { - this._conn.send(pres); - } - var close = ''; - this._conn.xmlOutput(document.createElement("stream:stream")); - this._conn.rawOutput(close); - try { - this.socket.send(close); - } catch (e) { - Strophe.info("Couldn't send closing stream tag."); - } - } - + } else if (message.data === "") { + this._conn.rawInput(message.data); + this._conn.xmlInput(document.createElement("stream:stream")); + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "Received closing stream"); this._conn._doDisconnect(); - }, - - /** PrivateFunction: _doDisconnect - * _Private_ function to disconnect. - * - * Just closes the Socket for WebSockets - */ - _doDisconnect: function () - { - Strophe.info("WebSockets _doDisconnect was called"); - this._closeSocket(); - }, - - /** PrivateFunction _streamWrap - * _Private_ helper function to wrap a stanza in a tag. - * This is used so Strophe can process stanzas from WebSockets like BOSH - */ - _streamWrap: function (stanza) - { - return this.streamStart + stanza + ''; - }, - - - /** PrivateFunction: _closeSocket - * _Private_ function to close the WebSocket. - * - * Closes the socket if it is still open and deletes it - */ - _closeSocket: function () - { - if (this.socket) { try { - this.socket.close(); - } catch (e) {} } - this.socket = null; - }, - - /** PrivateFunction: _emptyQueue - * _Private_ function to check if the message queue is empty. - * - * Returns: - * True, because WebSocket messages are send immediately after queueing. - */ - _emptyQueue: function () - { - return true; - }, - - /** PrivateFunction: _onClose - * _Private_ function to handle websockets closing. - * - * Nothing to do here for WebSockets - */ - _onClose: function() { - if(this._conn.connected && !this._conn.disconnecting) { - Strophe.error("Websocket closed unexcectedly"); - this._conn._doDisconnect(); - } else { - Strophe.info("Websocket closed"); + return; + } else { + var string = this._streamWrap(message.data); + var elem = new DOMParser().parseFromString(string, "text/xml").documentElement; + this.socket.onmessage = this._onMessage.bind(this); + this._conn._connect_cb(elem, null, message.data); + } + }, + + /** PrivateFunction: _disconnect + * _Private_ function called by Strophe.Connection.disconnect + * + * Disconnects and sends a last stanza if one is given + * + * Parameters: + * (Request) pres - This stanza will be sent before disconnecting. + */ + _disconnect: function (pres) + { + if (this.socket.readyState !== WebSocket.CLOSED) { + if (pres) { + this._conn.send(pres); } - }, - - /** PrivateFunction: _no_auth_received - * - * Called on stream start/restart when no stream:features - * has been received. - */ - _no_auth_received: function (_callback) - { - Strophe.error("Server did not send any auth methods"); - this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "Server did not send any auth methods"); - if (_callback) { - _callback = _callback.bind(this._conn); - _callback(); + var close = ''; + this._conn.xmlOutput(document.createElement("stream:stream")); + this._conn.rawOutput(close); + try { + this.socket.send(close); + } catch (e) { + Strophe.info("Couldn't send closing stream tag."); } + } + + this._conn._doDisconnect(); + }, + + /** PrivateFunction: _doDisconnect + * _Private_ function to disconnect. + * + * Just closes the Socket for WebSockets + */ + _doDisconnect: function () + { + Strophe.info("WebSockets _doDisconnect was called"); + this._closeSocket(); + }, + + /** PrivateFunction _streamWrap + * _Private_ helper function to wrap a stanza in a tag. + * This is used so Strophe can process stanzas from WebSockets like BOSH + */ + _streamWrap: function (stanza) + { + return this.streamStart + stanza + ''; + }, + + + /** PrivateFunction: _closeSocket + * _Private_ function to close the WebSocket. + * + * Closes the socket if it is still open and deletes it + */ + _closeSocket: function () + { + if (this.socket) { try { + this.socket.close(); + } catch (e) {} } + this.socket = null; + }, + + /** PrivateFunction: _emptyQueue + * _Private_ function to check if the message queue is empty. + * + * Returns: + * True, because WebSocket messages are send immediately after queueing. + */ + _emptyQueue: function () + { + return true; + }, + + /** PrivateFunction: _onClose + * _Private_ function to handle websockets closing. + * + * Nothing to do here for WebSockets + */ + _onClose: function() { + if(this._conn.connected && !this._conn.disconnecting) { + Strophe.error("Websocket closed unexcectedly"); this._conn._doDisconnect(); - }, - - /** PrivateFunction: _onDisconnectTimeout - * _Private_ timeout handler for handling non-graceful disconnection. - * - * This does nothing for WebSockets - */ - _onDisconnectTimeout: function () {}, - - /** PrivateFunction: _onError - * _Private_ function to handle websockets errors. - * - * Parameters: - * (Object) error - The websocket error. - */ - _onError: function(error) { - Strophe.error("Websocket error " + error); - this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "The WebSocket connection could not be established was disconnected."); - this._disconnect(); - }, - - /** PrivateFunction: _onIdle - * _Private_ function called by Strophe.Connection._onIdle - * - * sends all queued stanzas - */ - _onIdle: function () { - var data = this._conn._data; - if (data.length > 0 && !this._conn.paused) { - for (var i = 0; i < data.length; i++) { - if (data[i] !== null) { - var stanza, rawStanza; - if (data[i] === "restart") { - stanza = this._buildStream(); - rawStanza = this._removeClosingTag(stanza); - stanza = stanza.tree(); - } else { - stanza = data[i]; - rawStanza = Strophe.serialize(stanza); - } - this._conn.xmlOutput(stanza); - this._conn.rawOutput(rawStanza); - this.socket.send(rawStanza); + } else { + Strophe.info("Websocket closed"); + } + }, + + /** PrivateFunction: _no_auth_received + * + * Called on stream start/restart when no stream:features + * has been received. + */ + _no_auth_received: function (_callback) + { + Strophe.error("Server did not send any auth methods"); + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "Server did not send any auth methods"); + if (_callback) { + _callback = _callback.bind(this._conn); + _callback(); + } + this._conn._doDisconnect(); + }, + + /** PrivateFunction: _onDisconnectTimeout + * _Private_ timeout handler for handling non-graceful disconnection. + * + * This does nothing for WebSockets + */ + _onDisconnectTimeout: function () {}, + + /** PrivateFunction: _onError + * _Private_ function to handle websockets errors. + * + * Parameters: + * (Object) error - The websocket error. + */ + _onError: function(error) { + Strophe.error("Websocket error " + error); + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "The WebSocket connection could not be established was disconnected."); + this._disconnect(); + }, + + /** PrivateFunction: _onIdle + * _Private_ function called by Strophe.Connection._onIdle + * + * sends all queued stanzas + */ + _onIdle: function () { + var data = this._conn._data; + if (data.length > 0 && !this._conn.paused) { + for (var i = 0; i < data.length; i++) { + if (data[i] !== null) { + var stanza, rawStanza; + if (data[i] === "restart") { + stanza = this._buildStream(); + rawStanza = this._removeClosingTag(stanza); + stanza = stanza.tree(); + } else { + stanza = data[i]; + rawStanza = Strophe.serialize(stanza); } + this._conn.xmlOutput(stanza); + this._conn.rawOutput(rawStanza); + this.socket.send(rawStanza); } - this._conn._data = []; } - }, - - /** PrivateFunction: _onMessage - * _Private_ function to handle websockets messages. - * - * This function parses each of the messages as if they are full documents. [TODO : We may actually want to use a SAX Push parser]. - * - * Since all XMPP traffic starts with "" - * The first stanza will always fail to be parsed... - * Addtionnaly, the seconds stanza will always be a with the stream NS defined in the previous stanza... so we need to 'force' the inclusion of the NS in this stanza! - * - * Parameters: - * (string) message - The websocket message. - */ - _onMessage: function(message) { - var elem, data; - // check for closing stream - if (message.data === "") { - var close = ""; - this._conn.rawInput(close); - this._conn.xmlInput(document.createElement("stream:stream")); - if (!this._conn.disconnecting) { - this._conn._doDisconnect(); - } - return; - } else if (message.data.search("/, ""); - elem = new DOMParser().parseFromString(data, "text/xml").documentElement; + this._conn._data = []; + } + }, - if (!this._handleStreamStart(elem)) { - return; - } - } else { - data = this._streamWrap(message.data); - elem = new DOMParser().parseFromString(data, "text/xml").documentElement; + /** PrivateFunction: _onMessage + * _Private_ function to handle websockets messages. + * + * This function parses each of the messages as if they are full documents. [TODO : We may actually want to use a SAX Push parser]. + * + * Since all XMPP traffic starts with "" + * The first stanza will always fail to be parsed... + * Addtionnaly, the seconds stanza will always be a with the stream NS defined in the previous stanza... so we need to 'force' the inclusion of the NS in this stanza! + * + * Parameters: + * (string) message - The websocket message. + */ + _onMessage: function(message) { + var elem, data; + // check for closing stream + if (message.data === "") { + var close = ""; + this._conn.rawInput(close); + this._conn.xmlInput(document.createElement("stream:stream")); + if (!this._conn.disconnecting) { + this._conn._doDisconnect(); } + return; + } else if (message.data.search("/, ""); + elem = new DOMParser().parseFromString(data, "text/xml").documentElement; - if (this._check_streamerror(elem, Strophe.Status.ERROR)) { + if (!this._handleStreamStart(elem)) { return; } + } else { + data = this._streamWrap(message.data); + elem = new DOMParser().parseFromString(data, "text/xml").documentElement; + } - //handle unavailable presence stanza before disconnecting - if (this._conn.disconnecting && - elem.firstChild.nodeName === "presence" && - elem.firstChild.getAttribute("type") === "unavailable") { - this._conn.xmlInput(elem); - this._conn.rawInput(Strophe.serialize(elem)); - // if we are already disconnecting we will ignore the unavailable stanza and - // wait for the tag before we close the connection - return; - } - this._conn._dataRecv(elem, message.data); - }, - - /** PrivateFunction: _onOpen - * _Private_ function to handle websockets connection setup. - * - * The opening stream tag is sent here. - */ - _onOpen: function() { - Strophe.info("Websocket open"); - var start = this._buildStream(); - this._conn.xmlOutput(start.tree()); - - var startString = this._removeClosingTag(start); - this._conn.rawOutput(startString); - this.socket.send(startString); - }, - - /** PrivateFunction: _removeClosingTag - * _Private_ function to Make the first non-selfclosing - * - * Parameters: - * (Object) elem - The tag. - * - * Returns: - * The stream:stream tag as String - */ - _removeClosingTag: function(elem) { - var string = Strophe.serialize(elem); - string = string.replace(/<(stream:stream .*[^\/])\/>$/, "<$1>"); - return string; - }, - - /** PrivateFunction: _reqToData - * _Private_ function to get a stanza out of a request. - * - * WebSockets don't use requests, so the passed argument is just returned. - * - * Parameters: - * (Object) stanza - The stanza. - * - * Returns: - * The stanza that was passed. - */ - _reqToData: function (stanza) - { - return stanza; - }, - - /** PrivateFunction: _send - * _Private_ part of the Connection.send function for WebSocket - * - * Just flushes the messages that are in the queue - */ - _send: function () { - this._conn.flush(); - }, - - /** PrivateFunction: _sendRestart - * - * Send an xmpp:restart stanza. - */ - _sendRestart: function () - { - clearTimeout(this._conn._idleTimeout); - this._conn._onIdle.bind(this._conn)(); + if (this._check_streamerror(elem, Strophe.Status.ERROR)) { + return; + } + + //handle unavailable presence stanza before disconnecting + if (this._conn.disconnecting && + elem.firstChild.nodeName === "presence" && + elem.firstChild.getAttribute("type") === "unavailable") { + this._conn.xmlInput(elem); + this._conn.rawInput(Strophe.serialize(elem)); + // if we are already disconnecting we will ignore the unavailable stanza and + // wait for the tag before we close the connection + return; } - }; - return Strophe; + this._conn._dataRecv(elem, message.data); + }, + + /** PrivateFunction: _onOpen + * _Private_ function to handle websockets connection setup. + * + * The opening stream tag is sent here. + */ + _onOpen: function() { + Strophe.info("Websocket open"); + var start = this._buildStream(); + this._conn.xmlOutput(start.tree()); + + var startString = this._removeClosingTag(start); + this._conn.rawOutput(startString); + this.socket.send(startString); + }, + + /** PrivateFunction: _removeClosingTag + * _Private_ function to Make the first non-selfclosing + * + * Parameters: + * (Object) elem - The tag. + * + * Returns: + * The stream:stream tag as String + */ + _removeClosingTag: function(elem) { + var string = Strophe.serialize(elem); + string = string.replace(/<(stream:stream .*[^\/])\/>$/, "<$1>"); + return string; + }, + + /** PrivateFunction: _reqToData + * _Private_ function to get a stanza out of a request. + * + * WebSockets don't use requests, so the passed argument is just returned. + * + * Parameters: + * (Object) stanza - The stanza. + * + * Returns: + * The stanza that was passed. + */ + _reqToData: function (stanza) + { + return stanza; + }, + + /** PrivateFunction: _send + * _Private_ part of the Connection.send function for WebSocket + * + * Just flushes the messages that are in the queue + */ + _send: function () { + this._conn.flush(); + }, + + /** PrivateFunction: _sendRestart + * + * Send an xmpp:restart stanza. + */ + _sendRestart: function () + { + clearTimeout(this._conn._idleTimeout); + this._conn._onIdle.bind(this._conn)(); + } +}; +return Strophe; })); From 93d333ed316f005022b5ee1b4dc7b7f2bab85064 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Tue, 3 Feb 2015 22:17:29 +0100 Subject: [PATCH 11/22] Add the concat task again. We need it to create strophe.js. The almond/r.js build cannot be used in its stead. --- Gruntfile.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index c695e6d9..ade39781 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -40,6 +40,18 @@ module.exports = function(grunt){ } }, + concat: { + dist: { + src: ['src/wrap_header.js', 'src/base64.js', 'src/sha1.js', 'src/md5.js', 'src/polyfills.js', 'src/core.js', 'src/bosh.js', 'src/websocket.js', 'src/wrap_footer.js'], + dest: '<%= pkg.name %>' + }, + options: { + process: function(src){ + return src.replace('@VERSION@', pkg.version); + } + } + }, + copy: { "prepare-release": { files:[ @@ -87,7 +99,7 @@ module.exports = function(grunt){ watch: { files: ['<%= jshint.files %>'], - tasks: ['build', 'uglify'] + tasks: ['concat', 'uglify'] }, natural_docs: { @@ -114,23 +126,24 @@ module.exports = function(grunt){ grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks("grunt-contrib-clean"); + grunt.loadNpmTasks("grunt-contrib-concat"); grunt.loadNpmTasks('grunt-shell'); grunt.loadNpmTasks('grunt-natural-docs'); grunt.loadNpmTasks('grunt-mkdir'); grunt.loadNpmTasks('grunt-contrib-qunit'); grunt.registerTask("default", ["jshint", "min"]); - grunt.registerTask("min", ["build", "uglify"]); + grunt.registerTask("min", ["concat", "uglify"]); grunt.registerTask("prepare-release", ["copy:prepare-release"]); - grunt.registerTask("doc", ["build", "copy:prepare-doc", "mkdir:prepare-doc", "natural_docs"]); + grunt.registerTask("doc", ["concat", "copy:prepare-doc", "mkdir:prepare-doc", "natural_docs"]); grunt.registerTask("release", ["default", "doc", "copy:prepare-release", "shell:tar", "shell:zip"]); grunt.registerTask("all", ["release", "clean"]); grunt.registerTask("test", ["connect", "qunit"]); - grunt.registerTask('concat', 'Create a new build', function () { + grunt.registerTask('almond', 'Create an almond build with r.js', function () { var done = this.async(); require('child_process').exec( - './node_modules/requirejs/bin/r.js -o build.js optimize=none out=strophe.js', + './node_modules/requirejs/bin/r.js -o build.js optimize=none out=strophe.almond.js', function (err, stdout, stderr) { if (err) { grunt.log.write('build failed with error code '+err.code); From 125ab01aa9f360e221c7bade16e92957583546b6 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Tue, 3 Feb 2015 22:18:35 +0100 Subject: [PATCH 12/22] Expose more functions in the non-AMD usecase. --- src/core.js | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/core.js b/src/core.js index 99fa4f43..92ba1408 100644 --- a/src/core.js +++ b/src/core.js @@ -35,11 +35,18 @@ } else { // Browser globals var o = factory(root.SHA1, root.Base64, root.MD5); - window.Strophe = o.Strophe; - window.$build = o.$build; - window.$iq = o.$iq; - window.$msg = o.$msg; - window.$pres = o.$pres; + window.Strophe = o.Strophe; + window.$build = o.$build; + window.$iq = o.$iq; + window.$msg = o.$msg; + window.$pres = o.$pres; + window.SHA1 = o.SHA1; + window.Base64 = o.Base64; + window.MD5 = o.MD5; + window.b64_hmac_sha1 = o.SHA1.b64_hmac_sha1; + window.b64_sha1 = o.SHA1.b64_sha1; + window.str_hmac_sha1 = o.SHA1.str_hmac_sha1; + window.str_sha1 = o.SHA1.str_sha1; } }(this, function (SHA1, Base64, MD5) { @@ -3289,13 +3296,13 @@ Strophe.SASLMD5.prototype.onChallenge = function(connection, challenge, test_cno Strophe.Connection.prototype.mechanisms[Strophe.SASLMD5.prototype.name] = Strophe.SASLMD5; return { - Strophe: Strophe, - $build: $build, - $msg: $msg, - $iq: $iq, - $pres: $pres, - SHA1: SHA1, - Base64: Base64, - MD5: MD5 + Strophe: Strophe, + $build: $build, + $msg: $msg, + $iq: $iq, + $pres: $pres, + SHA1: SHA1, + Base64: Base64, + MD5: MD5, }; })); From 4f7860009a301c6a50addbdedc46885142503772 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Tue, 3 Feb 2015 22:27:21 +0100 Subject: [PATCH 13/22] Generate strophe.js (from the AMD-enabled source files) --- strophe.js | 182 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 138 insertions(+), 44 deletions(-) diff --git a/strophe.js b/strophe.js index 117d8076..949fa12c 100644 --- a/strophe.js +++ b/strophe.js @@ -12,7 +12,17 @@ // public domain. It would be nice if you left this header intact. // Base64 code from Tyler Akins -- http://rumkin.com -var Base64 = (function () { +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(function () { + return factory(); + }); + } else { + // Browser globals + root.Base64 = factory(); + } +}(this, function () { var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; var obj = { @@ -86,9 +96,8 @@ var Base64 = (function () { return output; } }; - return obj; -})(); +})); /* * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined @@ -99,16 +108,22 @@ var Base64 = (function () { * See http://pajhome.org.uk/crypt/md5 for details. */ +/* jshint undef: true, unused: true:, noarg: true, latedef: true */ +/* global define */ + /* Some functions and variables have been stripped for use with Strophe */ -/* - * These are the functions you'll usually want to call - * They take string arguments and return either hex or base-64 encoded strings - */ -function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * 8));} -function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * 8));} -function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));} -function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));} +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(function () { + return factory(); + }); + } else { + // Browser globals + root.SHA1 = factory(); + } +}(this, function () { /* * Calculate the SHA-1 of an array of big-endian words, and a bit length @@ -267,6 +282,20 @@ function binb2b64(binarray) return str; } +/* + * These are the functions you'll usually want to call + * They take string arguments and return either hex or base-64 encoded strings + */ +return { + b64_hmac_sha1: function (key, data){ return binb2b64(core_hmac_sha1(key, data)); }, + b64_sha1: function (s) { return binb2b64(core_sha1(str2binb(s),s.length * 8)); }, + binb2str: binb2str, + core_hmac_sha1: core_hmac_sha1, + str_hmac_sha1: function (key, data){ return binb2str(core_hmac_sha1(key, data)); }, + str_sha1: function (s) { return binb2str(core_sha1(str2binb(s),s.length * 8)); }, +}; +})); + /* * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message * Digest Algorithm, as defined in RFC 1321. @@ -280,7 +309,17 @@ function binb2b64(binarray) * Everything that isn't used by Strophe has been stripped here! */ -var MD5 = (function () { +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(function () { + return factory(); + }); + } else { + // Browser globals + root.MD5 = factory(); + } +}(this, function (b) { /* * Add integers, wrapping at 2^32. This uses 16-bit operations internally * to work around bugs in some JS interpreters. @@ -456,7 +495,6 @@ var MD5 = (function () { return [a, b, c, d]; }; - var obj = { /* * These are the functions you'll usually want to call. @@ -471,9 +509,8 @@ var MD5 = (function () { return binl2str(core_md5(str2binl(s), s.length * 8)); } }; - return obj; -})(); +})); /* This program is distributed under the terms of the MIT license. @@ -580,10 +617,7 @@ if (!Array.prototype.indexOf) */ /* jshint undef: true, unused: true:, noarg: true, latedef: true */ -/*global document, window, setTimeout, clearTimeout, console, - ActiveXObject, Base64, MD5, DOMParser */ -// from sha1.js -/*global core_hmac_sha1, binb2str, str_hmac_sha1, str_sha1, b64_hmac_sha1*/ +/*global define, document, window, setTimeout, clearTimeout, console, ActiveXObject, DOMParser */ /** File: strophe.js * A JavaScript library for XMPP BOSH/XMPP over Websocket. @@ -598,12 +632,34 @@ if (!Array.prototype.indexOf) * For more information on XMPP-over WebSocket see this RFC draft: * http://tools.ietf.org/html/draft-ietf-xmpp-websocket-00 */ - -/* All of the Strophe globals are defined in this special function below so - * that references to the globals become closures. This will ensure that - * on page reload, these references will still be available to callbacks - * that are still executing. - */ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([ + 'strophe-sha1', + 'strophe-base64', + 'strophe-md5', + "strophe-polyfill" + ], function () { + return factory.apply(this, arguments); + }); + } else { + // Browser globals + var o = factory(root.SHA1, root.Base64, root.MD5); + window.Strophe = o.Strophe; + window.$build = o.$build; + window.$iq = o.$iq; + window.$msg = o.$msg; + window.$pres = o.$pres; + window.SHA1 = o.SHA1; + window.Base64 = o.Base64; + window.MD5 = o.MD5; + window.b64_hmac_sha1 = o.SHA1.b64_hmac_sha1; + window.b64_sha1 = o.SHA1.b64_sha1; + window.str_hmac_sha1 = o.SHA1.str_hmac_sha1; + window.str_sha1 = o.SHA1.str_sha1; + } +}(this, function (SHA1, Base64, MD5) { var Strophe; @@ -619,6 +675,7 @@ var Strophe; * 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. * @@ -628,9 +685,8 @@ function $build(name, attrs) { return new Strophe.Builder(name, attrs); } * Returns: * A new Strophe.Builder object. */ -/* jshint ignore:start */ function $msg(attrs) { return new Strophe.Builder("message", attrs); } -/* jshint ignore:end */ + /** Function: $iq * Create a Strophe.Builder with an element as the root. * @@ -641,6 +697,7 @@ function $msg(attrs) { return new Strophe.Builder("message", attrs); } * 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. * @@ -3730,26 +3787,26 @@ Strophe.SASLSHA1.prototype.onChallenge = function(connection, challenge, test_cn salt = Base64.decode(salt); salt += "\x00\x00\x00\x01"; - Hi = U_old = core_hmac_sha1(connection.pass, salt); + Hi = U_old = SHA1.core_hmac_sha1(connection.pass, salt); for (i = 1; i < iter; i++) { - U = core_hmac_sha1(connection.pass, binb2str(U_old)); + U = SHA1.core_hmac_sha1(connection.pass, SHA1.binb2str(U_old)); for (k = 0; k < 5; k++) { Hi[k] ^= U[k]; } U_old = U; } - Hi = binb2str(Hi); + Hi = SHA1.binb2str(Hi); - clientKey = core_hmac_sha1(Hi, "Client Key"); - serverKey = str_hmac_sha1(Hi, "Server Key"); - clientSignature = core_hmac_sha1(str_sha1(binb2str(clientKey)), authMessage); - connection._sasl_data["server-signature"] = b64_hmac_sha1(serverKey, authMessage); + clientKey = SHA1.core_hmac_sha1(Hi, "Client Key"); + serverKey = SHA1.str_hmac_sha1(Hi, "Server Key"); + clientSignature = SHA1.core_hmac_sha1(SHA1.str_sha1(SHA1.binb2str(clientKey)), authMessage); + connection._sasl_data["server-signature"] = SHA1.b64_hmac_sha1(serverKey, authMessage); for (k = 0; k < 5; k++) { clientKey[k] ^= clientSignature[k]; } - responseText += ",p=" + Base64.encode(binb2str(clientKey)); + responseText += ",p=" + Base64.encode(SHA1.binb2str(clientKey)); return responseText; }.bind(this); @@ -3840,9 +3897,8 @@ Strophe.SASLMD5.prototype.onChallenge = function(connection, challenge, test_cno MD5.hexdigest(A2)) + ","; responseText += 'qop=auth'; - this.onChallenge = function () - { - return ""; + this.onChallenge = function () { + return ""; }.bind(this); return responseText; @@ -3850,6 +3906,17 @@ Strophe.SASLMD5.prototype.onChallenge = function(connection, challenge, test_cno Strophe.Connection.prototype.mechanisms[Strophe.SASLMD5.prototype.name] = Strophe.SASLMD5; +return { + Strophe: Strophe, + $build: $build, + $msg: $msg, + $iq: $iq, + $pres: $pres, + SHA1: SHA1, + Base64: Base64, + MD5: MD5, +}; +})); /* This program is distributed under the terms of the MIT license. @@ -3859,10 +3926,22 @@ Strophe.Connection.prototype.mechanisms[Strophe.SASLMD5.prototype.name] = Stroph */ /* jshint undef: true, unused: true:, noarg: true, latedef: true */ -/*global window, setTimeout, clearTimeout, - XMLHttpRequest, ActiveXObject, - Strophe, $build */ - +/* global define, window, setTimeout, clearTimeout, XMLHttpRequest, ActiveXObject, Strophe, $build */ + +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['strophe-core'], function (core) { + return factory( + core.Strophe, + core.$build + ); + }); + } else { + // Browser globals + return factory(Strophe, $build); + } +}(this, function (Strophe, $build) { /** PrivateClass: Strophe.Request * _Private_ helper class that provides a cross implementation abstraction @@ -4706,6 +4785,8 @@ Strophe.Bosh.prototype = { } } }; +return Strophe; +})); /* This program is distributed under the terms of the MIT license. @@ -4715,8 +4796,19 @@ Strophe.Bosh.prototype = { */ /* jshint undef: true, unused: true:, noarg: true, latedef: true */ -/*global window, clearTimeout, WebSocket, - DOMParser, Strophe, $build */ +/* global define, window, clearTimeout, WebSocket, DOMParser, Strophe, $build */ + +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['strophe-core'], function (wrapper) { + return factory(wrapper.Strophe); + }); + } else { + // Browser globals + return factory(Strophe); + } +}(this, function (Strophe) { /** Class: Strophe.WebSocket * _Private_ helper class that handles WebSocket Connections @@ -5217,6 +5309,8 @@ Strophe.Websocket.prototype = { this._conn._onIdle.bind(this._conn)(); } }; +return Strophe; +})); /* jshint ignore:start */ if (callback) { From b42b75a75ab216ddf5a5ba65420050a2fe7790fc Mon Sep 17 00:00:00 2001 From: JC Brand Date: Wed, 4 Feb 2015 13:04:55 +0100 Subject: [PATCH 14/22] Use the destination file configured in the concat task. --- Gruntfile.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index ade39781..d0bd4fb9 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -19,7 +19,7 @@ module.exports = function(grunt){ "doc": ["<%= natural_docs.docs.output %>"], "prepare-release": ["strophejs-<%= pkg.version %>"], "release": ["strophejs-<%= pkg.version %>.zip", "strophejs-<%= pkg.version %>.tar.gz"], - "js": ["strophe.js", "strophe.min.js"] + "js": ["<%= concat.dist.dest %>", "strophe.min.js"] }, qunit: { @@ -57,7 +57,7 @@ module.exports = function(grunt){ files:[ { expand: true, - src:['', 'strophe.min.js', 'LICENSE.txt', 'README.txt', + src:['<%= concat.dist.dest %>', 'strophe.min.js', 'LICENSE.txt', 'README.txt', 'contrib/**', 'examples/**', 'plugins/**', 'tests/**', 'doc/**'], dest:"strophejs-<%= pkg.version %>" } @@ -66,7 +66,7 @@ module.exports = function(grunt){ "prepare-doc": { files:[ { - src:['strophe.js'], + src:['<%= concat.dist.dest %>'], dest:"<%= natural_docs.docs.inputs[0] %>" } ] @@ -93,7 +93,7 @@ module.exports = function(grunt){ banner: '/*! <%= pkg.name %> v<%= pkg.version %> - built on <%= grunt.template.today("dd-mm-yyyy") %> */\n' }, dist: { - files: { 'strophe.min.js': ['strophe.js'] } + files: { 'strophe.min.js': ['<%= concat.dist.dest %>'] } } }, @@ -125,8 +125,8 @@ module.exports = function(grunt){ grunt.loadNpmTasks("grunt-contrib-uglify"); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-watch'); - grunt.loadNpmTasks("grunt-contrib-clean"); grunt.loadNpmTasks("grunt-contrib-concat"); + grunt.loadNpmTasks("grunt-contrib-clean"); grunt.loadNpmTasks('grunt-shell'); grunt.loadNpmTasks('grunt-natural-docs'); grunt.loadNpmTasks('grunt-mkdir'); From bd04e79f5948cfba18a0e6017af0a8104abfa830 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Wed, 4 Feb 2015 13:08:47 +0100 Subject: [PATCH 15/22] Don't clean when calling make all. Also, small change to simplify diff with master --- Makefile | 2 +- src/core.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 41ccfc05..7b545cea 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ NDPROJ_DIR = ndproj STROPHE = strophe.js STROPHE_MIN = strophe.min.js -all: clean $(STROPHE_MIN) +all: $(STROPHE_MIN) stamp-npm: package.json npm install diff --git a/src/core.js b/src/core.js index 92ba1408..5ba78fbc 100644 --- a/src/core.js +++ b/src/core.js @@ -3286,7 +3286,8 @@ Strophe.SASLMD5.prototype.onChallenge = function(connection, challenge, test_cno MD5.hexdigest(A2)) + ","; responseText += 'qop=auth'; - this.onChallenge = function () { + this.onChallenge = function () + { return ""; }.bind(this); From fbccc30ca4701a6a54b061a02ece6b6948638a50 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Wed, 4 Feb 2015 13:10:51 +0100 Subject: [PATCH 16/22] Update changelog to mention AMD support --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index efeee0ac..5cbbbff0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Add commandline testing support via qunit-phantomjs-runner * Add integrated testing via TravisCI. * #25 Item-not-found-error caused by long term request. +* #29 Add support for the Asynchronous Module Definition (AMD) and require.js * #30 Base64 encoding problem in some older browsers. * #45 Move xhlr plugin to strophejs-plugins repo. * #62 Add `xmlunescape` method. From b4aeec57a6a4f3b416035cd2570c2b77a841938e Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sat, 7 Feb 2015 14:39:30 +0100 Subject: [PATCH 17/22] Bugfix. $build was not defined in websocket.js --- src/websocket.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/websocket.js b/src/websocket.js index e6b7bb27..e424bb84 100644 --- a/src/websocket.js +++ b/src/websocket.js @@ -11,14 +11,17 @@ (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. - define(['strophe-core'], function (wrapper) { - return factory(wrapper.Strophe); + define(['strophe-core'], function (core) { + return factory( + core.Strophe, + core.$build + ); }); } else { // Browser globals - return factory(Strophe); + return factory(Strophe, $build); } -}(this, function (Strophe) { +}(this, function (Strophe, $build) { /** Class: Strophe.WebSocket * _Private_ helper class that handles WebSocket Connections From eeeea2abbef0498fba3e1faf64645797731725ac Mon Sep 17 00:00:00 2001 From: Gordin <9ordin@gmail.com> Date: Fri, 20 Feb 2015 23:36:43 +0100 Subject: [PATCH 18/22] compile strophe --- strophe.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/strophe.js b/strophe.js index 949fa12c..91cb51ea 100644 --- a/strophe.js +++ b/strophe.js @@ -3897,7 +3897,8 @@ Strophe.SASLMD5.prototype.onChallenge = function(connection, challenge, test_cno MD5.hexdigest(A2)) + ","; responseText += 'qop=auth'; - this.onChallenge = function () { + this.onChallenge = function () + { return ""; }.bind(this); @@ -4801,14 +4802,17 @@ return Strophe; (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. - define(['strophe-core'], function (wrapper) { - return factory(wrapper.Strophe); + define(['strophe-core'], function (core) { + return factory( + core.Strophe, + core.$build + ); }); } else { // Browser globals - return factory(Strophe); + return factory(Strophe, $build); } -}(this, function (Strophe) { +}(this, function (Strophe, $build) { /** Class: Strophe.WebSocket * _Private_ helper class that handles WebSocket Connections From 71434e3fd47960310694e3fb2d502327abde0690 Mon Sep 17 00:00:00 2001 From: Gordin <9ordin@gmail.com> Date: Sat, 21 Feb 2015 00:00:38 +0100 Subject: [PATCH 19/22] v1.2.0 release + added strophe.min.js --- CHANGELOG.md | 9 ++++++++- package.json | 5 +++-- strophe.js | 2 +- strophe.min.js | 3 +++ 4 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 strophe.min.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cbbbff0..ee58b64b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,24 @@ # Strophe.js Change Log -## Version 1.1.4 - Unreleased +## Version 1.2.0 - 2015-02-21 * Add bower package manager support. * Add commandline testing support via qunit-phantomjs-runner * Add integrated testing via TravisCI. +* Fix Websocket connections now use the current XMPP-over-WebSockets RFC * #25 Item-not-found-error caused by long term request. * #29 Add support for the Asynchronous Module Definition (AMD) and require.js * #30 Base64 encoding problem in some older browsers. * #45 Move xhlr plugin to strophejs-plugins repo. +* #60 Fixed deletion of handlers in websocket connections * #62 Add `xmlunescape` method. +* #67 Use correct Content-Type in BOSH * #70 `_onDisconnectTimeout` never tiggers because maxRetries is undefined. +* #71 switched to case sensitive handling of XML elements * #73 `getElementsByTagName` problem with namespaced elements. +* #76 respect "Invalid SID" message +* #79 connect.pause work correctly again * #90 The queue data was not reset in .reset() method. +* #104 Websocket connections with MongooseIM work now ## Version 1.1.3 - 2014-01-20 * Fix SCRAM-SHA1 auth now works for multiple connections at the same time diff --git a/package.json b/package.json index 3f232ca4..b5e5bac2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "strophe.js", "description": "Strophe.js is an XMPP library for JavaScript", - "version": "1.1.4dev1", + "version": "1.2.0", "homepage": "http://strophe.im/strophejs", "repository": { "type": "git", @@ -30,7 +30,8 @@ "dodo", "Lee Boynton (lboynton)", "Theo Cushion (theozaurus)", - "Brendon Crawford (brendoncrawford)" + "Brendon Crawford (brendoncrawford)", + "JC Brand (jcbrand)" ], "licenses": [ "MIT" diff --git a/strophe.js b/strophe.js index 91cb51ea..0248deed 100644 --- a/strophe.js +++ b/strophe.js @@ -721,7 +721,7 @@ Strophe = { * The version of the Strophe library. Unreleased builds will have * a version of head-HASH where HASH is a partial revision. */ - VERSION: "1.1.4dev1", + VERSION: "1.2.0", /** Constants: XMPP Namespace Constants * Common namespace constants from the XMPP RFCs and XEPs. diff --git a/strophe.min.js b/strophe.min.js new file mode 100644 index 00000000..255e8c0b --- /dev/null +++ b/strophe.min.js @@ -0,0 +1,3 @@ +/*! strophe.js v1.2.0 - built on 21-02-2015 */ +!function(a){return function(a,b){"function"==typeof define&&define.amd?define(function(){return b()}):a.Base64=b()}(this,function(){var a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",b={encode:function(b){var c,d,e,f,g,h,i,j="",k=0;do c=b.charCodeAt(k++),d=b.charCodeAt(k++),e=b.charCodeAt(k++),f=c>>2,g=(3&c)<<4|d>>4,h=(15&d)<<2|e>>6,i=63&e,isNaN(d)?(g=(3&c)<<4,h=i=64):isNaN(e)&&(i=64),j=j+a.charAt(f)+a.charAt(g)+a.charAt(h)+a.charAt(i);while(k>4,d=(15&g)<<4|h>>2,e=(3&h)<<6|i,j+=String.fromCharCode(c),64!=h&&(j+=String.fromCharCode(d)),64!=i&&(j+=String.fromCharCode(e));while(k>5]|=128<<24-d%32,a[(d+64>>9<<4)+15]=d;var g,h,i,j,k,l,m,n,o=new Array(80),p=1732584193,q=-271733879,r=-1732584194,s=271733878,t=-1009589776;for(g=0;gh;h++)o[h]=16>h?a[g+h]:f(o[h-3]^o[h-8]^o[h-14]^o[h-16],1),i=e(e(f(p,5),b(h,q,r,s)),e(e(t,o[h]),c(h))),t=s,s=r,r=f(q,30),q=p,p=i;p=e(p,j),q=e(q,k),r=e(r,l),s=e(s,m),t=e(t,n)}return[p,q,r,s,t]}function b(a,b,c,d){return 20>a?b&c|~b&d:40>a?b^c^d:60>a?b&c|b&d|c&d:b^c^d}function c(a){return 20>a?1518500249:40>a?1859775393:60>a?-1894007588:-899497514}function d(b,c){var d=g(b);d.length>16&&(d=a(d,8*b.length));for(var e=new Array(16),f=new Array(16),h=0;16>h;h++)e[h]=909522486^d[h],f[h]=1549556828^d[h];var i=a(e.concat(g(c)),512+8*c.length);return a(f.concat(i),672)}function e(a,b){var c=(65535&a)+(65535&b),d=(a>>16)+(b>>16)+(c>>16);return d<<16|65535&c}function f(a,b){return a<>>32-b}function g(a){for(var b=[],c=255,d=0;d<8*a.length;d+=8)b[d>>5]|=(a.charCodeAt(d/8)&c)<<24-d%32;return b}function h(a){for(var b="",c=255,d=0;d<32*a.length;d+=8)b+=String.fromCharCode(a[d>>5]>>>24-d%32&c);return b}function i(a){for(var b,c,d="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",e="",f=0;f<4*a.length;f+=3)for(b=(a[f>>2]>>8*(3-f%4)&255)<<16|(a[f+1>>2]>>8*(3-(f+1)%4)&255)<<8|a[f+2>>2]>>8*(3-(f+2)%4)&255,c=0;4>c;c++)e+=8*f+6*c>32*a.length?"=":d.charAt(b>>6*(3-c)&63);return e}return{b64_hmac_sha1:function(a,b){return i(d(a,b))},b64_sha1:function(b){return i(a(g(b),8*b.length))},binb2str:h,core_hmac_sha1:d,str_hmac_sha1:function(a,b){return h(d(a,b))},str_sha1:function(b){return h(a(g(b),8*b.length))}}}),function(a,b){"function"==typeof define&&define.amd?define(function(){return b()}):a.MD5=b()}(this,function(){var a=function(a,b){var c=(65535&a)+(65535&b),d=(a>>16)+(b>>16)+(c>>16);return d<<16|65535&c},b=function(a,b){return a<>>32-b},c=function(a){for(var b=[],c=0;c<8*a.length;c+=8)b[c>>5]|=(255&a.charCodeAt(c/8))<>5]>>>c%32&255);return b},e=function(a){for(var b="0123456789abcdef",c="",d=0;d<4*a.length;d++)c+=b.charAt(a[d>>2]>>d%4*8+4&15)+b.charAt(a[d>>2]>>d%4*8&15);return c},f=function(c,d,e,f,g,h){return a(b(a(a(d,c),a(f,h)),g),e)},g=function(a,b,c,d,e,g,h){return f(b&c|~b&d,a,b,e,g,h)},h=function(a,b,c,d,e,g,h){return f(b&d|c&~d,a,b,e,g,h)},i=function(a,b,c,d,e,g,h){return f(b^c^d,a,b,e,g,h)},j=function(a,b,c,d,e,g,h){return f(c^(b|~d),a,b,e,g,h)},k=function(b,c){b[c>>5]|=128<>>9<<4)+14]=c;for(var d,e,f,k,l=1732584193,m=-271733879,n=-1732584194,o=271733878,p=0;pc?Math.ceil(c):Math.floor(c),0>c&&(c+=b);b>c;c++)if(c in this&&this[c]===a)return c;return-1}),function(a,b){if("function"==typeof define&&define.amd)define(["strophe-sha1","strophe-base64","strophe-md5","strophe-polyfill"],function(){return b.apply(this,arguments)});else{var c=b(a.SHA1,a.Base64,a.MD5);window.Strophe=c.Strophe,window.$build=c.$build,window.$iq=c.$iq,window.$msg=c.$msg,window.$pres=c.$pres,window.SHA1=c.SHA1,window.Base64=c.Base64,window.MD5=c.MD5,window.b64_hmac_sha1=c.SHA1.b64_hmac_sha1,window.b64_sha1=c.SHA1.b64_sha1,window.str_hmac_sha1=c.SHA1.str_hmac_sha1,window.str_sha1=c.SHA1.str_sha1}}(this,function(a,b,c){function d(a,b){return new h.Builder(a,b)}function e(a){return new h.Builder("message",a)}function f(a){return new h.Builder("iq",a)}function g(a){return new h.Builder("presence",a)}var h;return h={VERSION:"1.2.0",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",FRAMING:"urn:ietf:params:xml:ns:xmpp-framing",BIND:"urn:ietf:params:xml:ns:xmpp-bind",SESSION:"urn:ietf:params:xml:ns:xmpp-session",VERSION:"jabber:iq:version",STANZAS:"urn:ietf:params:xml:ns:xmpp-stanzas",XHTML_IM:"http://jabber.org/protocol/xhtml-im",XHTML:"http://www.w3.org/1999/xhtml"},XHTML:{tags:["a","blockquote","br","cite","em","img","li","ol","p","span","strong","ul","body"],attributes:{a:["href"],blockquote:["style"],br:[],cite:["style"],em:[],img:["src","alt","style","height","width"],li:["style"],ol:["style"],p:["style"],span:["style"],strong:[],ul:["style"],body:[]},css:["background-color","color","font-family","font-size","font-style","font-weight","margin-left","margin-right","text-align","text-decoration"],validTag:function(a){for(var b=0;b0)for(var c=0;c/g,">"),a=a.replace(/'/g,"'"),a=a.replace(/"/g,""")},xmlunescape:function(a){return a=a.replace(/\&/g,"&"),a=a.replace(/</g,"<"),a=a.replace(/>/g,">"),a=a.replace(/'/g,"'"),a=a.replace(/"/g,'"')},xmlTextNode:function(a){return h.xmlGenerator().createTextNode(a)},xmlHtmlNode:function(a){var b;if(window.DOMParser){var c=new DOMParser;b=c.parseFromString(a,"text/xml")}else b=new ActiveXObject("Microsoft.XMLDOM"),b.async="false",b.loadXML(a);return b},getText:function(a){if(!a)return null;var b="";0===a.childNodes.length&&a.nodeType==h.ElementType.TEXT&&(b+=a.nodeValue);for(var c=0;c0&&(g=i.join("; "),c.setAttribute(f,g))}else c.setAttribute(f,g);for(b=0;b/g,"\\3e").replace(/@/g,"\\40")},unescapeNode:function(a){return"string"!=typeof a?a:a.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,"\\")},getNodeFromJid:function(a){return a.indexOf("@")<0?null:a.split("@")[0]},getDomainFromJid:function(a){var b=h.getBareJidFromJid(a);if(b.indexOf("@")<0)return b;var c=b.split("@");return c.splice(0,1),c.join("@")},getResourceFromJid:function(a){var b=a.split("/");return b.length<2?null:(b.splice(0,1),b.join("/"))},getBareJidFromJid:function(a){return a?a.split("/")[0]:null},log:function(){},debug:function(a){this.log(this.LogLevel.DEBUG,a)},info:function(a){this.log(this.LogLevel.INFO,a)},warn:function(a){this.log(this.LogLevel.WARN,a)},error:function(a){this.log(this.LogLevel.ERROR,a)},fatal:function(a){this.log(this.LogLevel.FATAL,a)},serialize:function(a){var b;if(!a)return null;"function"==typeof a.tree&&(a=a.tree());var c,d,e=a.nodeName;for(a.getAttribute("_realname")&&(e=a.getAttribute("_realname")),b="<"+e,c=0;c/g,">").replace(/0){for(b+=">",c=0;c"}b+=""}else b+="/>";return b},_requestId:0,_connectionPlugins:{},addConnectionPlugin:function(a,b){h._connectionPlugins[a]=b}},h.Builder=function(a,b){("presence"==a||"message"==a||"iq"==a)&&(b&&!b.xmlns?b.xmlns=h.NS.CLIENT:b||(b={xmlns:h.NS.CLIENT})),this.nodeTree=h.xmlElement(a,b),this.node=this.nodeTree},h.Builder.prototype={tree:function(){return this.nodeTree},toString:function(){return h.serialize(this.nodeTree)},up:function(){return this.node=this.node.parentNode,this},attrs:function(a){for(var b in a)a.hasOwnProperty(b)&&this.node.setAttribute(b,a[b]);return this},c:function(a,b,c){var d=h.xmlElement(a,b,c);return this.node.appendChild(d),c||(this.node=d),this},cnode:function(a){var b,c=h.xmlGenerator();try{b=void 0!==c.importNode}catch(d){b=!1}var e=b?c.importNode(a,!0):h.copyElement(a);return this.node.appendChild(e),this.node=e,this},t:function(a){var b=h.xmlTextNode(a);return this.node.appendChild(b),this},h:function(a){var b=document.createElement("body");b.innerHTML=a;for(var c=h.createHtml(b);c.childNodes.length>0;)this.node.appendChild(c.childNodes[0]);return this}},h.Handler=function(a,b,c,d,e,f,g){this.handler=a,this.ns=b,this.name=c,this.type=d,this.id=e,this.options=g||{matchBare:!1},this.options.matchBare||(this.options.matchBare=!1),this.from=this.options.matchBare?f?h.getBareJidFromJid(f):null:f,this.user=!0},h.Handler.prototype={isMatch:function(a){var b,c=null;if(c=this.options.matchBare?h.getBareJidFromJid(a.getAttribute("from")):a.getAttribute("from"),b=!1,this.ns){var d=this;h.forEachChild(a,null,function(a){a.getAttribute("xmlns")==d.ns&&(b=!0)}),b=b||a.getAttribute("xmlns")==this.ns}else b=!0;var e=a.getAttribute("type");return!b||this.name&&!h.isTagEqual(a,this.name)||this.type&&(Array.isArray(this.type)?-1==this.type.indexOf(e):e!=this.type)||this.id&&a.getAttribute("id")!=this.id||this.from&&c!=this.from?!1:!0},run:function(a){var b=null;try{b=this.handler(a)}catch(c){throw c.sourceURL?h.fatal("error: "+this.handler+" "+c.sourceURL+":"+c.line+" - "+c.name+": "+c.message):c.fileName?("undefined"!=typeof console&&(console.trace(),console.error(this.handler," - error - ",c,c.message)),h.fatal("error: "+this.handler+" "+c.fileName+":"+c.lineNumber+" - "+c.name+": "+c.message)):h.fatal("error: "+c.message+"\n"+c.stack),c}return b},toString:function(){return"{Handler: "+this.handler+"("+this.name+","+this.id+","+this.ns+")}"}},h.TimedHandler=function(a,b){this.period=a,this.handler=b,this.lastCalled=(new Date).getTime(),this.user=!0},h.TimedHandler.prototype={run:function(){return this.lastCalled=(new Date).getTime(),this.handler()},reset:function(){this.lastCalled=(new Date).getTime()},toString:function(){return"{TimedHandler: "+this.handler+"("+this.period+")}"}},h.Connection=function(a,b){this.service=a,this.options=b||{};var c=this.options.protocol||"";this._proto=0===a.indexOf("ws:")||0===a.indexOf("wss:")||0===c.indexOf("ws")?new h.Websocket(this):new h.Bosh(this),this.jid="",this.domain=null,this.features=null,this._sasl_data={},this.do_session=!1,this.do_bind=!1,this.timedHandlers=[],this.handlers=[],this.removeTimeds=[],this.removeHandlers=[],this.addTimeds=[],this.addHandlers=[],this._authentication={},this._idleTimeout=null,this._disconnectTimeout=null,this.do_authentication=!0,this.authenticated=!1,this.disconnecting=!1,this.connected=!1,this.paused=!1,this._data=[],this._uniqueId=0,this._sasl_success_handler=null,this._sasl_failure_handler=null,this._sasl_challenge_handler=null,this.maxRetries=5,this._idleTimeout=setTimeout(this._onIdle.bind(this),100);for(var d in h._connectionPlugins)if(h._connectionPlugins.hasOwnProperty(d)){var e=h._connectionPlugins[d],f=function(){};f.prototype=e,this[d]=new f,this[d].init(this)}},h.Connection.prototype={reset:function(){this._proto._reset(),this.do_session=!1,this.do_bind=!1,this.timedHandlers=[],this.handlers=[],this.removeTimeds=[],this.removeHandlers=[],this.addTimeds=[],this.addHandlers=[],this._authentication={},this.authenticated=!1,this.disconnecting=!1,this.connected=!1,this._data=[],this._requests=[],this._uniqueId=0},pause:function(){this.paused=!0},resume:function(){this.paused=!1},getUniqueId:function(a){return"string"==typeof a||"number"==typeof a?++this._uniqueId+":"+a:++this._uniqueId+""},connect:function(a,b,c,d,e,f){this.jid=a,this.authzid=h.getBareJidFromJid(this.jid),this.authcid=h.getNodeFromJid(this.jid),this.pass=b,this.servtype="xmpp",this.connect_callback=c,this.disconnecting=!1,this.connected=!1,this.authenticated=!1,this.domain=h.getDomainFromJid(this.jid),this._changeConnectStatus(h.Status.CONNECTING,null),this._proto._connect(d,e,f)},attach:function(a,b,c,d,e,f,g){this._proto._attach(a,b,c,d,e,f,g)},xmlInput:function(){},xmlOutput:function(){},rawInput:function(){},rawOutput:function(){},send:function(a){if(null!==a){if("function"==typeof a.sort)for(var b=0;b=0&&this.addHandlers.splice(b,1)},disconnect:function(a){if(this._changeConnectStatus(h.Status.DISCONNECTING,a),h.info("Disconnect was called because: "+a),this.connected){var b=!1;this.disconnecting=!0,this.authenticated&&(b=g({xmlns:h.NS.CLIENT,type:"unavailable"})),this._disconnectTimeout=this._addSysTimedHandler(3e3,this._onDisconnectTimeout.bind(this)),this._proto._disconnect(b)}else h.info("Disconnect was called before Strophe connected to the server"),this._proto._abortAllRequests()},_changeConnectStatus:function(a,b){for(var c in h._connectionPlugins)if(h._connectionPlugins.hasOwnProperty(c)){var d=this[c];if(d.statusChanged)try{d.statusChanged(a,b)}catch(e){h.error(""+c+" plugin caused an exception changing status: "+e)}}if(this.connect_callback)try{this.connect_callback(a,b)}catch(f){h.error("User connection callback caused an exception: "+f)}},_doDisconnect:function(){"number"==typeof this._idleTimeout&&clearTimeout(this._idleTimeout),null!==this._disconnectTimeout&&(this.deleteTimedHandler(this._disconnectTimeout),this._disconnectTimeout=null),h.info("_doDisconnect was called"),this._proto._doDisconnect(),this.authenticated=!1,this.disconnecting=!1,this.handlers=[],this.timedHandlers=[],this.removeTimeds=[],this.removeHandlers=[],this.addTimeds=[],this.addHandlers=[],this._changeConnectStatus(h.Status.DISCONNECTED,null),this.connected=!1},_dataRecv:function(a,b){h.info("_dataRecv called");var c=this._proto._reqToData(a);if(null!==c){this.xmlInput!==h.Connection.prototype.xmlInput&&(c.nodeName===this._proto.strip&&c.childNodes.length?this.xmlInput(c.childNodes[0]):this.xmlInput(c)),this.rawInput!==h.Connection.prototype.rawInput&&(b?this.rawInput(b):this.rawInput(h.serialize(c)));for(var d,e;this.removeHandlers.length>0;)e=this.removeHandlers.pop(),d=this.handlers.indexOf(e),d>=0&&this.handlers.splice(d,1);for(;this.addHandlers.length>0;)this.handlers.push(this.addHandlers.pop());if(this.disconnecting&&this._proto._emptyQueue())return this._doDisconnect(),void 0;var f,g,i=c.getAttribute("type");if(null!==i&&"terminate"==i){if(this.disconnecting)return;return f=c.getAttribute("condition"),g=c.getElementsByTagName("conflict"),null!==f?("remote-stream-error"==f&&g.length>0&&(f="conflict"),this._changeConnectStatus(h.Status.CONNFAIL,f)):this._changeConnectStatus(h.Status.CONNFAIL,"unknown"),this._doDisconnect(),void 0}var j=this;h.forEachChild(c,null,function(a){var b,c;for(c=j.handlers,j.handlers=[],b=0;b0,j=d.getElementsByTagName("mechanism"),k=[],l=!1;if(!i)return this._proto._no_auth_received(b),void 0;if(j.length>0)for(f=0;f0,(l=this._authentication.legacy_auth||k.length>0)?(this.do_authentication!==!1&&this.authenticate(k),void 0):(this._proto._no_auth_received(b),void 0)}}},authenticate:function(a){var c;for(c=0;ca[e].prototype.priority&&(e=g);if(e!=c){var i=a[c];a[c]=a[e],a[e]=i}}var j=!1;for(c=0;c0&&(b="conflict"),this._changeConnectStatus(h.Status.AUTHFAIL,b),!1}var d,e=a.getElementsByTagName("bind");return e.length>0?(d=e[0].getElementsByTagName("jid"),d.length>0&&(this.jid=h.getText(d[0]),this.do_session?(this._addSysHandler(this._sasl_session_cb.bind(this),null,null,null,"_session_auth_2"),this.send(f({type:"set",id:"_session_auth_2"}).c("session",{xmlns:h.NS.SESSION}).tree())):(this.authenticated=!0,this._changeConnectStatus(h.Status.CONNECTED,null))),void 0):(h.info("SASL binding failed."),this._changeConnectStatus(h.Status.AUTHFAIL,null),!1)},_sasl_session_cb:function(a){if("result"==a.getAttribute("type"))this.authenticated=!0,this._changeConnectStatus(h.Status.CONNECTED,null);else if("error"==a.getAttribute("type"))return h.info("Session creation failed."),this._changeConnectStatus(h.Status.AUTHFAIL,null),!1;return!1},_sasl_failure_cb:function(){return this._sasl_success_handler&&(this.deleteHandler(this._sasl_success_handler),this._sasl_success_handler=null),this._sasl_challenge_handler&&(this.deleteHandler(this._sasl_challenge_handler),this._sasl_challenge_handler=null),this._sasl_mechanism&&this._sasl_mechanism.onFailure(),this._changeConnectStatus(h.Status.AUTHFAIL,null),!1},_auth2_cb:function(a){return"result"==a.getAttribute("type")?(this.authenticated=!0,this._changeConnectStatus(h.Status.CONNECTED,null)):"error"==a.getAttribute("type")&&(this._changeConnectStatus(h.Status.AUTHFAIL,null),this.disconnect("authentication failed")),!1},_addSysTimedHandler:function(a,b){var c=new h.TimedHandler(a,b);return c.user=!1,this.addTimeds.push(c),c},_addSysHandler:function(a,b,c,d,e){var f=new h.Handler(a,b,c,d,e);return f.user=!1,this.addHandlers.push(f),f},_onDisconnectTimeout:function(){return h.info("_onDisconnectTimeout was called"),this._proto._onDisconnectTimeout(),this._doDisconnect(),!1},_onIdle:function(){for(var a,b,c,d;this.addTimeds.length>0;)this.timedHandlers.push(this.addTimeds.pop());for(;this.removeTimeds.length>0;)b=this.removeTimeds.pop(),a=this.timedHandlers.indexOf(b),a>=0&&this.timedHandlers.splice(a,1);var e=(new Date).getTime();for(d=[],a=0;a=c-e?b.run()&&d.push(b):d.push(b));this.timedHandlers=d,clearTimeout(this._idleTimeout),this._proto._onIdle(),this.connected&&(this._idleTimeout=setTimeout(this._onIdle.bind(this),100))}},h.SASLMechanism=function(a,b,c){this.name=a,this.isClientFirst=b,this.priority=c},h.SASLMechanism.prototype={test:function(){return!0},onStart:function(a){this._connection=a},onChallenge:function(){throw new Error("You should implement challenge handling!")},onFailure:function(){this._connection=null},onSuccess:function(){this._connection=null}},h.SASLAnonymous=function(){},h.SASLAnonymous.prototype=new h.SASLMechanism("ANONYMOUS",!1,10),h.SASLAnonymous.test=function(a){return null===a.authcid},h.Connection.prototype.mechanisms[h.SASLAnonymous.prototype.name]=h.SASLAnonymous,h.SASLPlain=function(){},h.SASLPlain.prototype=new h.SASLMechanism("PLAIN",!0,20),h.SASLPlain.test=function(a){return null!==a.authcid},h.SASLPlain.prototype.onChallenge=function(a){var b=a.authzid; +return b+="\x00",b+=a.authcid,b+="\x00",b+=a.pass},h.Connection.prototype.mechanisms[h.SASLPlain.prototype.name]=h.SASLPlain,h.SASLSHA1=function(){},h.SASLSHA1.prototype=new h.SASLMechanism("SCRAM-SHA-1",!0,40),h.SASLSHA1.test=function(a){return null!==a.authcid},h.SASLSHA1.prototype.onChallenge=function(d,e,f){var g=f||c.hexdigest(1234567890*Math.random()),h="n="+d.authcid;return h+=",r=",h+=g,d._sasl_data.cnonce=g,d._sasl_data["client-first-message-bare"]=h,h="n,,"+h,this.onChallenge=function(c,d){for(var e,f,g,h,i,j,k,l,m,n,o,p="c=biws,",q=c._sasl_data["client-first-message-bare"]+","+d+",",r=c._sasl_data.cnonce,s=/([a-z]+)=([^,]+)(,|$)/;d.match(s);){var t=d.match(s);switch(d=d.replace(t[0],""),t[1]){case"r":e=t[2];break;case"s":f=t[2];break;case"i":g=t[2]}}if(e.substr(0,r.length)!==r)return c._sasl_data={},c._sasl_failure_cb();for(p+="r="+e,q+=p,f=b.decode(f),f+="\x00\x00\x00",h=j=a.core_hmac_sha1(c.pass,f),k=1;g>k;k++){for(i=a.core_hmac_sha1(c.pass,a.binb2str(j)),l=0;5>l;l++)h[l]^=i[l];j=i}for(h=a.binb2str(h),m=a.core_hmac_sha1(h,"Client Key"),n=a.str_hmac_sha1(h,"Server Key"),o=a.core_hmac_sha1(a.str_sha1(a.binb2str(m)),q),c._sasl_data["server-signature"]=a.b64_hmac_sha1(n,q),l=0;5>l;l++)m[l]^=o[l];return p+=",p="+b.encode(a.binb2str(m))}.bind(this),h},h.Connection.prototype.mechanisms[h.SASLSHA1.prototype.name]=h.SASLSHA1,h.SASLMD5=function(){},h.SASLMD5.prototype=new h.SASLMechanism("DIGEST-MD5",!1,30),h.SASLMD5.test=function(a){return null!==a.authcid},h.SASLMD5.prototype._quote=function(a){return'"'+a.replace(/\\/g,"\\\\").replace(/"/g,'\\"')+'"'},h.SASLMD5.prototype.onChallenge=function(a,b,d){for(var e,f=/([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/,g=d||c.hexdigest(""+1234567890*Math.random()),h="",i=null,j="",k="";b.match(f);)switch(e=b.match(f),b=b.replace(e[0],""),e[2]=e[2].replace(/^"(.+)"$/,"$1"),e[1]){case"realm":h=e[2];break;case"nonce":j=e[2];break;case"qop":k=e[2];break;case"host":i=e[2]}var l=a.servtype+"/"+a.domain;null!==i&&(l=l+"/"+i);var m=c.hash(a.authcid+":"+h+":"+this._connection.pass)+":"+j+":"+g,n="AUTHENTICATE:"+l,o="";return o+="charset=utf-8,",o+="username="+this._quote(a.authcid)+",",o+="realm="+this._quote(h)+",",o+="nonce="+this._quote(j)+",",o+="nc=00000001,",o+="cnonce="+this._quote(g)+",",o+="digest-uri="+this._quote(l)+",",o+="response="+c.hexdigest(c.hexdigest(m)+":"+j+":00000001:"+g+":auth:"+c.hexdigest(n))+",",o+="qop=auth",this.onChallenge=function(){return""}.bind(this),o},h.Connection.prototype.mechanisms[h.SASLMD5.prototype.name]=h.SASLMD5,{Strophe:h,$build:d,$msg:e,$iq:f,$pres:g,SHA1:a,Base64:b,MD5:c}}),function(a,b){return"function"==typeof define&&define.amd?(define(["strophe-core"],function(a){return b(a.Strophe,a.$build)}),void 0):b(Strophe,$build)}(this,function(a,b){return a.Request=function(b,c,d,e){this.id=++a._requestId,this.xmlData=b,this.data=a.serialize(b),this.origFunc=c,this.func=c,this.rid=d,this.date=0/0,this.sends=e||0,this.abort=!1,this.dead=null,this.age=function(){if(!this.date)return 0;var a=new Date;return(a-this.date)/1e3},this.timeDead=function(){if(!this.dead)return 0;var a=new Date;return(a-this.dead)/1e3},this.xhr=this._newXHR()},a.Request.prototype={getResponse:function(){var b=null;if(this.xhr.responseXML&&this.xhr.responseXML.documentElement){if(b=this.xhr.responseXML.documentElement,"parsererror"==b.tagName)throw a.error("invalid response received"),a.error("responseText: "+this.xhr.responseText),a.error("responseXML: "+a.serialize(this.xhr.responseXML)),"parsererror"}else this.xhr.responseText&&(a.error("invalid response received"),a.error("responseText: "+this.xhr.responseText),a.error("responseXML: "+a.serialize(this.xhr.responseXML)));return b},_newXHR:function(){var a=null;return window.XMLHttpRequest?(a=new XMLHttpRequest,a.overrideMimeType&&a.overrideMimeType("text/xml; charset=utf-8")):window.ActiveXObject&&(a=new ActiveXObject("Microsoft.XMLHTTP")),a.onreadystatechange=this.func.bind(null,this),a}},a.Bosh=function(a){this._conn=a,this.rid=Math.floor(4294967295*Math.random()),this.sid=null,this.hold=1,this.wait=60,this.window=5,this.errors=0,this._requests=[]},a.Bosh.prototype={strip:null,_buildBody:function(){var c=b("body",{rid:this.rid++,xmlns:a.NS.HTTPBIND});return null!==this.sid&&c.attrs({sid:this.sid}),c},_reset:function(){this.rid=Math.floor(4294967295*Math.random()),this.sid=null,this.errors=0},_connect:function(b,c,d){this.wait=b||this.wait,this.hold=c||this.hold,this.errors=0;var e=this._buildBody().attrs({to:this._conn.domain,"xml:lang":"en",wait:this.wait,hold:this.hold,content:"text/xml; charset=utf-8",ver:"1.6","xmpp:version":"1.0","xmlns:xmpp":a.NS.BOSH});d&&e.attrs({route:d});var f=this._conn._connect_cb;this._requests.push(new a.Request(e.tree(),this._onRequestStateChange.bind(this,f.bind(this._conn)),e.tree().getAttribute("rid"))),this._throttledRequestHandler()},_attach:function(b,c,d,e,f,g,h){this._conn.jid=b,this.sid=c,this.rid=d,this._conn.connect_callback=e,this._conn.domain=a.getDomainFromJid(this._conn.jid),this._conn.authenticated=!0,this._conn.connected=!0,this.wait=f||this.wait,this.hold=g||this.hold,this.window=h||this.window,this._conn._changeConnectStatus(a.Status.ATTACHED,null)},_connect_cb:function(b){var c,d,e=b.getAttribute("type");if(null!==e&&"terminate"==e)return a.error("BOSH-Connection failed: "+c),c=b.getAttribute("condition"),d=b.getElementsByTagName("conflict"),null!==c?("remote-stream-error"==c&&d.length>0&&(c="conflict"),this._conn._changeConnectStatus(a.Status.CONNFAIL,c)):this._conn._changeConnectStatus(a.Status.CONNFAIL,"unknown"),this._conn._doDisconnect(),a.Status.CONNFAIL;this.sid||(this.sid=b.getAttribute("sid"));var f=b.getAttribute("requests");f&&(this.window=parseInt(f,10));var g=b.getAttribute("hold");g&&(this.hold=parseInt(g,10));var h=b.getAttribute("wait");h&&(this.wait=parseInt(h,10))},_disconnect:function(a){this._sendTerminate(a)},_doDisconnect:function(){this.sid=null,this.rid=Math.floor(4294967295*Math.random())},_emptyQueue:function(){return 0===this._requests.length},_hitError:function(b){this.errors++,a.warn("request errored, status: "+b+", number of errors: "+this.errors),this.errors>4&&this._conn._onDisconnectTimeout()},_no_auth_received:function(b){b=b?b.bind(this._conn):this._conn._connect_cb.bind(this._conn);var c=this._buildBody();this._requests.push(new a.Request(c.tree(),this._onRequestStateChange.bind(this,b.bind(this._conn)),c.tree().getAttribute("rid"))),this._throttledRequestHandler()},_onDisconnectTimeout:function(){this._abortAllRequests()},_abortAllRequests:function(){for(var a;this._requests.length>0;)a=this._requests.pop(),a.abort=!0,a.xhr.abort(),a.xhr.onreadystatechange=function(){}},_onIdle:function(){var b=this._conn._data;if(this._conn.authenticated&&0===this._requests.length&&0===b.length&&!this._conn.disconnecting&&(a.info("no requests during idle cycle, sending blank request"),b.push(null)),!this._conn.paused){if(this._requests.length<2&&b.length>0){for(var c=this._buildBody(),d=0;d0){var e=this._requests[0].age();null!==this._requests[0].dead&&this._requests[0].timeDead()>Math.floor(a.SECONDARY_TIMEOUT*this.wait)&&this._throttledRequestHandler(),e>Math.floor(a.TIMEOUT*this.wait)&&(a.warn("Request "+this._requests[0].id+" timed out, over "+Math.floor(a.TIMEOUT*this.wait)+" seconds since last activity"),this._throttledRequestHandler())}}},_onRequestStateChange:function(b,c){if(a.debug("request id "+c.id+"."+c.sends+" state changed to "+c.xhr.readyState),c.abort)return c.abort=!1,void 0;var d;if(4==c.xhr.readyState){d=0;try{d=c.xhr.status}catch(e){}if("undefined"==typeof d&&(d=0),this.disconnecting&&d>=400)return this._hitError(d),void 0;var f=this._requests[0]==c,g=this._requests[1]==c;(d>0&&500>d||c.sends>5)&&(this._removeRequest(c),a.debug("request id "+c.id+" should now be removed")),200==d?((g||f&&this._requests.length>0&&this._requests[0].age()>Math.floor(a.SECONDARY_TIMEOUT*this.wait))&&this._restartRequest(0),a.debug("request id "+c.id+"."+c.sends+" got 200"),b(c),this.errors=0):(a.error("request id "+c.id+"."+c.sends+" error "+d+" happened"),(0===d||d>=400&&600>d||d>=12e3)&&(this._hitError(d),d>=400&&500>d&&(this._conn._changeConnectStatus(a.Status.DISCONNECTING,null),this._conn._doDisconnect()))),d>0&&500>d||c.sends>5||this._throttledRequestHandler()}},_processRequest:function(b){var c=this,d=this._requests[b],e=-1;try{4==d.xhr.readyState&&(e=d.xhr.status)}catch(f){a.error("caught an error in _requests["+b+"], reqStatus: "+e)}if("undefined"==typeof e&&(e=-1),d.sends>this._conn.maxRetries)return this._conn._onDisconnectTimeout(),void 0;var g=d.age(),h=!isNaN(g)&&g>Math.floor(a.TIMEOUT*this.wait),i=null!==d.dead&&d.timeDead()>Math.floor(a.SECONDARY_TIMEOUT*this.wait),j=4==d.xhr.readyState&&(1>e||e>=500);if((h||i||j)&&(i&&a.error("Request "+this._requests[b].id+" timed out (secondary), restarting"),d.abort=!0,d.xhr.abort(),d.xhr.onreadystatechange=function(){},this._requests[b]=new a.Request(d.xmlData,d.origFunc,d.rid,d.sends),d=this._requests[b]),0===d.xhr.readyState){a.debug("request id "+d.id+"."+d.sends+" posting");try{d.xhr.open("POST",this._conn.service,this._conn.options.sync?!1:!0),d.xhr.setRequestHeader("Content-Type","text/xml; charset=utf-8")}catch(k){return a.error("XHR open failed."),this._conn.connected||this._conn._changeConnectStatus(a.Status.CONNFAIL,"bad-service"),this._conn.disconnect(),void 0}var l=function(){if(d.date=new Date,c._conn.options.customHeaders){var a=c._conn.options.customHeaders;for(var b in a)a.hasOwnProperty(b)&&d.xhr.setRequestHeader(b,a[b])}d.xhr.send(d.data)};if(d.sends>1){var m=1e3*Math.min(Math.floor(a.TIMEOUT*this.wait),Math.pow(d.sends,3));setTimeout(l,m)}else l();d.sends++,this._conn.xmlOutput!==a.Connection.prototype.xmlOutput&&(d.xmlData.nodeName===this.strip&&d.xmlData.childNodes.length?this._conn.xmlOutput(d.xmlData.childNodes[0]):this._conn.xmlOutput(d.xmlData)),this._conn.rawOutput!==a.Connection.prototype.rawOutput&&this._conn.rawOutput(d.data)}else a.debug("_processRequest: "+(0===b?"first":"second")+" request has readyState of "+d.xhr.readyState)},_removeRequest:function(b){a.debug("removing request");var c;for(c=this._requests.length-1;c>=0;c--)b==this._requests[c]&&this._requests.splice(c,1);b.xhr.onreadystatechange=function(){},this._throttledRequestHandler()},_restartRequest:function(a){var b=this._requests[a];null===b.dead&&(b.dead=new Date),this._processRequest(a)},_reqToData:function(a){try{return a.getResponse()}catch(b){if("parsererror"!=b)throw b;this._conn.disconnect("strophe-parsererror")}},_sendTerminate:function(b){a.info("_sendTerminate was called");var c=this._buildBody().attrs({type:"terminate"});b&&c.cnode(b.tree());var d=new a.Request(c.tree(),this._onRequestStateChange.bind(this,this._conn._dataRecv.bind(this._conn)),c.tree().getAttribute("rid"));this._requests.push(d),this._throttledRequestHandler()},_send:function(){clearTimeout(this._conn._idleTimeout),this._throttledRequestHandler(),this._conn._idleTimeout=setTimeout(this._conn._onIdle.bind(this._conn),100)},_sendRestart:function(){this._throttledRequestHandler(),clearTimeout(this._conn._idleTimeout)},_throttledRequestHandler:function(){this._requests?a.debug("_throttledRequestHandler called with "+this._requests.length+" requests"):a.debug("_throttledRequestHandler called with undefined requests"),this._requests&&0!==this._requests.length&&(this._requests.length>0&&this._processRequest(0),this._requests.length>1&&Math.abs(this._requests[0].rid-this._requests[1].rid): "+d);var e=b.getAttribute("version");return"string"!=typeof e?c="Missing version in ":"1.0"!==e&&(c="Wrong version in : "+e),c?(this._conn._changeConnectStatus(a.Status.CONNFAIL,c),this._conn._doDisconnect(),!1):!0},_connect_cb_wrapper:function(b){if(0===b.data.indexOf("\s*)*/,"");if(""===c)return;var d=(new DOMParser).parseFromString(c,"text/xml").documentElement;this._conn.xmlInput(d),this._conn.rawInput(b.data),this._handleStreamStart(d)&&this._connect_cb(d)}else if(0===b.data.indexOf(" tag.")}}this._conn._doDisconnect()},_doDisconnect:function(){a.info("WebSockets _doDisconnect was called"),this._closeSocket()},_streamWrap:function(a){return""+a+""},_closeSocket:function(){if(this.socket)try{this.socket.close()}catch(a){}this.socket=null},_emptyQueue:function(){return!0},_onClose:function(){this._conn.connected&&!this._conn.disconnecting?(a.error("Websocket closed unexcectedly"),this._conn._doDisconnect()):a.info("Websocket closed")},_no_auth_received:function(b){a.error("Server did not send any auth methods"),this._conn._changeConnectStatus(a.Status.CONNFAIL,"Server did not send any auth methods"),b&&(b=b.bind(this._conn))(),this._conn._doDisconnect()},_onDisconnectTimeout:function(){},_abortAllRequests:function(){},_onError:function(b){a.error("Websocket error "+b),this._conn._changeConnectStatus(a.Status.CONNFAIL,"The WebSocket connection could not be established was disconnected."),this._disconnect()},_onIdle:function(){var b=this._conn._data;if(b.length>0&&!this._conn.paused){for(var c=0;c Date: Sat, 21 Feb 2015 01:52:02 +0100 Subject: [PATCH 20/22] Updated link to XMPP-over-Websocket RFC --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3710d8b3..76f393c4 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# Strophe.js +# Strophe.js [![Build Status](https://travis-ci.org/strophe/strophejs.png?branch=master)](https://travis-ci.org/strophe/strophejs) Strophe.js is a JavaScript library for speaking XMPP via BOSH ([XEP 124](http://xmpp.org/extensions/xep-0124.html) and [XEP 206](http://xmpp.org/extensions/xep-0206.html)) and WebSockets -(draft-ietf-xmpp-websocket-00). +([RFC 7395](http://tools.ietf.org/html/rfc7395)). Its primary purpose is to enable web-based, real-time XMPP applications that run in any browser. From 7d8a6e219cb1f8b3b084db747f6cb942c9543457 Mon Sep 17 00:00:00 2001 From: Gordin <9ordin@gmail.com> Date: Sat, 21 Feb 2015 02:11:01 +0100 Subject: [PATCH 21/22] fixed documentation --- src/core.js | 13 -------- src/polyfill.js | 77 ---------------------------------------------- src/polyfills.js | 6 ---- src/wrap_header.js | 14 +++++++++ strophe.js | 33 +++++++++----------- 5 files changed, 28 insertions(+), 115 deletions(-) delete mode 100644 src/polyfill.js diff --git a/src/core.js b/src/core.js index 5ba78fbc..7d627393 100644 --- a/src/core.js +++ b/src/core.js @@ -8,19 +8,6 @@ /* jshint undef: true, unused: true:, noarg: true, latedef: true */ /*global define, document, window, setTimeout, clearTimeout, console, ActiveXObject, DOMParser */ -/** File: strophe.js - * A JavaScript library for XMPP BOSH/XMPP over Websocket. - * - * This is the JavaScript version of the Strophe library. Since JavaScript - * had 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. - * - * This version of Strophe also works with WebSockets. - * For more information on XMPP-over WebSocket see this RFC draft: - * http://tools.ietf.org/html/draft-ietf-xmpp-websocket-00 - */ (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. diff --git a/src/polyfill.js b/src/polyfill.js deleted file mode 100644 index 7dcbd63c..00000000 --- a/src/polyfill.js +++ /dev/null @@ -1,77 +0,0 @@ -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(factory); - } else { - // Browser globals - factory(); - } -}(this, function () { - - /** PrivateFunction: Function.prototype.bind - * Bind a function to an instance. This is a polyfill for the ES5 bind method. - * which already exists in more modern browsers, but we provide it to support - * those that don't. - * - * Parameters: - * (Object) obj - The object that will become 'this' in the bound function. - * (Object) argN - An option argument that will be prepended to the - * arguments given for the function call - * - * Returns: - * The bound function. - */ - if (!Function.prototype.bind) { - Function.prototype.bind = function (obj /*, arg1, arg2, ... */) { - var func = this; - var _slice = Array.prototype.slice; - var _concat = Array.prototype.concat; - var _args = _slice.call(arguments, 1); - return function () { - return func.apply(obj ? obj : this, - _concat.call(_args, - _slice.call(arguments, 0))); - }; - }; - } - - /** PrivateFunction: Array.isArray - * This is a polyfill for the ES5 Array.isArray method. - */ - if (!Array.isArray) { - Array.isArray = function(arg) { - return Object.prototype.toString.call(arg) === '[object Array]'; - }; - } - - /** 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; - }; - } -})); diff --git a/src/polyfills.js b/src/polyfills.js index 0c023a56..d9e31088 100644 --- a/src/polyfills.js +++ b/src/polyfills.js @@ -7,12 +7,6 @@ /* jshint undef: true, unused: true:, noarg: true, latedef: true */ -/** File: polyfills.js - * A JavaScript library for XMPP BOSH/XMPP over Websocket. - * - * This file contains some polyfills used by strophe.js - */ - /** PrivateFunction: Function.prototype.bind * Bind a function to an instance. * diff --git a/src/wrap_header.js b/src/wrap_header.js index f9d13449..4b400469 100644 --- a/src/wrap_header.js +++ b/src/wrap_header.js @@ -1,3 +1,17 @@ +/** File: strophe.js + * A JavaScript library for XMPP BOSH/XMPP over Websocket. + * + * This is the JavaScript version of the Strophe library. Since JavaScript + * had 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. + * + * This version of Strophe also works with WebSockets. + * For more information on XMPP-over WebSocket see this RFC: + * http://tools.ietf.org/html/rfc7395 + */ + /* All of the Strophe globals are defined in this special function below so * that references to the globals become closures. This will ensure that * on page reload, these references will still be available to callbacks diff --git a/strophe.js b/strophe.js index 0248deed..cfa3a723 100644 --- a/strophe.js +++ b/strophe.js @@ -1,3 +1,17 @@ +/** File: strophe.js + * A JavaScript library for XMPP BOSH/XMPP over Websocket. + * + * This is the JavaScript version of the Strophe library. Since JavaScript + * had 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. + * + * This version of Strophe also works with WebSockets. + * For more information on XMPP-over WebSocket see this RFC: + * http://tools.ietf.org/html/rfc7395 + */ + /* All of the Strophe globals are defined in this special function below so * that references to the globals become closures. This will ensure that * on page reload, these references will still be available to callbacks @@ -521,12 +535,6 @@ return { /* jshint undef: true, unused: true:, noarg: true, latedef: true */ -/** File: polyfills.js - * A JavaScript library for XMPP BOSH/XMPP over Websocket. - * - * This file contains some polyfills used by strophe.js - */ - /** PrivateFunction: Function.prototype.bind * Bind a function to an instance. * @@ -619,19 +627,6 @@ if (!Array.prototype.indexOf) /* jshint undef: true, unused: true:, noarg: true, latedef: true */ /*global define, document, window, setTimeout, clearTimeout, console, ActiveXObject, DOMParser */ -/** File: strophe.js - * A JavaScript library for XMPP BOSH/XMPP over Websocket. - * - * This is the JavaScript version of the Strophe library. Since JavaScript - * had 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. - * - * This version of Strophe also works with WebSockets. - * For more information on XMPP-over WebSocket see this RFC draft: - * http://tools.ietf.org/html/draft-ietf-xmpp-websocket-00 - */ (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. From b05740268d9275e3e9358a8d8bb8981d7d5d6392 Mon Sep 17 00:00:00 2001 From: Gordin <9ordin@gmail.com> Date: Sat, 21 Feb 2015 02:16:40 +0100 Subject: [PATCH 22/22] fixed tests --- main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.js b/main.js index 64845433..158c61ef 100644 --- a/main.js +++ b/main.js @@ -22,7 +22,7 @@ require.config({ "strophe-md5": "src/md5", "strophe-sha1": "src/sha1", "strophe-websocket": "src/websocket", - "strophe-polyfill": "src/polyfill", + "strophe-polyfill": "src/polyfills", // Examples "basic": "examples/basic",