Mercurial > eldonilo > blog
changeset 0:f62b5c395a48
Initial commit.
author | Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> |
---|---|
date | Sat, 04 Jun 2011 05:02:47 +0200 |
parents | |
children | 82905edac9d8 |
files | atom.js blog.js configuration.js date.js forms.js index.xhtml jid.js nothing.js ns.js server.js strophe.js strophe.pubsub.js theme.css throbber.svg xml2json.js |
diffstat | 15 files changed, 5429 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/atom.js @@ -0,0 +1,109 @@ +'use strict'; + +parsers[ns.atom] = function(id, xml) { + var toDate = function(atom) { + if (!atom) + return new Date(); + + var last = atom.getChild('updated'); + if (!last) { + last = atom.getChild('published'); + if (!last) + return new Date(); + } + + // var d = new Date(last); // FIXME: don't work in obsolete browsers + var d = new Date(); + d.set8601(last); + + return d; + }; + + var toHTML = function(atom, date) { + var article = document.createElementNS(ns.xhtml, 'article'); + + article.setAttributeNS(ns.idq, 'id', id); + var d8601 = date.to8601(); + article.setAttributeNS(ns.idq, 'date', d8601); + + var aside = document.createElementNS(ns.xhtml, 'aside'); + article.appendChild(aside); + + var atomTitle = atom.getChild('title'); + if (atomTitle) { + var title = document.createElementNS(ns.xhtml, 'h2'); + title.appendChild(document.createTextNode(atomTitle)); + article.appendChild(title); + } + + var footer = document.createElementNS(ns.xhtml, 'footer'); + + var atomAuthor = atom.getChild('author', ns.atom); + if (atomAuthor) { + var atomName = atomAuthor.getChild('name'); + + var atomURI = atomAuthor.getChild('uri'); + var cite = document.createElementNS(ns.xhtml, 'cite'); + if (atomURI) { + var a = document.createElementNS(ns.xhtml, 'a'); + a.href = atomURI; + var atomJID = new JID; + atomJID.uri = atomURI; + + a.appendChild(document.createTextNode(atomName? atomName: atomJID.bare)); + cite.appendChild(a); + + var img = document.createElementNS(ns.xhtml, 'img'); + img.src = 'http://linkmauve.fr/avatar/' + atomJID.bare; + aside.appendChild(img); + } else + cite.appendChild(document.createTextNode(atomName)); + + footer.appendChild(document.createTextNode('By ')); + footer.appendChild(cite); + + var atomEmail = atomAuthor.getChild('email'); + if (atomEmail) { + footer.appendChild(document.createTextNode(' (')); + var a = document.createElementNS(ns.xhtml, 'a'); + a.href = atomEmail; + a.appendChild(document.createTextNode('email')); + footer.appendChild(a); + footer.appendChild(document.createTextNode(')')); + } + + article.appendChild(footer); + } + + footer.innerHTML += ', <time datetime="' + d8601 + '">' + date.getRelative() + '</time>'; + + var atomSummary = atom.getChild('summary'); + if (atomSummary) { + var p = document.createElementNS(ns.xhtml, 'p'); + p.appendChild(document.createTextNode(atomSummary)); + article.appendChild(p); + } + + var atomLinks = atom.getChildren('link'); + for (var i in atomLinks) { + var atomLink = atomLinks[i]; + + if (atomLink['@rel'] !== 'replies') + continue; + + if (atomLink['@title'] !== 'comments') + continue; + + var href = new JID; + href.uri = atomLink['@href']; + + article.innerHTML += '<a href="?jid=' + href.bare + ';node=' + href.query.node + ';comments=' + params.jid + '/' + params.node + '">Comments !</a>'; + } + + return article; + }; + + this.xml = xml; + this.date = toDate(xml); + this.html = toHTML(xml, this.date); +}
new file mode 100644 --- /dev/null +++ b/blog.js @@ -0,0 +1,243 @@ +//'use strict'; + +const BOSH_SERVICE = 'http://linkmauve.fr/http-bind/'; +var conn = null; +var jid = 'blog@linkmauve.fr'; // FIXME: Strophe should accept anonymous connections. +var password = 'blog'; +var service = 'psgxs.linkmauve.fr'; +var node = 'blog'; +var re = true; + +var params = (function() { + var vars = {}; + var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split(';'); + + for(var i = 0; i < hashes.length; i++) { + var s = hashes[i].indexOf('='); + var key = hashes[i].substring(0, s); + var value = hashes[i].substring(s+1); + vars[key] = value; + } + + return vars; +})(); + +if (params.jid) + service = params.jid; + +if (params.node) + node = params.node; + +var logs = document.getElementById('log'); +if (params.debug) + logs.parentNode.hidden = false; + +var received = {}; +var messages = document.getElementById('messages'); + +function log(msg, error) { + var p = document.createElementNS(ns.xhtml, 'p'); + p.appendChild(document.createTextNode(msg)); + + if (!error) + p.setAttributeNS(null, 'class', 'error'); + + logs.appendChild(p); +} + +function rawInput(data) { + log('RECV: ' + data, 1); +} + +function rawOutput(data) { + log('SENT: ' + data, 1); +} + +var html = function(name, id) { + return received[name][id].html; +} + +var date = function(name, id) { + return received[name][id].date; +} + +var updateMessage = function(name, id) { + var divs = messages.getElementsByTagNameNS(ns.xhtml, 'div'); + var container = null; + + for (var i in divs) { + var div = divs[i] + if (typeof div != 'object') + continue; + + if (div.getAttributeNS(ns.idq, 'jid') === name) { + container = div; + break; + } + } + + if (!container) { + var container = document.createElementNS(ns.xhtml, 'div'); + container.setAttributeNS(ns.idq, 'jid', name); + messages.appendChild(container); + } + + var articles = container.getElementsByTagNameNS(ns.xhtml, 'article'); + for (var i in articles) { + var article = articles[i]; + if (typeof article != 'object') + continue; + + if (article.getAttributeNS(ns.idq, 'id') === id) { + container.replaceChild(html(name, id), article); + return; + } + } + + var article = html(name, id); + + if (!container.firstChild) + container.appendChild(article); + else { + var d = date(name, id); + var toInsert; + for (var i in articles) { + var a = articles[i]; + if (typeof a != 'object') + continue; + + var ad = new Date(); + ad.set8601(a.getAttributeNS(ns.idq, 'date')); + + if (ad < d) { + toInsert = a; + break; + } + } + + if (toInsert) + container.insertBefore(article, toInsert); + else + container.appendChild(article); + } +} + +var convert = function(id, xml) { + var ns = xml['@xmlns']; + if (ns in parsers) + return new parsers[ns](id, xml); + return new parsers[''](id, xml); +}; + +var parsePubSubEvent = function(stanza) { + var e = {}; + + e.service = stanza.getAttribute('from'); + + var pubsub = stanza.getChild('event', ns.pse); + if (!pubsub) { + pubsub = stanza.getChild('pubsub', ns.ps); + if (!pubsub) + return; + } + e.ns = pubsub.getAttribute('xmlns'); + + var items = pubsub.getChild('items', e.ns); + if (!items) + return; + + e.node = items.getAttribute('node'); + items = items.getChildren('item', e.ns); + if (!items) + return; + + e.name = e.service + '/' + e.node; + + e.items = {}; + for (var i in items) { + var item = items[i]; + if (!item.getAttribute) + continue; + + var pl = item.getChild(); + if (!pl) + continue; + + var id = item.getAttribute('id'); + + e.items[id] = pl; + } + + return e; +} + +var onMessages = function(stanza) { + conn.addHandler(onMessages, null, 'message', null, null, null); + + log('message'); + stanza = xml2json(stanza); + var e = parsePubSubEvent(stanza); + + if (!received[e.name]) + received[e.name] = {}; + + for (var id in e.items) { + received[e.name][id] = convert(id, e.items[id]); + updateMessage(e.name, id); + } +} + +var onInfo = function(stanza) { + log('info'); + /*var query = stanza.getElementsByTagNameNS(ns.info, 'query')[0]; + var x = query.getElementsByTagNameNS(ns.data, 'x')[0]; + var form = forms.parse(x);*/ +} + +var onSubscribed = function(stanza) { + log('subscribed'); + var type = stanza.getAttribute('type'); + if (type !== 'result') { + messages.innerHTML = 'Error, impossible to retrieve messages.'; + conn.disconnect(); + } +} + +function onConnect(status) { + if (status == Strophe.Status.CONNECTING) { + log('Strophe is connecting.'); + } else if (status == Strophe.Status.CONNFAIL) { + log('Strophe failed to connect.'); + } else if (status == Strophe.Status.AUTHENTICATING) { + log('Strophe is authenticating.'); + } else if (status == Strophe.Status.DISCONNECTING) { + log('Strophe is disconnecting.'); + } else if (status == Strophe.Status.DISCONNECTED) { + log('Strophe is disconnected.'); + if (re) + conn.connect(jid, password, onConnect); + } else if (status == Strophe.Status.CONNECTED) { + log('Strophe is connected.'); + conn.addHandler(onMessages, null, 'message', null, null, null); + conn.send($pres().tree()); + conn.pubsub.subscribe(jid, service, node, undefined, onMessages, onSubscribed); + if (params.no === 'server') { + conn.pubsub.items(jid, service, node, onMessages); + conn.pubsub.info(jid, service, node, onInfo); + } + } else + log('Strophe is '+status+'.'); +} + +window.addEventListener('load', function () { + conn = new Strophe.Connection(BOSH_SERVICE); + conn.rawInput = rawInput; + conn.rawOutput = rawOutput; + conn.connect(jid, password, onConnect); +}, false); + +window.addEventListener('unload', function (e) { + re = false; + conn.disconnect(); + e.preventDefault(); +}, false);
new file mode 100644 --- /dev/null +++ b/configuration.js @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2011 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> + * + * This file is the source code of an XMPP avatar retriever. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +'use strict'; + +var config = exports || {}; + +// The JID and password of the account used. +config.jid = 'blog@linkmauve.fr'; +config.password = 'blog'; + +// The JID of the node displayed if no arguments are given. +config.defaultNode = 'psgxs.linkmauve.fr/blog'; + +// Root of the webservice, and Atom HTTP service. +config.webRoot = '/blog/'; +config.atomRoot = '/atom/'; + +// These are the host and the port on which the web service will +// listen. If you want IPv4 connection only, instead of both IPv4 and +// IPv6, replace '::' by '0.0.0.0'. If you want a port < 1024, you +// have to start it as root, use a proxy or redirect it using a +// firewall like iptables. +config.webHost = '::'; +config.webPort = 8033; + +// MIME types used to serve files. Normally only index.xhtml would have +// to be served by node, but if you are lazy you can serve the entire +// directory and those are there for you. +config.types = { + xhtml: 'application/xhtml+xml', + js: 'application/ecmascript', + css: 'text/css', + svg: 'image/svg+xml' +}
new file mode 100644 --- /dev/null +++ b/date.js @@ -0,0 +1,75 @@ +Date.prototype.set8601 = function (string) { + var regexp = "([0-9]{4})(-([0-9]{2})(-([0-9]{2})" + + "(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?" + + "(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?"; + var d = string.match(new RegExp(regexp)); + + var offset = 0; + var date = new Date(d[1], 0, 1); + + if (d[3]) { date.setMonth(d[3] - 1); } + if (d[5]) { date.setDate(d[5]); } + if (d[7]) { date.setHours(d[7]); } + if (d[8]) { date.setMinutes(d[8]); } + if (d[10]) { date.setSeconds(d[10]); } + if (d[12]) { date.setMilliseconds(Number("0." + d[12]) * 1000); } + if (d[14]) { + offset = (Number(d[16]) * 60) + Number(d[17]); + offset *= ((d[15] == '-') ? 1 : -1); + } + + offset -= date.getTimezoneOffset(); + var time = (Number(date) + (offset * 60 * 1000)); + this.setTime(Number(time)); + + return this; +} + +Date.prototype.to8601 = function(){ + function pad(n){return n<10 ? '0'+n : n} + return this.getUTCFullYear()+'-' + + pad(this.getUTCMonth()+1)+'-' + + pad(this.getUTCDate())+'T' + + pad(this.getUTCHours())+':' + + pad(this.getUTCMinutes())+':' + + pad(this.getUTCSeconds())+'Z'; +} + +Date.prototype.getRelative = function(){ + const s = 1000, m = s*60, h = m*60, d = h*24, y = d*365, M = y/12, + ref = Date.now(), + input = this.getTime(), + delta = ref - input, + year = Math.round((delta/y)*10)/10, + month = Math.round((delta/M)*10)/10, + day = Math.round((delta/d)*10)/10, + hour = Math.round((delta/h)*10)/10, + minute = Math.round((delta/m)*10)/10; + + if (year == 1) + return "a year ago"; + if (year > 1) + return Math.round(year)+" years ago"; + + if (month == 1) + return "a month ago"; + if (month > 1) + return Math.round(month)+" months ago"; + + if (day == 1) + return "today"; + if (day > 1) + return Math.round(day)+" days ago"; + + if (hour == 1) + return "an hour ago"; + if (hour > 1) + return Math.round(hour)+" hours ago"; + + if (minute == 1) + return "a minute ago"; + if (minute > 1) + return Math.round(minute)+" minutes ago"; + + return "just now"; +};
new file mode 100644 --- /dev/null +++ b/forms.js @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2010 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> + * + * This file is part of PSĜS, a PubSub server written in JavaScript. + * + * PSĜS is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License. + * + * PSĜS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with PSĜS. If not, see <http://www.gnu.org/licenses/>. + */ + +'use strict'; + +var xmpp = require('xmpp') + +var parseBoolean = function(b) { + if (b == 'true' || b == 'True' || b == 'TRUE' || b == '1') + return true; + return false; +} + +exports.build = function(type, desc, content, labels, title, instructions) { + var x = xmpp.stanza('x', {xmlns: 'jabber:x:data', type: type}); + + if (desc._TITLE) + x.c('title').t(desc._TITLE).up(); + else if (title) + x.c('title').t(title).up(); + + if (desc._INSTRUCTIONS) + x.c('instructions').t(desc._INSTRUCTIONS).up(); + else if (instructions) + x.c('instructions').t(instructions).up(); + + if (content == 'default') { + content = {}; + for (var i in desc) + content[i] = desc[i].value; + } + + for (var i in desc) { + if (i[0] == '_') + continue; + + var fieldAttr = {'var': i}; + + if (labels) { + if (desc[i].type) + fieldAttr.type = desc[i].type; + if (desc[i].label) + fieldAttr.label = desc[i].label; + } + x.c('field', fieldAttr); + + if (labels && + (desc[i].type == 'list-multi' || + desc[i].type == 'list-single')) { + for (var j in desc[i].options) { + var optAttr = {}; + if (desc[i].options[j].label) + optAttr.label = desc[i].options[j].label; + x.c('option', optAttr).c('value').t(j).up().up(); + } + } + + if (i == 'FORM_TYPE') + x.c('value').t(desc[i].value).up(); + else if (typeof content[i] != 'undefined') { + var md = content[i]; + if (desc[i].type == 'jid-multi' || + desc[i].type == 'list-multi' || + desc[i].type == 'text-multi') { + for (var j=0; j<md.length; j++) + x.c('value') + .t(md[j].toString()).up(); + } else + x.c('value').t(md.toString()).up(); + } + + x.up(); + } + return x; +} + +exports.parse = function(x, params) { + var form = {}; + + x.constructor.prototype.getAttribute = function(a) { + return this.attrs[a]; + } + + if (!params) { + var type = x.getAttribute('type'); + if (type) + form.type = type; + + var title = x.getChild('title'); + if (title) + form.title = title; + + var instructions = x.getChild('instructions'); + if (instructions) + form.instructions = instructions; + } + + form.fields = {}; + for (var i in x.children) { + var field = x.children[i]; + var name = field.getAttribute('var'); + if (params && name == 'FORM_TYPE') + continue; + + if (params) { + var type = field.getAttribute('type'); + if (type == 'jid-multi' || type == 'list-multi' || type == 'text-multi') { + form.fields[name] = []; + for (var j in field.children) { + var elem = field.children[j]; + if (elem.name == 'value') + form.fields[name].push(elem.getText()); + } + } else if (type == 'boolean') { + var value = field.getChild('value'); + if (value) + form.fields[name] = parseBoolean(value.getText()); + } else { + var value = field.getChild('value'); + if (value) + form.fields[name] = value.getText(); + } + } else { + form.fields[name] = {}; + + var label = field.getAttribute('label'); + if (label) + form.fields[name].label = label; + + var type = field.getAttribute('type'); + if (type) + form.fields[name].type = type; + + if (type == 'jid-multi' || type == 'list-multi' || type == 'text-multi') { + form.fields[name].options = {}; + form.fields[name].values = []; + for (var j in field.children) { + var elem = field.children[j]; + if (elem.name == 'option') { + var value = elem.getChild('value'); + if (!value) + continue; + + value = value.getText(); + if (!value) + continue; + + form.fields[name].options[value] = {}; + + label = elem.getAttribute('label') + if (label) + form.fields[name].options[value].label = label; + } else if (elem.name == 'value') + form.fields[name].values.push(elem.getText()); + } + } else if (type == 'boolean') { + var value = field.getChild('value'); + if (value) + form.fields[name].value = parseBoolean(value.getText()); + } else { + var value = field.getChild('value'); + if (value) + form.fields[name].value = value.getText(); + } + } + } + return form; +}
new file mode 100644 --- /dev/null +++ b/index.xhtml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<?xml-stylesheet type="text/css" href="theme.css" media="screen"?> +<!DOCTYPE html> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> + <head> + <title>Eldonilo blog</title> + + <script type="application/ecmascript" src="configuration.js" defer=""/> + <script type="application/ecmascript" src="xml2json.js" defer=""/> + <script type="application/ecmascript" src="strophe.js" defer=""/> + <script type="application/ecmascript" src="strophe.pubsub.js" defer=""/> + <script type="application/ecmascript" src="date.js" defer=""/> + <script type="application/ecmascript" src="ns.js" defer=""/> + <script type="application/ecmascript" src="jid.js" defer=""/> + <script type="application/ecmascript" src="atom.js" defer=""/> + <script type="application/ecmascript" src="nothing.js" defer=""/> + + <style type="text/css"> + .error { + color: red; + } + + *[hidden] { + display: none; + } + </style> + + <link rel="alternate" type="application/atom+xml" title="Atom feed" href="?type=atom"/> + </head> + + <body> + <header> + <h1>Eldonilo blog</h1> + <p>Displaying your nodes.</p> + </header> + + <nav> + <ul> + <li><a href="?no=client">Without client-side</a></li> + <li><a href="?no=server">Without server-side</a></li> + <li><a href="?">Hybrid mode</a></li> + </ul> + </nav> + + <hr/> + + <section id="messages"/> + + <section hidden=""> + <h2>logs</h2> + <div id="log"></div> + </section> + + <footer/> + + <script type="application/ecmascript" src="blog.js"/> + </body> +</html>
new file mode 100644 --- /dev/null +++ b/jid.js @@ -0,0 +1,105 @@ +var JID = function(jid) { + this.node = null; + this.domain = null; + this.resource = null; + this.action = null; + this.query = {}; + + this.toString = function() { + return this.full; + } + + this.__defineGetter__('bare', function() { + if (!this.domain) + return null; + + if (this.node) + return this.node + '@' + this.domain; + + return this.domain; + }); + + this.__defineSetter__('bare', function(jid) { + var s = jid.indexOf('/'); + if (s != -1) + jid = jid.substring(0, s); + + s = jid.indexOf('@'); + if (s == -1) { + this.node = null; + this.domain = jid; + } else { + this.node = jid.substring(0, s); + this.domain = jid.substring(s+1); + } + }); + + this.__defineGetter__('full', function() { + if (!this.domain) + return null; + + var full = this.domain; + + if (this.node) + full = this.node + '@' + full; + + if (this.resource) + full = full + '/' + this.resource; + + return full; + }); + + this.__defineSetter__('full', function(jid) { + var s = jid.indexOf('/'); + if (s == -1) + this.resource = null; + else { + this.resource = jid.substring(s+1); + jid = jid.substring(0, s); + } + + s = jid.indexOf('@'); + if (s == -1) { + this.node = null; + this.domain = jid; + } else { + this.node = jid.substring(0, s); + this.domain = jid.substring(s+1); + } + }); + + this.__defineSetter__('uri', function(uri) { + if (uri.indexOf('xmpp:') != 0) + return; + uri = uri.substring(5); + + var s = uri.indexOf('?'); + if (s == -1) { + this.full = uri; + return; + } + + this.full = uri.substring(0, s); + + uri = uri.substring(s+1).split(';'); + this.action = null; + this.query = {}; + + for (var i in uri) { + if (i == 0) { + this.action = uri[i]; + continue; + } + s = uri[i].indexOf('='); + var key = uri[i].substring(0, s); + var value = uri[i].substring(s+1); + this.query[key] = value; + } + }); + + if (jid) + this.full = jid; +}; + +if (typeof exports === 'object') + exports.JID = JID;
new file mode 100644 --- /dev/null +++ b/nothing.js @@ -0,0 +1,19 @@ +'use strict'; + +parsers[''] = function(id, xml) { + var toDate = function() { + return new Date(); + }; + + var toHTML = function(xml, date) { + var article = document.createElementNS(ns.xhtml, 'article'); + article.setAttributeNS(ns.idq, 'id', id); + article.setAttributeNS(ns.idq, 'date', date.to8601()); + article.appendChild(document.createElementNS(ns.xhtml, 'h2').appendChild(document.createTextNode('This post is in an unknown namespace.'))); + return article; + }; + + this.xml = xml; + this.date = toDate(xml); + this.html = toHTML(xml, this.date); +}
new file mode 100644 --- /dev/null +++ b/ns.js @@ -0,0 +1,16 @@ +var ns = { + xhtml: 'http://www.w3.org/1999/xhtml', + atom: 'http://www.w3.org/2005/Atom', + e: 'urn:eldonilo', + j: 'jabber:client', + info: 'http://jabber.org/protocol/disco#info', + items: 'http://jabber.org/protocol/disco#items', + data: 'jabber:x:data', + ps: 'http://jabber.org/protocol/pubsub', + pse: 'http://jabber.org/protocol/pubsub#event' +}; + +var parsers = {}; + +if (typeof exports === 'object') + exports.ns = ns;
new file mode 100755 --- /dev/null +++ b/server.js @@ -0,0 +1,382 @@ +#!/usr/bin/env node + +'use strict'; + +var config = require('./configuration'); + +var util = require('util'); +var http = require('http'); +var fs = require('fs'); +var xmpp = require('node-xmpp'); +var Element = xmpp.Element; +var JID = require('./jid').JID; +var ns = require('./ns').ns; +var forms = require('./forms'); +require('./date'); + +var received = {}; + +var cl = new xmpp.Client(config); + +(function() { + var send = cl.send; + cl.send = function(s) { + util.log('Sent: [1;32m' + s + '[0m'); + send.call(cl, s); + } +})(); + +cl.on('online', function() { + util.log('Connected.'); + cl.send(new Element('presence')); +}); + +var getUniqId = (function() { + var id = 0; + return function() { + return ++id; + } +})(); + +var getNodeInfo = function(jid) { + var iq = new Element('iq', {to: jid.bare, type: 'get', id: getUniqId()}) + .c('query', {xmlns: ns.info, node: jid.resource}) + .up(); + + cl.send(iq); +}; + +var getNodeItems = function(jid) { + var iq = new Element('iq', {to: jid.bare, type: 'get', id: getUniqId()}) + .c('pubsub', {xmlns: ns.ps}) + .c('items', {node: jid.resource}) + .up() + .up(); + + cl.send(iq); +}; + +var makeError = function(response) { + response.attrs.type = 'error'; + + response.c('error', {type: 'cancel'}) + .c('feature-not-implemented', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}); + + return cl.send(response); +}; + +var handleInfo = function(query, from) { + var x = query.getChild('x', ns.data); + if (!x) + return; + + var jid = new JID; + jid.bare = from; + jid.resource = query.attrs.node; + + var form = forms.parse(x, true).fields; + received[jid].form = form; + generatePage(jid); +}; + +var handlePubSub = function(pubsub, from) { + var items = pubsub.getChild('items'); + if (!items) + return; + + var itemsNS = items.getNS(); + if (itemsNS !== ns.ps && itemsNS !== ns.pse) + return; + + var jid = new JID; + jid.bare = from; + jid.resource = items.attrs.node; + + items = items.getChildren('item', itemsNS); + if (!items) + return; + + if (!received[jid]) + received[jid] = {data: null, form: null, wait: {}}; + + var data = received[jid].data || {}; + + for (var i in items) { + var item = items[i]; + var id = item.attrs.id; + + var payload = item.children[0]; + delete payload.parent; + data[id] = payload; + } + + received[jid].data = data; + generatePage(jid); +}; + +cl.on('stanza', function(stanza) { + util.log('Recv: [1;34m' + stanza + '[0m'); + if (stanza.is('iq', ns.j)) { + var type = stanza.attrs.type; + if (type === 'error') + return; + + var result = new Element('iq', {to: stanza.attrs.from, from: stanza.attrs.to, type: 'result'}); + if (type === 'get' || type === 'set') + return makeError(result); + + var payload = stanza.getChild('query', ns.info); + if (payload) + return handleInfo(payload, stanza.attrs.from); + + payload = stanza.getChild('pubsub', ns.ps); + if (payload) + return handlePubSub(payload, stanza.attrs.from); + + } else if (stanza.is('message')) { + var type = stanza.attrs.type; + if (type === 'error') + return; + + if (type !== 'headline') + return; + + var payload = stanza.getChild('event', ns.pse); + if (payload) + return handlePubSub(payload, stanza.attrs.from); + } +}); + +var parseAtom = function(atom, id) { + var article = new Element('article', {'e:id': id, 'e:date': '2011-06-02T10:59:39Z'}); + + var avatar = article.c('aside').c('img') + article.up(); + + try { + var title = atom.getChild('title', ns.atom).getText(); + if (title) + article.c('h2').t(title).up(); + } catch (e) { } + + var footer = article.c('footer'); + article.up(); + + var author = atom.getChild('author', ns.atom); + if (author) { + footer.t('By '); + var name = author.getChild('name', ns.atom).getText(); + + try{ + var uri = author.getChild('uri', ns.atom).getText(); + footer.c('cite').c('a', {href: uri}).t(name).up(); + avatar.attrs.src = '/avatar/' + uri.substring(5); + } catch (e) { + footer.c('cite').t(name); + } + + try { + var email = author.getChild('email', ns.atom).getText(); + footer.t(', (').c('a', {href: email}).t('email').up().t(')'); + } catch (e) { } + footer.up(); + } + + var published = atom.getChild('published', ns.atom).getText(); + if (published) { + if (author) + footer.t(', '); + footer.c('time', {datetime: published}).t((new Date).set8601(published).getRelative()).up(); + } + + try { + var summary = atom.getChild('summary', ns.atom).getText(); + if (summary) + article.c('p').t(summary).up(); + } catch (e) { } + + try { + var links = atom.getChildren('link'); + for (var i in links) { + var link = links[i]; + + if (link.attrs.rel !== 'replies') + continue; + + if (link.attrs.title !== 'comments') + continue; + + var href = new JID; + href.uri = link.attrs.href; + + article.c('a', {href: '?jid=' + href.bare + ';node=' + href.query.node/* + ';comments=' + params.jid + '/' + params.node*/}).t('Comments !'); + break; + } + } catch (e) { } + + return article; +}; + +var generatePage = function(jid) { + var r = received[jid.full]; + var s = r.wait; + var form = r.form; + var data = r.data; + + if (!form || !data) + return; + + for (var i in s) { + r = s[i]; + delete s[i]; + makePage(r.res, jid.full, form, data, r.noscript); + } +}; + +var makePage = function(res, jid, form, data, noscript) { + var body = '</div>'; + + for (var id in data) { + var item = data[id]; + var article = parseAtom(item, id); + body = article + body; + } + body = '<div e:jid="' + jid + '">' + body; + + home(res, form['pubsub#title'], form['pubsub#description'], body, 'Node created the <time>' + form['pubsub#creation_date'] + '</time> by <cite>' + form['pubsub#creator'] + '</cite> with <a href="http://linkmauve.fr/dev/eldonilo/blog">Eldonilo blog</a>.', noscript); +}; + +var servePage = function(url, res) { + util.log(url.href); + + var query = require('querystring').parse(url.query, ';'); + + var page = new JID(config.defaultNode); + if (query.jid) + page.bare = query.jid; + if (query.node) + page.resource = query.node; + + if (query.type === 'atom' && config.atomRoot) { + res.writeHead(301, {'Location': config.atomRoot + page.resource}); + return res.end(); + } + + res.writeHead(200, {'Content-Type': 'application/xhtml+xml'}); + + var noscript = false; + if (query.no === 'server') + return fs.readFile('index.xhtml', function(err, data) { + res.end(data); + }); + else if (query.no === 'client') + noscript = true; + + var jid = page.full; + if (!received[jid]) { + getNodeInfo(page); + getNodeItems(page); + received[jid] = {data: null, form: null, wait: {}}; + received[jid].wait[getUniqId()] = {res: res, noscript: noscript}; + } else { + received[jid].wait[getUniqId()] = {res: res, noscript: noscript}; + generatePage(page); + } +} + +var home = function(res, title, desc, body, footer, noscript) { + res.writeHead(200, {'Content-Type': 'application/xhtml+xml'}); + res.write('<?xml version="1.0" encoding="utf-8"?>\n'); + res.write('<?xml-stylesheet type="text/css" href="theme.css" media="screen"?>\n'); + res.write('<!DOCTYPE html>\n'); + res.write('<html xmlns="http://www.w3.org/1999/xhtml">\n'); + res.write(' <head>\n'); + res.write(' <title>' + (title? title: 'Eldonilo blog') + '</title>\n'); + + if (!noscript) { + res.write('\n'); + res.write(' <script type="application/ecmascript" src="configuration.js" defer=""/>\n'); + res.write(' <script type="application/ecmascript" src="xml2json.js" defer=""/>\n'); + res.write(' <script type="application/ecmascript" src="strophe.js" defer=""/>\n'); + res.write(' <script type="application/ecmascript" src="strophe.pubsub.js" defer=""/>\n'); + res.write(' <script type="application/ecmascript" src="date.js" defer=""/>\n'); + res.write(' <script type="application/ecmascript" src="ns.js" defer=""/>\n'); + res.write(' <script type="application/ecmascript" src="jid.js" defer=""/>\n'); + res.write(' <script type="application/ecmascript" src="atom.js" defer=""/>\n'); + res.write(' <script type="application/ecmascript" src="nothing.js" defer=""/>\n'); + } + + if (config.atomRoot) { + res.write('\n'); + res.write(' <link rel="alternate" type="application/atom+xml" title="Atom feed" href="?type=atom"/>\n'); + } + + res.write(' </head>\n'); + res.write('\n'); + res.write(' <body>\n'); + res.write(' <header>\n'); + res.write(' <h1>' + (title? title: 'Eldonilo blog') + '</h1>\n'); + res.write(' <p>' + (desc? desc: 'Displaying your nodes.') + '</p>\n'); + res.write(' </header>\n'); + res.write('\n'); + res.write(' <nav>\n'); + res.write(' <ul>\n'); + res.write(' <li><a href="?no=client">Without client-side</a></li>\n'); + res.write(' <li><a href="?no=server">Without server-side</a></li>\n'); + res.write(' <li><a href="?">Hybrid mode</a></li>\n'); + res.write(' </ul>\n'); + res.write(' </nav>\n'); + res.write('\n'); + res.write(' <hr/>\n'); + res.write('\n'); + + if (!body) + res.write(' <section id="messages"/>\n'); + else { + res.write(' <section id="messages" xmlns:e="' + ns.e + '">\n'); + res.write(body); + res.write(' </section>\n'); + } + + res.write('\n'); + res.write(' <section hidden="">\n'); + res.write(' <h2>logs</h2>\n'); + res.write(' <div id="log"></div>\n'); + res.write(' </section>\n'); + res.write('\n'); + + if (!footer) + res.write(' <footer/>\n'); + else { + res.write(' <footer>\n'); + res.write(footer); + res.write(' </footer>\n'); + } + + if (!noscript) { + res.write('\n'); + res.write(' <script type="application/ecmascript" src="blog.js"/>\n'); + } + + res.write(' </body>\n'); + res.end('</html>\n'); +}; + +http.createServer(function(req, res) { + var re = new RegExp('^' + config.webRoot); + req.url = req.url.replace(re, ''); + var ext = req.url.substring(req.url.lastIndexOf('.')+1); + var url = require('url').parse(req.url); + + if (url.pathname === '') + return servePage(url, res); + + fs.readFile(req.url, function(err, data) { + if (err) + return servePage(url, res); + + res.writeHead(200, {'Content-Type': config.types[ext] || 'application/octet-stream'}); + + res.end(data); + }); +}).listen(config.webPort, config.webHost);
new file mode 100644 --- /dev/null +++ b/strophe.js @@ -0,0 +1,3570 @@ +// This code was written by Tyler Akins and has been placed in the +// public domain. It would be nice if you left this header intact. +// Base64 code from Tyler Akins -- http://rumkin.com + +var Base64 = (function () { + var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + + var obj = { + /** + * Encodes a string in base64 + * @param {String} input The string to encode in base64. + */ + encode: function (input) { + var output = ""; + var chr1, chr2, chr3; + var enc1, enc2, enc3, enc4; + var i = 0; + + do { + chr1 = input.charCodeAt(i++); + chr2 = input.charCodeAt(i++); + chr3 = input.charCodeAt(i++); + + enc1 = chr1 >> 2; + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); + enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); + enc4 = chr3 & 63; + + if (isNaN(chr2)) { + enc3 = enc4 = 64; + } else if (isNaN(chr3)) { + enc4 = 64; + } + + output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + + keyStr.charAt(enc3) + keyStr.charAt(enc4); + } while (i < input.length); + + return output; + }, + + /** + * Decodes a base64 string. + * @param {String} input The string to decode. + */ + decode: function (input) { + var output = ""; + var chr1, chr2, chr3; + var enc1, enc2, enc3, enc4; + var i = 0; + + // remove all characters that are not A-Z, a-z, 0-9, +, /, or = + input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); + + do { + enc1 = keyStr.indexOf(input.charAt(i++)); + enc2 = keyStr.indexOf(input.charAt(i++)); + enc3 = keyStr.indexOf(input.charAt(i++)); + enc4 = keyStr.indexOf(input.charAt(i++)); + + chr1 = (enc1 << 2) | (enc2 >> 4); + chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); + chr3 = ((enc3 & 3) << 6) | enc4; + + output = output + String.fromCharCode(chr1); + + if (enc3 != 64) { + output = output + String.fromCharCode(chr2); + } + if (enc4 != 64) { + output = output + String.fromCharCode(chr3); + } + } while (i < input.length); + + return output; + } + }; + + return obj; +})(); +/* + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + +var MD5 = (function () { + /* + * Configurable variables. You may need to tweak these to be compatible with + * the server-side, but the defaults work in most cases. + */ + var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ + var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ + var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ + + /* + * 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); + return (msw << 16) | (lsw & 0xFFFF); + }; + + /* + * 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 + * If chrsz is ASCII, characters >255 have their hi-byte silently ignored. + */ + var str2binl = function (str) { + var bin = []; + var mask = (1 << chrsz) - 1; + for(var i = 0; i < str.length * chrsz; i += chrsz) + { + bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32); + } + return bin; + }; + + /* + * Convert an array of little-endian words to a string + */ + var binl2str = function (bin) { + var str = ""; + var mask = (1 << chrsz) - 1; + for(var i = 0; i < bin.length * 32; i += chrsz) + { + str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask); + } + return str; + }; + + /* + * Convert an array of little-endian words to a hex string. + */ + var binl2hex = function (binarray) { + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i++) + { + str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) + + hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF); + } + return str; + }; + + /* + * Convert an array of little-endian words to a base-64 string + */ + var binl2b64 = function (binarray) { + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var str = ""; + var triplet, j; + for(var i = 0; i < binarray.length * 4; i += 3) + { + triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16) | + (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) | + ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF); + for(j = 0; j < 4; j++) + { + if(i * 8 + j * 6 > binarray.length * 32) { str += b64pad; } + else { str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); } + } + } + return str; + }; + + /* + * 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); + }; + + var md5_ff = function (a, b, c, d, x, s, t) { + return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); + }; + + var md5_gg = function (a, b, c, d, x, s, t) { + return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); + }; + + var md5_hh = function (a, b, c, d, x, s, t) { + return md5_cmn(b ^ c ^ d, a, b, x, s, t); + }; + + var md5_ii = function (a, b, c, d, x, s, t) { + return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); + }; + + /* + * 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); + x[(((len + 64) >>> 9) << 4) + 14] = len; + + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + + var olda, oldb, oldc, oldd; + for (var i = 0; i < x.length; i += 16) + { + olda = a; + oldb = b; + oldc = c; + oldd = d; + + a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); + d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); + c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); + b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); + a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); + d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); + c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); + b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); + a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); + d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); + c = md5_ff(c, d, a, b, x[i+10], 17, -42063); + b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); + a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); + d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); + c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); + b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); + + a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); + d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); + c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); + b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); + a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); + d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); + c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); + b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); + a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); + d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); + c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); + b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); + a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); + d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); + c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); + b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); + + a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); + d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); + c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); + b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); + a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); + d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); + c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); + b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); + a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); + d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); + c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); + b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); + a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); + d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); + c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); + b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); + + a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); + d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); + c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); + b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); + a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); + d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); + c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); + b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); + a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); + d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); + c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); + b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); + a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); + d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); + c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); + b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + } + return [a, b, c, d]; + }; + + + /* + * Calculate the HMAC-MD5, of a key and some data + */ + var core_hmac_md5 = function (key, data) { + var bkey = str2binl(key); + if(bkey.length > 16) { bkey = core_md5(bkey, key.length * chrsz); } + + 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_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz); + return core_md5(opad.concat(hash), 512 + 128); + }; + + 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. + */ + hexdigest: function (s) { + return binl2hex(core_md5(str2binl(s), s.length * chrsz)); + }, + + b64digest: function (s) { + return binl2b64(core_md5(str2binl(s), s.length * chrsz)); + }, + + hash: function (s) { + return binl2str(core_md5(str2binl(s), s.length * chrsz)); + }, + + hmac_hexdigest: function (key, data) { + return binl2hex(core_hmac_md5(key, data)); + }, + + hmac_b64digest: function (key, data) { + return binl2b64(core_hmac_md5(key, data)); + }, + + hmac_hash: function (key, data) { + return binl2str(core_hmac_md5(key, data)); + }, + + /* + * Perform a simple self-test to see if the VM is working + */ + test: function () { + return MD5.hexdigest("abc") === "900150983cd24fb0d6963f7d28e17f72"; + } + }; + + return obj; +})(); +/* + This program is distributed under the terms of the MIT license. + Please see the LICENSE file for details. + + Copyright 2006-2008, OGG, LLC +*/ + +/* jslint configuration: */ +/*global document, window, setTimeout, clearTimeout, console, + XMLHttpRequest, ActiveXObject, + Base64, MD5, + Strophe, $build, $msg, $iq, $pres */ + +/** File: strophe.js + * A JavaScript library for XMPP BOSH. + * + * This is the JavaScript version of the Strophe library. Since JavaScript + * has no facilities for persistent TCP connections, this library uses + * Bidirectional-streams Over Synchronous HTTP (BOSH) to emulate + * a persistent, stateful, two-way connection to an XMPP server. More + * information on BOSH can be found in XEP 124. + */ + +/** PrivateFunction: Function.prototype.bind + * Bind a function to an instance. + * + * This Function object extension method creates a bound method similar + * to those in Python. This means that the 'this' object will point + * to the instance you want. See + * <a href='https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind'>MDC's bind() documentation</a> and + * <a href='http://benjamin.smedbergs.us/blog/2007-01-03/bound-functions-and-function-imports-in-javascript/'>Bound Functions and Function Imports in JavaScript</a> + * for a complete explanation. + * + * This extension already exists in some browsers (namely, Firefox 3), but + * we provide it to support those that don't. + * + * Parameters: + * (Object) obj - The object that will become 'this' in the bound function. + * (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.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 <message/> element as the root. + * + * Parmaeters: + * (Object) attrs - The <message/> 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 <iq/> element as the root. + * + * Parameters: + * (Object) attrs - The <iq/> 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 <presence/> element as the root. + * + * Parameters: + * (Object) attrs - The <presence/> 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: "", + + /** Constants: XMPP Namespace Constants + * Common namespace constants from the XMPP RFCs and XEPs. + * + * NS.HTTPBIND - HTTP BIND namespace from XEP 124. + * NS.BOSH - BOSH namespace from XEP 206. + * NS.CLIENT - Main XMPP client namespace. + * NS.AUTH - Legacy authentication namespace. + * NS.ROSTER - Roster operations namespace. + * NS.PROFILE - Profile namespace. + * NS.DISCO_INFO - Service discovery info namespace from XEP 30. + * NS.DISCO_ITEMS - Service discovery items namespace from XEP 30. + * NS.MUC - Multi-User Chat namespace from XEP 45. + * NS.SASL - XMPP SASL namespace from RFC 3920. + * NS.STREAM - XMPP Streams namespace from RFC 3920. + * NS.BIND - XMPP Binding namespace from RFC 3920. + * NS.SESSION - XMPP Session namespace from RFC 3920. + */ + NS: { + HTTPBIND: "http://jabber.org/protocol/httpbind", + BOSH: "urn:xmpp:xbosh", + CLIENT: "jabber:client", + AUTH: "jabber:iq:auth", + ROSTER: "jabber:iq:roster", + PROFILE: "jabber:iq:profile", + DISCO_INFO: "http://jabber.org/protocol/disco#info", + DISCO_ITEMS: "http://jabber.org/protocol/disco#items", + MUC: "http://jabber.org/protocol/muc", + SASL: "urn:ietf:params:xml:ns:xmpp-sasl", + STREAM: "http://etherx.jabber.org/streams", + BIND: "urn:ietf:params:xml:ns:xmpp-bind", + SESSION: "urn:ietf:params:xml:ns:xmpp-session", + VERSION: "jabber:iq:version", + STANZAS: "urn:ietf:params:xml:ns:xmpp-stanzas" + }, + + /** 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; + }, + + /** 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: { + NORMAL: 1, + TEXT: 3 + }, + + /** PrivateConstants: Timeout Values + * Timeout values for error states. These values are in seconds. + * These should not be changed unless you know exactly what you are + * doing. + * + * TIMEOUT - 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: forEachChild + * Map a function over some or all child elements of a given element. + * + * This is a small convenience function for mapping a function over + * some or all of the children of an element. If elemName is null, all + * children will be passed to the function, otherwise only children + * whose tag names match elemName will be passed. + * + * Parameters: + * (XMLElement) elem - The element to operate on. + * (String) elemName - The child element tag name filter. + * (Function) func - The function to apply to each child. This + * function should take a single argument, a DOM element. + */ + forEachChild: function (elem, elemName, func) + { + var i, childNode; + + for (i = 0; i < elem.childNodes.length; i++) { + childNode = elem.childNodes[i]; + if (childNode.nodeType == Strophe.ElementType.NORMAL && + (!elemName || this.isTagEqual(childNode, elemName))) { + func(childNode); + } + } + }, + + /** Function: isTagEqual + * Compare an element's tag name with a string. + * + * This function is case insensitive. + * + * Parameters: + * (XMLElement) el - A DOM element. + * (String) name - The element name. + * + * Returns: + * true if the element's tag name matches _el_, and false + * otherwise. + */ + isTagEqual: function (el, name) + { + return el.tagName.toLowerCase() == name.toLowerCase(); + }, + + /** 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; + + if (window.ActiveXObject) { + doc = this._getIEXmlDom(); + doc.appendChild(doc.createElement('strophe')); + } else { + doc = document.implementation + .createDocument('jabber:client', 'strophe', null); + } + + 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; + } + } 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]); + } + } + } + } + + 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, ">"); + 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) + { + //ensure text is escaped + text = Strophe.xmlescape(text); + + return Strophe.xmlGenerator().createTextNode(text); + }, + + /** Function: getText + * Get the concatenation of all text children of an element. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * A String with the concatenated text of all text element children. + */ + getText: function (elem) + { + if (!elem) { return null; } + + var str = ""; + if (elem.childNodes.length === 0 && elem.nodeType == + Strophe.ElementType.TEXT) { + str += elem.nodeValue; + } + + for (var i = 0; i < elem.childNodes.length; i++) { + if (elem.childNodes[i].nodeType == Strophe.ElementType.TEXT) { + str += elem.childNodes[i].nodeValue; + } + } + + return str; + }, + + /** Function: copyElement + * Copy an XML DOM element. + * + * This function copies a DOM element and all its descendants and returns + * the new copy. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * A new, copied DOM element tree. + */ + copyElement: function (elem) + { + var i, el; + if (elem.nodeType == Strophe.ElementType.NORMAL) { + el = Strophe.xmlElement(elem.tagName); + + for (i = 0; i < elem.attributes.length; i++) { + el.setAttribute(elem.attributes[i].nodeName.toLowerCase(), + elem.attributes[i].value); + } + + for (i = 0; i < elem.childNodes.length; i++) { + el.appendChild(Strophe.copyElement(elem.childNodes[i])); + } + } else if (elem.nodeType == Strophe.ElementType.TEXT) { + el = Strophe.xmlTextNode(elem.nodeValue); + } + + return el; + }, + + /** Function: 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, "\\3c") + .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. + */ + log: function (level, msg) + { + return; + }, + + /** Function: debug + * Log a message at the Strophe.LogLevel.DEBUG level. + * + * Parameters: + * (String) msg - The log message. + */ + debug: function(msg) + { + this.log(this.LogLevel.DEBUG, msg); + }, + + /** Function: info + * Log a message at the Strophe.LogLevel.INFO level. + * + * Parameters: + * (String) msg - The log message. + */ + info: function (msg) + { + this.log(this.LogLevel.INFO, msg); + }, + + /** Function: warn + * Log a message at the Strophe.LogLevel.WARN level. + * + * Parameters: + * (String) msg - The log message. + */ + warn: function (msg) + { + this.log(this.LogLevel.WARN, msg); + }, + + /** Function: error + * Log a message at the Strophe.LogLevel.ERROR level. + * + * Parameters: + * (String) msg - The log message. + */ + error: function (msg) + { + this.log(this.LogLevel.ERROR, msg); + }, + + /** Function: fatal + * Log a message at the Strophe.LogLevel.FATAL level. + * + * Parameters: + * (String) msg - The log message. + */ + fatal: function (msg) + { + this.log(this.LogLevel.FATAL, msg); + }, + + /** Function: serialize + * Render a DOM element and all descendants to a String. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * The serialized element tree as a String. + */ + serialize: function (elem) + { + var result; + + if (!elem) { return null; } + + 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.toLowerCase() + + "='" + elem.attributes[i].value + .replace(/&/g, "&") + .replace(/\'/g, "'") + .replace(/</g, "<") + "'"; + } + } + + if (elem.childNodes.length > 0) { + result += ">"; + for (i = 0; i < elem.childNodes.length; i++) { + child = elem.childNodes[i]; + if (child.nodeType == Strophe.ElementType.NORMAL) { + // normal element, so recurse + result += Strophe.serialize(child); + } else if (child.nodeType == Strophe.ElementType.TEXT) { + // text element + result += child.nodeValue; + } + } + result += "</" + nodeName + ">"; + } 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. + * + * Paramaters: + * (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 + * > <iq to='you' from='me' type='get' id='1'> + * > <query xmlns='strophe:example'> + * > <example/> + * > </query> + * > </iq> + * 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. If you + * need to add another child, it is necessary to use up() to go back + * to the parent in the tree. + * + * Parameters: + * (String) name - The name of the child. + * (Object) attrs - The attributes of the child in object notation. + * + * Returns: + * The Strophe.Builder object. + */ + c: function (name, attrs) + { + var child = Strophe.xmlElement(name, attrs); + this.node.appendChild(child); + this.node = child; + return this; + }, + + /** Function: cnode + * Add a child to the current element and make it the new current + * element. + * + * This function is the same as c() except that instead of using a + * name and an attributes object to create the child it uses an + * existing DOM element object. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * The Strophe.Builder object. + */ + cnode: function (elem) + { + var xmlGen = Strophe.xmlGenerator(); + var newElem = xmlGen.importNode ? 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; + } +}; + + +/** 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; + } + + if (nsMatch && + (!this.name || Strophe.isTagEqual(elem, this.name)) && + (!this.type || elem.getAttribute("type") == this.type) && + (!this.id || elem.getAttribute("id") == this.id) && + (!this.from || from == this.from)) { + return true; + } + + return false; + }, + + /** PrivateFunction: run + * Run the callback on a matching stanza. + * + * Parameters: + * (XMLElement) elem - The DOM element that triggered the + * Strophe.Handler. + * + * Returns: + * A boolean indicating if the handler should remain active. + */ + run: function (elem) + { + var result = null; + try { + result = this.handler(elem); + } catch (e) { + if (e.sourceURL) { + Strophe.fatal("error: " + this.handler + + " " + e.sourceURL + ":" + + e.line + " - " + e.name + ": " + e.message); + } else if (e.fileName) { + if (typeof(console) != "undefined") { + console.trace(); + console.error(this.handler, " - error - ", e, e.message); + } + Strophe.fatal("error: " + this.handler + " " + + e.fileName + ":" + e.lineNumber + " - " + + e.name + ": " + e.message); + } else { + Strophe.fatal("error: " + this.handler); + } + + throw e; + } + + return result; + }, + + /** PrivateFunction: toString + * Get a String representation of the Strophe.Handler object. + * + * Returns: + * A String. + */ + toString: function () + { + return "{Handler: " + this.handler + "(" + this.name + "," + + this.id + "," + this.ns + ")}"; + } +}; + +/** PrivateClass: Strophe.TimedHandler + * _Private_ helper class for managing timed handlers. + * + * A Strophe.TimedHandler encapsulates a user provided callback that + * should be called after a certain period of time or at regular + * intervals. The return value of the callback determines whether the + * Strophe.TimedHandler will continue to fire. + * + * Users will not use Strophe.TimedHandler objects directly, but instead + * they will use Strophe.Connection.addTimedHandler() and + * Strophe.Connection.deleteTimedHandler(). + */ + +/** PrivateConstructor: Strophe.TimedHandler + * Create and initialize a new Strophe.TimedHandler object. + * + * Parameters: + * (Integer) period - The number of milliseconds to wait before the + * handler is called. + * (Function) handler - The callback to run when the handler fires. This + * function should take no arguments. + * + * Returns: + * A new Strophe.TimedHandler object. + */ +Strophe.TimedHandler = function (period, handler) +{ + this.period = period; + this.handler = handler; + + this.lastCalled = new Date().getTime(); + this.user = true; +}; + +Strophe.TimedHandler.prototype = { + /** PrivateFunction: run + * Run the callback for the Strophe.TimedHandler. + * + * Returns: + * true if the Strophe.TimedHandler should be called again, and false + * otherwise. + */ + run: function () + { + this.lastCalled = new Date().getTime(); + return this.handler(); + }, + + /** PrivateFunction: reset + * Reset the last called time for the Strophe.TimedHandler. + */ + reset: function () + { + this.lastCalled = new Date().getTime(); + }, + + /** PrivateFunction: toString + * Get a string representation of the Strophe.TimedHandler object. + * + * Returns: + * The string representation. + */ + toString: function () + { + return "{TimedHandler: " + this.handler + "(" + this.period +")}"; + } +}; + +/** PrivateClass: Strophe.Request + * _Private_ helper class that provides a cross implementation abstraction + * for a BOSH related XMLHttpRequest. + * + * The Strophe.Request class is used internally to encapsulate BOSH request + * information. It is not meant to be used from user's code. + */ + +/** PrivateConstructor: Strophe.Request + * Create and initialize a new Strophe.Request object. + * + * Parameters: + * (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(); +}; + +Strophe.Request.prototype = { + /** PrivateFunction: getResponse + * Get a response from the underlying XMLHttpRequest. + * + * This function attempts to get a response from the request and checks + * for errors. + * + * Throws: + * "parsererror" - A parser error occured. + * + * Returns: + * The DOM element tree of the response. + */ + getResponse: function () + { + var node = null; + if (this.xhr.responseXML && this.xhr.responseXML.documentElement) { + node = this.xhr.responseXML.documentElement; + if (node.tagName == "parsererror") { + Strophe.error("invalid response received"); + Strophe.error("responseText: " + this.xhr.responseText); + Strophe.error("responseXML: " + + Strophe.serialize(this.xhr.responseXML)); + throw "parsererror"; + } + } else if (this.xhr.responseText) { + Strophe.error("invalid response received"); + Strophe.error("responseText: " + this.xhr.responseText); + Strophe.error("responseXML: " + + Strophe.serialize(this.xhr.responseXML)); + } + + return node; + }, + + /** PrivateFunction: _newXHR + * _Private_ helper function to create XMLHttpRequests. + * + * This function creates XMLHttpRequests across all implementations. + * + * Returns: + * A new XMLHttpRequest. + */ + _newXHR: function () + { + var xhr = null; + if (window.XMLHttpRequest) { + xhr = new XMLHttpRequest(); + if (xhr.overrideMimeType) { + xhr.overrideMimeType("text/xml"); + } + } else if (window.ActiveXObject) { + xhr = new ActiveXObject("Microsoft.XMLHTTP"); + } + + // use Function.bind() to prepend ourselves as an argument + xhr.onreadystatechange = this.func.bind(null, this); + + return xhr; + } +}; + +/** Class: Strophe.Connection + * XMPP Connection manager. + * + * Thie class is the main part of Strophe. It manages a BOSH connection + * to an XMPP server and dispatches events to the user callbacks as + * data arrives. It supports SASL PLAIN, SASL DIGEST-MD5, and legacy + * authentication. + * + * After creating a Strophe.Connection object, the user will typically + * call connect() with a user supplied callback to handle connection level + * events like authentication failure, disconnection, or connection + * complete. + * + * The user will also have several event handlers defined by using + * addHandler() and addTimedHandler(). These will allow the user code to + * respond to interesting stanzas or do something periodically with the + * connection. These handlers will be active once authentication is + * finished. + * + * To send data to the connection, use send(). + */ + +/** Constructor: Strophe.Connection + * Create and initialize a Strophe.Connection object. + * + * Parameters: + * (String) service - The BOSH service URL. + * + * Returns: + * A new Strophe.Connection object. + */ +Strophe.Connection = function (service) +{ + /* The path to the httpbind service. */ + this.service = service; + /* The connected JID. */ + this.jid = ""; + /* request id for body tags */ + this.rid = Math.floor(Math.random() * 4294967295); + /* The current session ID. */ + this.sid = null; + this.streamId = null; + /* stream:features */ + this.features = null; + + // SASL + this.do_session = false; + this.do_bind = false; + + // handler lists + this.timedHandlers = []; + this.handlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + + this._idleTimeout = null; + this._disconnectTimeout = null; + + this.authenticated = false; + this.disconnecting = false; + this.connected = false; + + this.errors = 0; + + this.paused = false; + + // default BOSH values + this.hold = 1; + this.wait = 60; + this.window = 5; + + this._data = []; + this._requests = []; + this._uniqueId = Math.round(Math.random() * 10000); + + this._sasl_success_handler = null; + this._sasl_failure_handler = null; + this._sasl_challenge_handler = null; + + // setup onIdle callback every 1/10th of a second + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + + // 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 () {}; + 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.rid = Math.floor(Math.random() * 4294967295); + + this.sid = null; + this.streamId = null; + + // SASL + this.do_session = false; + this.do_bind = false; + + // handler lists + this.timedHandlers = []; + this.handlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + + this.authenticated = false; + this.disconnecting = false; + this.connected = false; + + this.errors = 0; + + this._requests = []; + this._uniqueId = Math.round(Math.random()*10000); + }, + + /** Function: pause + * Pause the request manager. + * + * This will prevent Strophe from sending any more requests to the + * server. This is very useful for temporarily pausing while a lot + * of send() calls are happening quickly. This causes Strophe to + * send the data in a single request, saving many request trips. + */ + pause: function () + { + this.paused = true; + }, + + /** Function: resume + * Resume the request manager. + * + * This resumes after pause() has been called. + */ + resume: function () + { + this.paused = false; + }, + + /** Function: getUniqueId + * Generate a unique ID for use in <iq/> elements. + * + * All <iq/> stanzas are required to have unique id attributes. This + * function makes creating these easy. Each connection instance has + * a counter which starts from zero, and the value of this counter + * plus a colon followed by the suffix becomes the unique id. If no + * suffix is supplied, the counter is used as the unique id. + * + * Suffixes are used to make debugging easier when reading the stream + * data, and their use is recommended. The counter resets to 0 for + * every new connection for the same reason. For connections to the + * same server that authenticate the same way, all the ids should be + * the same, which makes it easy to see changes. This is useful for + * automated testing as well. + * + * Parameters: + * (String) suffix - A optional suffix to append to the id. + * + * Returns: + * A unique string to be used for the id attribute. + */ + getUniqueId: function (suffix) + { + if (typeof(suffix) == "string" || typeof(suffix) == "number") { + return ++this._uniqueId + ":" + suffix; + } else { + return ++this._uniqueId + ""; + } + }, + + /** Function: connect + * Starts the connection process. + * + * As the connection process proceeds, the user supplied callback will + * be triggered multiple times with status updates. The callback + * should take two arguments - the status code and the error condition. + * + * The status code will be one of the values in the Strophe.Status + * constants. The error condition will be one of the conditions + * defined in RFC 3920 or the condition 'strophe-parsererror'. + * + * Please see XEP 124 for a more detailed explanation of the optional + * parameters below. + * + * Parameters: + * (String) jid - The user's JID. This may be a bare JID, + * or a full JID. If a node is not supplied, SASL ANONYMOUS + * authentication will be attempted. + * (String) pass - The user's password. + * (Function) callback - The connect callback function. + * (Integer) wait - The optional HTTPBIND wait value. This is the + * time the server will wait before returning an empty result for + * a request. The default setting of 60 seconds is recommended. + * Other settings will require tweaks to the Strophe.TIMEOUT value. + * (Integer) hold - The optional HTTPBIND hold value. This is the + * number of connections the server will hold at one time. This + * should almost always be set to 1 (the default). + */ + connect: function (jid, pass, callback, wait, hold) + { + this.jid = jid; + this.pass = pass; + this.connect_callback = callback; + this.disconnecting = false; + this.connected = false; + this.authenticated = false; + this.errors = 0; + + this.wait = wait || this.wait; + this.hold = hold || this.hold; + + // parse jid for domain and resource + this.domain = Strophe.getDomainFromJid(this.jid); + + // build the body tag + var body = this._buildBody().attrs({ + to: this.domain, + "xml:lang": "en", + wait: this.wait, + hold: this.hold, + content: "text/xml; charset=utf-8", + ver: "1.6", + "xmpp:version": "1.0", + "xmlns:xmpp": Strophe.NS.BOSH + }); + + this._changeConnectStatus(Strophe.Status.CONNECTING, null); + + this._requests.push( + new Strophe.Request(body.tree(), + this._onRequestStateChange.bind( + this, this._connect_cb.bind(this)), + body.tree().getAttribute("rid"))); + this._throttledRequestHandler(); + }, + + /** Function: attach + * Attach to an already created and authenticated BOSH session. + * + * This function is provided to allow Strophe to attach to BOSH + * sessions which have been created externally, perhaps by a Web + * application. This is often used to support auto-login type features + * without putting user credentials into the page. + * + * Parameters: + * (String) jid - The full JID that is bound by the session. + * (String) sid - The SID of the BOSH session. + * (String) rid - The current RID of the BOSH session. This RID + * will be used by the next request. + * (Function) callback The connect callback function. + * (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.jid = jid; + this.sid = sid; + this.rid = rid; + this.connect_callback = callback; + + this.domain = Strophe.getDomainFromJid(this.jid); + + this.authenticated = true; + this.connected = true; + + this.wait = wait || this.wait; + this.hold = hold || this.hold; + this.window = wind || this.window; + + this._changeConnectStatus(Strophe.Status.ATTACHED, null); + }, + + /** 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) + * > }; + * + * Parameters: + * (XMLElement) elem - The XML data received by the connection. + */ + xmlInput: function (elem) + { + return; + }, + + /** 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) + * > }; + * + * Parameters: + * (XMLElement) elem - The XMLdata sent by the connection. + */ + xmlOutput: function (elem) + { + return; + }, + + /** Function: rawInput + * User overrideable function that receives raw data coming into the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.rawInput = function (data) { + * > (user code) + * > }; + * + * Parameters: + * (String) data - The data received by the connection. + */ + rawInput: function (data) + { + return; + }, + + /** Function: rawOutput + * User overrideable function that receives raw data sent to the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.rawOutput = function (data) { + * > (user code) + * > }; + * + * Parameters: + * (String) data - The data sent by the connection. + */ + rawOutput: function (data) + { + return; + }, + + /** Function: send + * Send a stanza. + * + * This function is called to push data onto the send queue to + * go out over the wire. Whenever a request is sent to the BOSH + * server, all pending data is sent and the queue is flushed. + * + * Parameters: + * (XMLElement | + * [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._throttledRequestHandler(); + clearTimeout(this._idleTimeout); + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + }, + + /** 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 handler = this.addHandler(function (stanza) { + // remove timeout handler if there is one + if (timeoutHandler) { + that.deleteTimedHandler(timeoutHandler); + } + + 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', null, 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); + }, + + /** PrivateFunction: _sendRestart + * Send an xmpp:restart stanza. + */ + _sendRestart: function () + { + this._data.push("restart"); + + this._throttledRequestHandler(); + clearTimeout(this._idleTimeout); + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + }, + + /** Function: addTimedHandler + * Add a timed handler to the connection. + * + * This function adds a timed handler. The provided handler will + * be called every period milliseconds until it returns false, + * the connection is terminated, or the handler is removed. Handlers + * that wish to continue being invoked should return true. + * + * Because of method binding it is necessary to save the result of + * this function if you wish to remove a handler with + * deleteTimedHandler(). + * + * Note that user handlers are not active until authentication is + * successful. + * + * Parameters: + * (Integer) period - The period of the handler. + * (Function) handler - The callback function. + * + * Returns: + * A reference to the handler that can be used to remove it. + */ + addTimedHandler: function (period, handler) + { + var thand = new Strophe.TimedHandler(period, handler); + this.addTimeds.push(thand); + return thand; + }, + + /** Function: deleteTimedHandler + * Delete a timed handler for a connection. + * + * This function removes a timed handler from the connection. The + * handRef parameter is *not* the function passed to addTimedHandler(), + * but is the reference returned from addTimedHandler(). + * + * Parameters: + * (Strophe.TimedHandler) handRef - The handler reference. + */ + deleteTimedHandler: function (handRef) + { + // this must be done in the Idle loop so that we don't change + // the handlers during iteration + this.removeTimeds.push(handRef); + }, + + /** Function: addHandler + * Add a stanza handler for the connection. + * + * This function adds a stanza handler to the connection. The + * handler callback will be called for any stanza that matches + * the parameters. Note that if multiple parameters are supplied, + * they must all match for the handler to be invoked. + * + * The handler will receive the stanza that triggered it as its argument. + * The handler should return true if it is to be invoked again; + * returning false will remove the handler after it returns. + * + * As a convenience, the ns parameters applies to the top level element + * and also any of its immediate children. This is primarily to make + * matching /iq/query elements easy. + * + * The 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); + }, + + /** 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) { + // setup timeout handler + this._disconnectTimeout = this._addSysTimedHandler( + 3000, this._onDisconnectTimeout.bind(this)); + this._sendTerminate(); + } + }, + + /** 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); + } + } + } + } + + // 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); + } + } + }, + + /** PrivateFunction: _buildBody + * _Private_ helper function to generate the <body/> wrapper for BOSH. + * + * Returns: + * A Strophe.Builder with a <body/> element. + */ + _buildBody: function () + { + var bodyWrap = $build('body', { + rid: this.rid++, + xmlns: Strophe.NS.HTTPBIND + }); + + if (this.sid !== null) { + bodyWrap.attrs({sid: this.sid}); + } + + return bodyWrap; + }, + + /** PrivateFunction: _removeRequest + * _Private_ function to remove a request from the queue. + * + * Parameters: + * (Strophe.Request) req - The request to remove. + */ + _removeRequest: function (req) + { + Strophe.debug("removing request"); + + var i; + for (i = this._requests.length - 1; i >= 0; i--) { + if (req == this._requests[i]) { + this._requests.splice(i, 1); + } + } + + // 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: _processRequest + * _Private_ function to process a request in the queue. + * + * This function takes requests off the queue and sends them and + * restarts dead requests. + * + * Parameters: + * (Integer) i - The index of the request in the queue. + */ + _processRequest: function (i) + { + var req = this._requests[i]; + var reqStatus = -1; + + try { + if (req.xhr.readyState == 4) { + reqStatus = req.xhr.status; + } + } catch (e) { + Strophe.error("caught an error in _requests[" + i + + "], reqStatus: " + reqStatus); + } + + if (typeof(reqStatus) == "undefined") { + reqStatus = -1; + } + + // make sure we limit the number of retries + if (req.sends > 5) { + this._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"); + + req.date = new Date(); + try { + req.xhr.open("POST", this.service, true); + } catch (e2) { + Strophe.error("XHR open failed."); + if (!this.connected) { + this._changeConnectStatus(Strophe.Status.CONNFAIL, + "bad-service"); + } + this.disconnect(); + return; + } + + // Fires the XHR request -- may be invoked immediately + // or on a gradually expanding retry window for reconnects + var sendFunc = function () { + req.xhr.send(req.data); + }; + + // Implement progressive backoff for reconnects -- + // First retry (send == 1) should also be instantaneous + if (req.sends > 1) { + // Using a cube of the retry number creats a nicely + // expanding retry window + var backoff = Math.pow(req.sends, 3) * 1000; + setTimeout(sendFunc, backoff); + } else { + sendFunc(); + } + + req.sends++; + + this.xmlOutput(req.xmlData); + this.rawOutput(req.data); + } else { + Strophe.debug("_processRequest: " + + (i === 0 ? "first" : "second") + + " request has readyState of " + + req.xhr.readyState); + } + }, + + /** PrivateFunction: _throttledRequestHandler + * _Private_ function to throttle requests to the connection window. + * + * This function makes sure we don't send requests so fast that the + * request ids overflow the connection window in the case that one + * request died. + */ + _throttledRequestHandler: function () + { + if (!this._requests) { + Strophe.debug("_throttledRequestHandler called with " + + "undefined requests"); + } else { + Strophe.debug("_throttledRequestHandler called with " + + this._requests.length + " requests"); + } + + if (!this._requests || this._requests.length === 0) { + return; + } + + if (this._requests.length > 0) { + this._processRequest(0); + } + + if (this._requests.length > 1 && + Math.abs(this._requests[0].rid - + this._requests[1].rid) < this.window) { + this._processRequest(1); + } + }, + + /** PrivateFunction: _onRequestStateChange + * _Private_ handler for Strophe.Request state changes. + * + * This function is called when the XMLHttpRequest readyState changes. + * It contains a lot of error handling logic for the many ways that + * requests can fail, and calls the request callback when requests + * succeed. + * + * Parameters: + * (Function) func - The handler for the request. + * (Strophe.Request) req - The request that is changing readyState. + */ + _onRequestStateChange: function (func, req) + { + Strophe.debug("request id " + req.id + + "." + req.sends + " state changed to " + + req.xhr.readyState); + + if (req.abort) { + req.abort = false; + return; + } + + // request complete + var reqStatus; + if (req.xhr.readyState == 4) { + reqStatus = 0; + try { + reqStatus = req.xhr.status; + } catch (e) { + // ignore errors from undefined status attribute. works + // around a browser bug + } + + if (typeof(reqStatus) == "undefined") { + reqStatus = 0; + } + + if (this.disconnecting) { + if (reqStatus >= 400) { + this._hitError(reqStatus); + return; + } + } + + var reqIs0 = (this._requests[0] == req); + var reqIs1 = (this._requests[1] == req); + + if ((reqStatus > 0 && reqStatus < 500) || req.sends > 5) { + // remove from internal queue + this._removeRequest(req); + Strophe.debug("request id " + + req.id + + " should now be removed"); + } + + // request succeeded + if (reqStatus == 200) { + // if request 1 finished, or request 0 finished and request + // 1 is over Strophe.SECONDARY_TIMEOUT seconds old, we need to + // restart the other - both will be in the first spot, as the + // completed request has been removed from the queue already + if (reqIs1 || + (reqIs0 && this._requests.length > 0 && + this._requests[0].age() > 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._changeConnectStatus(Strophe.Status.DISCONNECTING, + null); + this._doDisconnect(); + } + } + } + + if (!((reqStatus > 0 && reqStatus < 500) || + req.sends > 5)) { + this._throttledRequestHandler(); + } + } + }, + + /** PrivateFunction: _hitError + * _Private_ function to handle the error count. + * + * Requests are resent automatically until their error count reaches + * 5. Each time an error is encountered, this function is called to + * increment the count and disconnect if the count is too high. + * + * Parameters: + * (Integer) reqStatus - The request status. + */ + _hitError: function (reqStatus) + { + this.errors++; + Strophe.warn("request errored, status: " + reqStatus + + ", number of errors: " + this.errors); + if (this.errors > 4) { + this._onDisconnectTimeout(); + } + }, + + /** PrivateFunction: _doDisconnect + * _Private_ function to disconnect. + * + * This is the last piece of the disconnection logic. This resets the + * connection and alerts the user's connection callback. + */ + _doDisconnect: function () + { + Strophe.info("_doDisconnect was called"); + this.authenticated = false; + this.disconnecting = false; + this.sid = null; + this.streamId = null; + this.rid = Math.floor(Math.random() * 4294967295); + + // tell the parent we disconnected + if (this.connected) { + this._changeConnectStatus(Strophe.Status.DISCONNECTED, null); + this.connected = false; + } + + // delete handlers + this.handlers = []; + this.timedHandlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + }, + + /** PrivateFunction: _dataRecv + * _Private_ handler to processes incoming data from the the connection. + * + * Except for _connect_cb handling the initial connection request, + * this function handles the incoming data for all requests. This + * function also fires stanza handlers that match each incoming + * stanza. + * + * Parameters: + * (Strophe.Request) req - The request that has data ready. + */ + _dataRecv: function (req) + { + try { + var elem = req.getResponse(); + } catch (e) { + if (e != "parsererror") { throw e; } + this.disconnect("strophe-parsererror"); + } + if (elem === null) { return; } + + this.xmlInput(elem); + 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); + } + } + + // add handlers scheduled for addition + while (this.addHandlers.length > 0) { + this.handlers.push(this.addHandlers.pop()); + } + + // handle graceful disconnect + if (this.disconnecting && this._requests.length === 0) { + this.deleteTimedHandler(this._disconnectTimeout); + this._disconnectTimeout = null; + this._doDisconnect(); + return; + } + + var typ = elem.getAttribute("type"); + var cond, conflict; + if (typ !== null && typ == "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"); + } + this.disconnect(); + 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]; + if (hand.isMatch(child) && + (that.authenticated || !hand.user)) { + if (hand.run(child)) { + that.handlers.push(hand); + } + } else { + that.handlers.push(hand); + } + } + }); + }, + + /** PrivateFunction: _sendTerminate + * _Private_ function to send initial disconnect sequence. + * + * This is the first step in a graceful disconnect. It sends + * the BOSH server a terminate body and includes an unavailable + * presence if authentication has completed. + */ + _sendTerminate: function () + { + Strophe.info("_sendTerminate was called"); + var body = this._buildBody().attrs({type: "terminate"}); + + if (this.authenticated) { + body.c('presence', { + xmlns: Strophe.NS.CLIENT, + type: 'unavailable' + }); + } + + this.disconnecting = true; + + var req = new Strophe.Request(body.tree(), + this._onRequestStateChange.bind( + this, this._dataRecv.bind(this)), + body.tree().getAttribute("rid")); + + this._requests.push(req); + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _connect_cb + * _Private_ handler for initial connection request. + * + * This handler is used to process the initial connection request + * response from the BOSH server. It is used to set up authentication + * handlers and start the authentication process. + * + * SASL authentication will be attempted if available, otherwise + * the code will fall back to legacy authentication. + * + * Parameters: + * (Strophe.Request) req - The current request. + */ + _connect_cb: function (req) + { + Strophe.info("_connect_cb was called"); + + this.connected = true; + var bodyWrap = req.getResponse(); + if (!bodyWrap) { return; } + + this.xmlInput(bodyWrap); + this.rawInput(Strophe.serialize(bodyWrap)); + + var typ = bodyWrap.getAttribute("type"); + var cond, conflict; + if (typ !== null && typ == "terminate") { + // an error occurred + cond = bodyWrap.getAttribute("condition"); + conflict = bodyWrap.getElementsByTagName("conflict"); + if (cond !== null) { + if (cond == "remote-stream-error" && conflict.length > 0) { + cond = "conflict"; + } + this._changeConnectStatus(Strophe.Status.CONNFAIL, cond); + } else { + this._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); + } + return; + } + + // 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"); + } + if (!this.stream_id) { + this.stream_id = bodyWrap.getAttribute("authid"); + } + 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); } + + + var do_sasl_plain = false; + var do_sasl_digest_md5 = false; + var do_sasl_anonymous = false; + + var mechanisms = bodyWrap.getElementsByTagName("mechanism"); + var i, mech, auth_str, hashed_auth_str; + if (mechanisms.length > 0) { + for (i = 0; i < mechanisms.length; i++) { + mech = Strophe.getText(mechanisms[i]); + if (mech == 'DIGEST-MD5') { + do_sasl_digest_md5 = true; + } else if (mech == 'PLAIN') { + do_sasl_plain = true; + } else if (mech == 'ANONYMOUS') { + do_sasl_anonymous = true; + } + } + } else { + // we didn't get stream:features yet, so we need wait for it + // by sending a blank poll request + var body = this._buildBody(); + this._requests.push( + new Strophe.Request(body.tree(), + this._onRequestStateChange.bind( + this, this._connect_cb.bind(this)), + body.tree().getAttribute("rid"))); + this._throttledRequestHandler(); + return; + } + + if (Strophe.getNodeFromJid(this.jid) === null && + do_sasl_anonymous) { + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "ANONYMOUS" + }).tree()); + } else if (Strophe.getNodeFromJid(this.jid) === null) { + // we don't have a node, which is required for non-anonymous + // client connections + this._changeConnectStatus(Strophe.Status.CONNFAIL, + 'x-strophe-bad-non-anon-jid'); + this.disconnect(); + } else if (do_sasl_digest_md5) { + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._sasl_challenge_handler = this._addSysHandler( + this._sasl_challenge1_cb.bind(this), null, + "challenge", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "DIGEST-MD5" + }).tree()); + } else if (do_sasl_plain) { + // Build the plain auth string (barejid null + // username null password) and base 64 encoded. + auth_str = Strophe.getBareJidFromJid(this.jid); + auth_str = auth_str + "\u0000"; + auth_str = auth_str + Strophe.getNodeFromJid(this.jid); + auth_str = auth_str + "\u0000"; + auth_str = auth_str + this.pass; + + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + hashed_auth_str = Base64.encode(auth_str); + this.send($build("auth", { + xmlns: Strophe.NS.SASL, + mechanism: "PLAIN" + }).t(hashed_auth_str).tree()); + } else { + 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()); + } + }, + + /** PrivateFunction: _sasl_challenge1_cb + * _Private_ handler for DIGEST-MD5 SASL authentication. + * + * Parameters: + * (XMLElement) elem - The challenge stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_challenge1_cb: function (elem) + { + var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/; + + var challenge = Base64.decode(Strophe.getText(elem)); + var cnonce = MD5.hexdigest(Math.random() * 1234567890); + var realm = ""; + var host = null; + var nonce = ""; + var qop = ""; + var matches; + + // remove unneeded handlers + this.deleteHandler(this._sasl_failure_handler); + + while (challenge.match(attribMatch)) { + matches = challenge.match(attribMatch); + challenge = challenge.replace(matches[0], ""); + matches[2] = matches[2].replace(/^"(.+)"$/, "$1"); + switch (matches[1]) { + case "realm": + realm = matches[2]; + break; + case "nonce": + nonce = matches[2]; + break; + case "qop": + qop = matches[2]; + break; + case "host": + host = matches[2]; + break; + } + } + + var digest_uri = "xmpp/" + this.domain; + if (host !== null) { + digest_uri = digest_uri + "/" + host; + } + + var A1 = MD5.hash(Strophe.getNodeFromJid(this.jid) + + ":" + realm + ":" + this.pass) + + ":" + nonce + ":" + cnonce; + var A2 = 'AUTHENTICATE:' + digest_uri; + + var responseText = ""; + responseText += 'username=' + + this._quote(Strophe.getNodeFromJid(this.jid)) + ','; + responseText += 'realm=' + this._quote(realm) + ','; + responseText += 'nonce=' + this._quote(nonce) + ','; + responseText += 'cnonce=' + this._quote(cnonce) + ','; + responseText += 'nc="00000001",'; + responseText += 'qop="auth",'; + responseText += 'digest-uri=' + this._quote(digest_uri) + ','; + responseText += 'response=' + this._quote( + MD5.hexdigest(MD5.hexdigest(A1) + ":" + + nonce + ":00000001:" + + cnonce + ":auth:" + + MD5.hexdigest(A2))) + ','; + responseText += 'charset="utf-8"'; + + this._sasl_challenge_handler = this._addSysHandler( + this._sasl_challenge2_cb.bind(this), null, + "challenge", null, null); + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + + this.send($build('response', { + xmlns: Strophe.NS.SASL + }).t(Base64.encode(responseText)).tree()); + + return false; + }, + + /** PrivateFunction: _quote + * _Private_ utility function to backslash escape and quote strings. + * + * Parameters: + * (String) str - The string to be quoted. + * + * Returns: + * quoted string + */ + _quote: function (str) + { + return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; + //" end string workaround for emacs + }, + + + /** PrivateFunction: _sasl_challenge2_cb + * _Private_ handler for second step of DIGEST-MD5 SASL authentication. + * + * Parameters: + * (XMLElement) elem - The challenge stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_challenge2_cb: function (elem) + { + // remove unneeded handlers + this.deleteHandler(this._sasl_success_handler); + this.deleteHandler(this._sasl_failure_handler); + + this._sasl_success_handler = this._addSysHandler( + this._sasl_success_cb.bind(this), null, + "success", null, null); + this._sasl_failure_handler = this._addSysHandler( + this._sasl_failure_cb.bind(this), null, + "failure", null, null); + this.send($build('response', {xmlns: Strophe.NS.SASL}).tree()); + return false; + }, + + /** PrivateFunction: _auth1_cb + * _Private_ handler for legacy authentication. + * + * This handler is called in response to the initial <iq type='get'/> + * for legacy authentication. It builds an authentication <iq/> and + * sends it, creating a handler (calling back to _auth2_cb()) to + * handle the result + * + * Parameters: + * (XMLElement) elem - The stanza that triggered the callback. + * + * Returns: + * false to remove the handler. + */ + _auth1_cb: function (elem) + { + // 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._addSysHandler(this._auth2_cb.bind(this), null, + null, null, "_auth_2"); + + this.send(iq.tree()); + + return false; + }, + + /** PrivateFunction: _sasl_success_cb + * _Private_ handler for succesful SASL authentication. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_success_cb: function (elem) + { + Strophe.info("SASL authentication succeeded."); + + // remove old handlers + this.deleteHandler(this._sasl_failure_handler); + this._sasl_failure_handler = null; + if (this._sasl_challenge_handler) { + this.deleteHandler(this._sasl_challenge_handler); + this._sasl_challenge_handler = null; + } + + this._addSysHandler(this._sasl_auth1_cb.bind(this), null, + "stream:features", null, null); + + // we must send an xmpp:restart now + this._sendRestart(); + + return false; + }, + + /** PrivateFunction: _sasl_auth1_cb + * _Private_ handler to start stream binding. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_auth1_cb: function (elem) + { + // save stream:features for future usage + this.features = elem; + + var i, child; + + for (i = 0; i < elem.childNodes.length; i++) { + child = elem.childNodes[i]; + if (child.nodeName == 'bind') { + this.do_bind = true; + } + + if (child.nodeName == 'session') { + this.do_session = true; + } + } + + if (!this.do_bind) { + this._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()); + } + } + + return false; + }, + + /** PrivateFunction: _sasl_bind_cb + * _Private_ handler for binding result and session start. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_bind_cb: function (elem) + { + if (elem.getAttribute("type") == "error") { + Strophe.info("SASL binding failed."); + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + } + + // TODO - need to grab errors + var bind = elem.getElementsByTagName("bind"); + var jidNode; + if (bind.length > 0) { + // Grab jid + jidNode = bind[0].getElementsByTagName("jid"); + if (jidNode.length > 0) { + this.jid = Strophe.getText(jidNode[0]); + + if (this.do_session) { + this._addSysHandler(this._sasl_session_cb.bind(this), + null, null, null, "_session_auth_2"); + + this.send($iq({type: "set", id: "_session_auth_2"}) + .c('session', {xmlns: Strophe.NS.SESSION}) + .tree()); + } else { + 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; + } + + return false; + }, + + /** PrivateFunction: _sasl_failure_cb + * _Private_ handler for SASL authentication failure. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_failure_cb: function (elem) + { + // delete unneeded handlers + if (this._sasl_success_handler) { + this.deleteHandler(this._sasl_success_handler); + this._sasl_success_handler = null; + } + if (this._sasl_challenge_handler) { + this.deleteHandler(this._sasl_challenge_handler); + this._sasl_challenge_handler = null; + } + + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + return false; + }, + + /** PrivateFunction: _auth2_cb + * _Private_ handler to finish legacy authentication. + * + * This handler is called when the result from the jabber:iq:auth + * <iq/> 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(); + } + + return false; + }, + + /** PrivateFunction: _addSysTimedHandler + * _Private_ function to add a system level timed handler. + * + * This function is used to add a Strophe.TimedHandler for the + * library code. System timed handlers are allowed to run before + * authentication is complete. + * + * Parameters: + * (Integer) period - The period of the handler. + * (Function) handler - The callback function. + */ + _addSysTimedHandler: function (period, handler) + { + var thand = new Strophe.TimedHandler(period, handler); + thand.user = false; + this.addTimeds.push(thand); + return thand; + }, + + /** PrivateFunction: _addSysHandler + * _Private_ function to add a system level stanza handler. + * + * This function is used to add a Strophe.Handler for the + * library code. System stanza handlers are allowed to run before + * authentication is complete. + * + * Parameters: + * (Function) handler - The callback function. + * (String) ns - The namespace to match. + * (String) name - The stanza name to match. + * (String) type - The stanza type attribute to match. + * (String) id - The stanza id attribute to match. + */ + _addSysHandler: function (handler, ns, name, type, id) + { + var hand = new Strophe.Handler(handler, ns, name, type, id); + hand.user = false; + this.addHandlers.push(hand); + return hand; + }, + + /** PrivateFunction: _onDisconnectTimeout + * _Private_ timeout handler for handling non-graceful disconnection. + * + * If the graceful disconnect process does not complete within the + * time allotted, this handler finishes the disconnect anyway. + * + * Returns: + * false to remove the handler. + */ + _onDisconnectTimeout: function () + { + Strophe.info("_onDisconnectTimeout was called"); + + // cancel all remaining requests and clear the queue + var req; + while (this._requests.length > 0) { + req = this._requests.pop(); + req.abort = true; + req.xhr.abort(); + // jslint complains, but this is fine. setting to empty func + // is necessary for IE6 + req.xhr.onreadystatechange = function () {}; + } + + // 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()); + } + + // 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; + + var body, time_elapsed; + + // if no requests are in progress, poll + if (this.authenticated && this._requests.length === 0 && + this._data.length === 0 && !this.disconnecting) { + Strophe.info("no requests during idle cycle, sending " + + "blank request"); + this._data.push(null); + } + + if (this._requests.length < 2 && this._data.length > 0 && + !this.paused) { + body = this._buildBody(); + for (i = 0; i < this._data.length; i++) { + if (this._data[i] !== null) { + if (this._data[i] === "restart") { + body.attrs({ + to: this.domain, + "xml:lang": "en", + "xmpp:restart": "true", + "xmlns:xmpp": Strophe.NS.BOSH + }); + } else { + body.cnode(this._data[i]).up(); + } + } + } + delete this._data; + this._data = []; + this._requests.push( + new Strophe.Request(body.tree(), + this._onRequestStateChange.bind( + this, this._dataRecv.bind(this)), + body.tree().getAttribute("rid"))); + this._processRequest(this._requests.length - 1); + } + + if (this._requests.length > 0) { + time_elapsed = this._requests[0].age(); + if (this._requests[0].dead !== null) { + if (this._requests[0].timeDead() > + Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)) { + this._throttledRequestHandler(); + } + } + + 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(); + } + } + + // reactivate the timer + clearTimeout(this._idleTimeout); + this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); + } +}; + +if (callback) { + callback(Strophe, $build, $msg, $iq, $pres); +} + +})(function () { + window.Strophe = arguments[0]; + window.$build = arguments[1]; + window.$msg = arguments[2]; + window.$iq = arguments[3]; + window.$pres = arguments[4]; +});
new file mode 100644 --- /dev/null +++ b/strophe.pubsub.js @@ -0,0 +1,284 @@ +/* + Copyright 2008, Stanziq Inc. +*/ + +Strophe.addConnectionPlugin('pubsub', { +/* + Extend connection object to have plugin name 'pubsub'. +*/ + _connection: null, + + //The plugin must have the init function. + init: function(conn) { + + this._connection = conn; + + /* + Function used to setup plugin. + */ + + /* extend name space + * NS.PUBSUB - XMPP Publish Subscribe namespace + * from XEP 60. + * + * NS.PUBSUB_SUBSCRIBE_OPTIONS - XMPP pubsub + * options namespace from XEP 60. + */ + Strophe.addNamespace('PUBSUB',"http://jabber.org/protocol/pubsub"); + Strophe.addNamespace('PUBSUB_SUBSCRIBE_OPTIONS', + Strophe.NS.PUBSUB+"#subscribe_options"); + Strophe.addNamespace('PUBSUB_ERRORS',Strophe.NS.PUBSUB+"#errors"); + Strophe.addNamespace('PUBSUB_EVENT',Strophe.NS.PUBSUB+"#event"); + Strophe.addNamespace('PUBSUB_OWNER',Strophe.NS.PUBSUB+"#owner"); + Strophe.addNamespace('PUBSUB_AUTO_CREATE', + Strophe.NS.PUBSUB+"#auto-create"); + Strophe.addNamespace('PUBSUB_PUBLISH_OPTIONS', + Strophe.NS.PUBSUB+"#publish-options"); + Strophe.addNamespace('PUBSUB_NODE_CONFIG', + Strophe.NS.PUBSUB+"#node_config"); + Strophe.addNamespace('PUBSUB_CREATE_AND_CONFIGURE', + Strophe.NS.PUBSUB+"#create-and-configure"); + Strophe.addNamespace('PUBSUB_SUBSCRIBE_AUTHORIZATION', + Strophe.NS.PUBSUB+"#subscribe_authorization"); + Strophe.addNamespace('PUBSUB_GET_PENDING', + Strophe.NS.PUBSUB+"#get-pending"); + Strophe.addNamespace('PUBSUB_MANAGE_SUBSCRIPTIONS', + Strophe.NS.PUBSUB+"#manage-subscriptions"); + Strophe.addNamespace('PUBSUB_META_DATA', + Strophe.NS.PUBSUB+"#meta-data"); + + }, + /***Function + + Create a pubsub node on the given service with the given node + name. + + Parameters: + (String) jid - The node owner's jid. + (String) service - The name of the pubsub service. + (String) node - The name of the pubsub node. + (Dictionary) options - The configuration options for the node. + (Function) call_back - Used to determine if node + creation was sucessful. + + Returns: + Iq id used to send subscription. + */ + createNode: function(jid,service,node,options, call_back) { + + var iqid = this._connection.getUniqueId("pubsubcreatenode"); + + var iq = $iq({from:jid, to:service, type:'set', id:iqid}); + + var c_options = Strophe.xmlElement("configure",[]); + var x = Strophe.xmlElement("x",[["xmlns","jabber:x:data"]]); + var form_field = Strophe.xmlElement("field",[["var","FORM_TYPE"], + ["type","hidden"]]); + var value = Strophe.xmlElement("value",[]); + var text = Strophe.xmlTextNode(Strophe.NS.PUBSUB+"#node_config"); + value.appendChild(text); + form_field.appendChild(value); + x.appendChild(form_field); + + for (var i in options) + { + var val = options[i]; + x.appendChild(val); + } + + if(options.length && options.length != 0) + { + c_options.appendChild(x); + } + + iq.c('pubsub', + {xmlns:Strophe.NS.PUBSUB}).c('create', + {node:node}).up().cnode(c_options); + + this._connection.addHandler(call_back, + null, + 'iq', + null, + iqid, + null); + this._connection.send(iq.tree()); + return iqid; + }, + /***Function + Subscribe to a node in order to receive event items. + + Parameters: + (String) jid - The node owner's jid. + (String) service - The name of the pubsub service. + (String) node - The name of the pubsub node. + (Array) options - The configuration options for the node. + (Function) event_cb - Used to recieve subscription events. + (Function) call_back - Used to determine if node + creation was sucessful. + + Returns: + Iq id used to send subscription. + */ + subscribe: function(jid,service,node,options, event_cb, call_back) { + + var subid = this._connection.getUniqueId("subscribenode"); + + var sub = $iq({from:jid, to:service, type:'set', id:subid}) + + if(options && options.length && options.length !== 0) + { + + //create subscription options + var sub_options = Strophe.xmlElement("options",[]); + var x = Strophe.xmlElement("x",[["xmlns","jabber:x:data"]]); + var form_field = Strophe.xmlElement("field",[["var","FORM_TYPE"], + ["type","hidden"]]); + var value = Strophe.xmlElement("value",[]); + var text = Strophe.xmlTextNode(Strophe.NS.PUBSUB_SUBSCRIBE_OPTIONS); + value.appendChild(text); + form_field.appendChild(value); + x.appendChild(form_field); + + for (var i = 0; i < options.length; i++) + { + var val = options[i]; + x.appendChild(val); + } + sub_options.appendChild(x); + + sub.c('pubsub', { xmlns:Strophe.NS.PUBSUB }).c('subscribe', + {node:node,jid:jid}).up().cnode(sub_options); + } + else + { + + sub.c('pubsub', { xmlns:Strophe.NS.PUBSUB }).c('subscribe', + {node:node,jid:jid}); + } + + + this._connection.addHandler(call_back, + null, + 'iq', + null, + subid, + null); + + //add the event handler to receive items + this._connection.addHandler(event_cb, + null, + 'message', + null, + null, + null); + this._connection.send(sub.tree()); + return subid; + + }, + /***Function + Unsubscribe from a node. + + Parameters: + (String) jid - The node owner's jid. + (String) service - The name of the pubsub service. + (String) node - The name of the pubsub node. + (Function) call_back - Used to determine if node + creation was sucessful. + + */ + unsubscribe: function(jid,service,node, call_back) { + + var subid = this._connection.getUniqueId("unsubscribenode"); + + + var sub = $iq({from:jid, to:service, type:'set', id:subid}) + sub.c('pubsub', { xmlns:Strophe.NS.PUBSUB }).c('unsubscribe', + {node:node,jid:jid}); + + + + this._connection.addHandler(call_back, + null, + 'iq', + null, + subid, + null); + this._connection.send(sub.tree()); + + + return subid; + + }, + /***Function + + Publish and item to the given pubsub node. + + Parameters: + (String) jid - The node owner's jid. + (String) service - The name of the pubsub service. + (String) node - The name of the pubsub node. + (Array) items - The list of items to be published. + (Function) call_back - Used to determine if node + creation was sucessful. + */ + publish: function(jid, service, node, items, call_back) { + var pubid = this._connection.getUniqueId("publishnode"); + + + var publish_elem = Strophe.xmlElement("publish", + [["node", + node], + ["jid", + jid]]); + for (var i in items) + { + var item = Strophe.xmlElement("item",[]); + var entry = Strophe.xmlElement("entry",[]); + var t = Strophe.xmlTextNode(items[i]); + entry.appendChild(t); + item.appendChild(entry); + publish_elem.appendChild(item); + } + + var pub = $iq({from:jid, to:service, type:'set', id:pubid}) + pub.c('pubsub', { xmlns:Strophe.NS.PUBSUB }).cnode(publish_elem); + + + this._connection.addHandler(call_back, + null, + 'iq', + null, + pubid, + null); + this._connection.send(pub.tree()); + + + return pubid; + }, + /*Function: items + Used to retrieve the persistent items from the pubsub node. + + */ + items: function(jid,service,node,ok_callback,error_back) { + var pub = $iq({from:jid, to:service, type:'get'}) + + //ask for all items + pub.c('pubsub', + { xmlns:Strophe.NS.PUBSUB }).c('items',{node:node}); + + return this._connection.sendIQ(pub.tree(),ok_callback,error_back); + }, + /*Function: info + Used to retrieve the persistent infos from the pubsub node. + + */ + info: function(jid,service,node,ok_callback,error_back) { + var pub = $iq({from:jid, to:service, type:'get'}) + + //ask for all items + pub.c('query', + { xmlns:'http://jabber.org/protocol/disco#info', node:node }); + + return this._connection.sendIQ(pub.tree(),ok_callback,error_back); + } +});
new file mode 100644 --- /dev/null +++ b/theme.css @@ -0,0 +1,128 @@ +body { + font-family: sans-serif; + -moz-box-shadow: 0px 0px 20px #e5e5e5; + box-shadow: 0px 0px 20px #e5e5e5; + width: 540px; + margin: 56px auto; + padding: 32px 64px 64px; + font-size: 0.76em; + line-height: 1.5em; +} + +*[hidden] { + display: none; +} + +a { + color: #999; + text-decoration: none; +} + +body > header { + float: left; + line-height: 20px; + margin: 0 auto; + min-height: 75px; + overflow: auto; + padding-top: 20px; +} + +body > header > h1 > a { + font-size: 2em; + font-weight: normal; + font-family: serif; +} + +body > header > p { + margin-top: -16px; + color: #aaa; +} + +body > nav { + line-height: 20px; + padding: 20px 10px; +} + +body > nav > ul { + font-variant: small-caps; + list-style: none outside none; + text-align: right; +} + +body > nav > ul > li { + display: inline; +} + +body > nav > ul > li > a { + padding: 3px 0 3px 10px; +} + +body > hr { + background-color: #F5F5F5; + clear: both; + height: 5px; + border: none; +} + +body > section { + margin-top: 42px; +} + +body > section:empty { + background-image: url(throbber.svg); + min-height: 400px; +} + +body > section > div > article { + clear: right; + margin: 1.5em 0; + border-bottom: 1px dotted #ccc; + min-height: 64px; +} + +body > section > div > article > h2 { + font-size: 1.5em; + font-weight: normal; + color: #999; + white-space: nowrap; + overflow: hidden; +} + +body > section > div > article > aside { + float: right; +} + +body > section > div > article > aside > img { + width: 64px; + height: 64px; +} + +body > section > div > article > footer { + display: block; + color: #999; + line-height: 1em; + padding-bottom: 10px; +} + +body > section > div > article > p { + font-size: 16px; + line-height: 140%; + padding: 10px 5px 30px; +} + +body > section > div > article > a { + background: url(throbber.svg) no-repeat scroll left center transparent; + background-size: 16px 16px; + padding-left: 20px; + color: #aaa; + font-size: 1em; + font-weight: bold; +} + +body > footer > p { + float: left; +} + +body > footer > p + p { + float: right; +}
new file mode 100644 --- /dev/null +++ b/throbber.svg @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-150 -150 300 300"> + <g fill="gray"> + + <rect x="50" y="-16" width="100" height="32" rx="32" ry="64" id="r"/> + <g opacity="0.4"> + <use xlink:href="#r" transform="rotate(30)"/> + <use xlink:href="#r" transform="rotate(60)"/> + <use xlink:href="#r" transform="rotate(90)"/> + <use xlink:href="#r" transform="rotate(120)"/> + <use xlink:href="#r" transform="rotate(150)"/> + <use xlink:href="#r" transform="rotate(180)"/> + <use xlink:href="#r" transform="rotate(210)"/> + <use xlink:href="#r" transform="rotate(240)"/> + <use xlink:href="#r" transform="rotate(270)"/> + </g> + <use xlink:href="#r" transform="rotate(300)" opacity="0.6"/> + <use xlink:href="#r" transform="rotate(330)" opacity="0.8"/> + + <animateTransform attributeName="transform" attributeType="XML" + type="rotate" begin="0s" dur="1s" repeatCount="indefinite" + calcMode="discrete" + values="0; 30; 60; 90; 120; 150; 180; 210; 240; 270; 300; 330" + keyTimes="0; 0.083; 0.167; 0.25; 0.333; 0.417; 0.5; 0.583; 0.667; 0.75; 0.833; 0.917"/> + </g> +</svg>
new file mode 100644 --- /dev/null +++ b/xml2json.js @@ -0,0 +1,180 @@ +/* This work is licensed under Creative Commons GNU LGPL License. + + License: http://creativecommons.org/licenses/LGPL/2.1/ + Version: 0.9 + Author: Stefan Goessner/2006 + Web: http://goessner.net/ +*/ +var Element = function() { + this.getChildren = function(name, ns) { + var children = []; + var r = false; + + for (var i in this) { + if (typeof this[i] !== 'object') + continue; + + var child = this[i]; + if((!name || i === name) && + (!ns || child[0]['@xmlns'] === ns)) { + for (var j=0; j<child.length; j++) + children.push(child[j]); + r = true; + } + } + + if (r) + return children; + + return null; + }; + + this.getChild = function(name, ns) { + var children = this.getChildren(name, ns); + if (!children || children.length != 1) + return null; + + return children[0]; + }; + + this.getAttribute = function(name, ns) { + return this['@'+name]; // FIXME: namespace + }; + + this.getText = function() { + return this; + } +}; + +function xml2json(xml, tab) { + var X = { + toObj: function(xml, parentNS) { + var o = new Element(); + if (xml.nodeType==1) { // element node .. + if (xml.attributes.length) // element with attributes .. + for (var i=0; i<xml.attributes.length; i++) { + o["@"+xml.attributes[i].nodeName] = (xml.attributes[i].nodeValue||"").toString(); + if (xml.attributes[i].nodeName === 'xmlns') + parentNS = xml.attributes[i].nodeValue; + } + if (!o['@xmlns']) + o['@xmlns'] = parentNS; + if (xml.firstChild) { // element has child nodes .. + var textChild=0, cdataChild=0, hasElementChild=false; + for (var n=xml.firstChild; n; n=n.nextSibling) { + if (n.nodeType==1) hasElementChild = true; + else if (n.nodeType==3 && n.nodeValue.match(/[\S]/)) textChild++; // non-whitespace text + else if (n.nodeType==4) cdataChild++; // cdata section node + } + if (hasElementChild) { + // structured element with evtl. a single text or/and cdata + // node .. + if (textChild < 2 && cdataChild < 2) { + X.removeWhite(xml); + for (var n=xml.firstChild; n; n=n.nextSibling) + if (n.nodeType == 3) // text node + o["#text"] = X.escape(n.nodeValue); + else if (n.nodeType == 4) // cdata node + o["#cdata"] = X.escape(n.nodeValue); + else { + if(!o[n.nodeName]) + o[n.nodeName] = [X.toObj(n, parentNS)]; + else + o[n.nodeName].push(X.toObj(n, parentNS)); + } + } + else { // mixed content + if (!xml.attributes.length) + o = X.escape(X.innerXml(xml)); + else + o["#text"] = X.escape(X.innerXml(xml)); + } + } + else if (textChild) { // pure text + if (!xml.attributes.length) + o = X.escape(X.innerXml(xml)).replace(/\\\\/g, '\\'); + else + o["#text"] = X.escape(X.innerXml(xml)); + } + else if (cdataChild) { // cdata + if (cdataChild > 1) + o = X.escape(X.innerXml(xml)); + else + for (var n=xml.firstChild; n; n=n.nextSibling) + o["#cdata"] = X.escape(n.nodeValue); + } + } + + return (!xml.attributes.length && !xml.firstChild) ? null : o; + } + + if (xml.nodeType==9) // document.node + return X.toObj(xml.documentElement); + + throw("unhandled node type: " + xml.nodeType); + }, + innerXml: function(node) { + var s = ""; + if ("innerHTML" in node) + s = node.innerHTML; + else { + var asXml = function(n) { + var s = ""; + if (n.nodeType == 1) { + s += "<" + n.nodeName; + for (var i=0; i<n.attributes.length;i++) + s += " " + n.attributes[i].nodeName + "=\"" + (n.attributes[i].nodeValue||"").toString() + "\""; + if (n.firstChild) { + s += ">"; + for (var c=n.firstChild; c; c=c.nextSibling) + s += asXml(c); + s += "</"+n.nodeName+">"; + } + else + s += "/>"; + } + else if (n.nodeType == 3) + s += n.nodeValue; + else if (n.nodeType == 4) + s += "<![CDATA[" + n.nodeValue + "]]>"; + return s; + }; + for (var c=node.firstChild; c; c=c.nextSibling) + s += asXml(c); + } + return s; + }, + escape: function(txt) { + return txt.replace(/[\\]/g, "\\\\") + .replace(/[\"]/g, '\\"') + .replace(/[\n]/g, '\\n') + .replace(/[\r]/g, '\\r'); + }, + removeWhite: function(e) { + e.normalize(); + for (var n = e.firstChild; n; ) { + if (n.nodeType == 3) { // text node + if (!n.nodeValue.match(/[^ \f\n\r\t\v]/)) { // pure whitespace text node + var nxt = n.nextSibling; + e.removeChild(n); + n = nxt; + } + else + n = n.nextSibling; + } + else if (n.nodeType == 1) { // element node + X.removeWhite(n); + n = n.nextSibling; + } + else // any other node + n = n.nextSibling; + } + return e; + } + }; + if (xml.nodeType == 9) // document node + xml = xml.documentElement; + + //modified + return X.toObj(X.removeWhite(xml)); +}