view psgxs.js @ 15:60c80751cfa5

JID handling conforming to the RFC.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Tue, 19 Oct 2010 17:48:45 +0200
parents 9a6b8b3357c6
children ec7cea92fe8a
line wrap: on
line source

#!/usr/bin/env node

/*
 *  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/>.
 */

var xmpp = require('xmpp');
var sha1 = require('sha1');
require('./iso8601');
var storage = require('./storage');
var errors = require('./errors');
var utils = require('./util');
var toBareJID = utils.toBareJID;
var config = require('./configuration');
var forms = require('./forms');
var conn = new xmpp.Connection();

var service_configuration = config.service_configuration;
var componentJID = config.jid;
var componentPassword = config.password;

conn.log = function (_, m) { console.log(m); };

function _(obj, color) {
	var str = require('sys').inspect(obj, false, null);
	if (color)
		console.log('\033['+c+';1m' + str + '\033[0m');
	else
		console.log(str);
};

process.addListener('uncaughtException', function (err) {
	console.log('\033[41;1mUncaught exception, this should never happen: ' + err + '.\033[0m');
});

if (typeof xmpp.StanzaBuilder.cnode != 'function' || typeof xmpp.StanzaBuilder.prototype.cnode != 'function') {
	xmpp.StanzaBuilder.prototype.cnode = function (stanza)
	{
		var parent = this;
		parent.tags.push(stanza);
		parent.children.push(stanza);
		return this;
	};
}

function onIq(stanza) {
	var type = stanza.getAttribute('type');
	var from = stanza.getAttribute('to');
	var to = stanza.getAttribute('from');
	var id = stanza.getAttribute('id');

	var response;
	if (id)
		response = xmpp.iq({to: to, from: from, type: 'result', id: id});
	else
		response = xmpp.iq({to: to, from: from, type: 'result'});

	if (type == 'get') {

		// XEP-0092: Software Version
		if (stanza.getChild('query', 'jabber:iq:version')) {
			var query = xmpp.stanza('query', {xmlns: 'jabber:iq:version'})
				.s('name').t('PSĜS')
				.s('version').t(config.version)
				.s('os').t(config.os);
			response.cnode(query);

		// SECTION 5.1
		} else if (stanza.getChild('query', 'http://jabber.org/protocol/disco#info')) {
			var query = stanza.getChild('query', 'http://jabber.org/protocol/disco#info');
			var nodeID = query.getAttribute('node');

			// SECTION 5.3
			if (nodeID && nodeID != '') {
				if (!storage.existsNode(nodeID))
					return makeError(response, errors.node_does_not_exist.n);

				var conf = storage.getConfiguration(nodeID);
				if (typeof conf == 'number')
					return makeError(response, conf);

				var type = 'leaf'
				if (conf['pubsub#node_type'])
					type = conf['pubsub#node_type'];

				var q = xmpp.stanza('query', {xmlns: 'http://jabber.org/protocol/disco#info', node: nodeID})
				q.s('identity', {category: 'pubsub', type: type});
				q.s('feature', {'var': 'http://jabber.org/protocol/pubsub'});

				// SECTION 5.4
				if (config.enabled('meta-data')) {
					var x = forms.build('result', 'node_metadata', storage.getMetadata(nodeID), true);
					if (x)
						q.cnode(x);
				}
				response.cnode(q);

			// SECTION 5.1
			} else {
				var q = xmpp.stanza('query', {xmlns: 'http://jabber.org/protocol/disco#info'})
					.s('identity', {category: 'pubsub', type: 'service', name: 'PubSub JavaScript Server'})
					.s('feature', {'var': 'http://jabber.org/protocol/disco#info'})
					.s('feature', {'var': 'http://jabber.org/protocol/disco#items'})
					.s('feature', {'var': 'http://jabber.org/protocol/pubsub'})
					.s('feature', {'var': 'jabber:iq:version'})
					.s('feature', {'var': 'http://jabber.org/protocol/commands'});

				for (var i in config.activated)
					if (typeof i == 'string')
						q.s('feature', {'var': 'http://jabber.org/protocol/pubsub#' + config.activated[i]});

				response.cnode(q);
			}

		// SECTION 5.2
		} else if (stanza.getChild('query', 'http://jabber.org/protocol/disco#items')) {
			var query = stanza.getChild('query', 'http://jabber.org/protocol/disco#items');
			var q;
			var children;
			var nodeID = query.getAttribute('node');
			if (nodeID && nodeID != '') {
				if (nodeID == 'http://jabber.org/protocol/commands') {
					// XEP-0050: Ad-Hoc Commands

					q = xmpp.stanza('query', {xmlns: 'http://jabber.org/protocol/disco#items', node: nodeID})
						.s('item', {jid: componentJID, node: 'ping', name: 'Ping'})
						.s('item', {jid: componentJID, node: 'reload', name: 'Reload'});

					response.cnode(q);
					return conn.send(response);
				} else {
					if (!storage.existsNode(nodeID))
						return makeError(response, errors.node_does_not_exist.n);

					q = xmpp.stanza('query', {xmlns: 'http://jabber.org/protocol/disco#items', node: nodeID});

					children = storage.getChildren(nodeID);
					if (typeof children == 'number')
						return makeError(response, children);
				}
			} else {
				q = xmpp.stanza('query', {xmlns: 'http://jabber.org/protocol/disco#items'});

				children = storage.getChildren();
				if (typeof children == 'number')
					return makeError(response, children);
			}

			for (var i in children) {
				var attr = {jid: componentJID};
				if (children[i] == 'node') {
					if (config.enabled('meta-data')) {
						var metadata = storage.getMetadata(i);
						if (metadata['pubsub#title'])
							attr.name = metadata['pubsub#title'];
					}
					attr.node = i;

				// SECTION 5.5
				} else
					attr.name = i;

				q.s('item', attr);
			}
			response.cnode(q);
		} else if (stanza.getChild('pubsub', 'http://jabber.org/protocol/pubsub')) {
			var pubsub = stanza.getChild('pubsub', 'http://jabber.org/protocol/pubsub');

			// SECTION 5.6
			if (pubsub.getChild('subscriptions')) {
				if (!config.enabled('retrieve-subscriptions'))
					return makeError(response, errors.subscriptions_retrieval_not_supported.n);

				var subscriptions = pubsub.getChild('subscriptions');
				var subs;

				var nodeID = subscriptions.getAttribute('node');
				if (nodeID && nodeID != '') {
					if (!storage.existsNode(nodeID))
						return makeError(response, errors.node_does_not_exist.n);
					subs = storage.getSubscription(toBareJID(to), node);
				} else
					subs = storage.getSubscription(toBareJID(to));

				var s = xmpp.stanza('subscriptions');
				for (i in subs)
					s.s('subscription', {node: i, jid: to, subscription: subs[i].type, subid: subs[i].subid});

				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'});
				p.cnode(s);
				response.cnode(p);

			// SECTION 5.7
			} else if (pubsub.getChild('affiliations')) {
				if (!config.enabled('retrieve-affiliations'))
					return makeError(response, errors.affiliations_retrieval_not_supported.n);

				var affiliations = pubsub.getChild('affiliations');
				var nodeID = affiliations.getAttribute('node');
				var affils;
				if (nodeID && nodeID != '') {
					if (!storage.existsNode(nodeID))
						return makeError(response, errors.node_does_not_exist.n);
					affils = {};
					affils[nodeID] = storage.getAffiliation(toBareJID(to), nodeID);
				} else
					affils = storage.getAffiliationsFromJID(toBareJID(to));

				var s = xmpp.stanza('affiliations');
				for (i in affils)
					s.s('affiliation', {node: i, affiliation: affils[i]});

				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'});
				p.cnode(s);
				response.cnode(p);

			// SECTION 6.3.2
			} else if (pubsub.getChild('options')) {
				if (!config.enabled('subscription-options'))
					return makeError(response, errors.sub.configure.subscription_options_not_supported.n);

				var options = pubsub.getChild('options');

				var nodeID = options.getAttribute('node');
				if (!nodeID || nodeID == '')
					return makeError(response, errors.nodeid_required.n);
				if (!storage.existsNode(nodeID))
					return makeError(response, errors.node_does_not_exist.n);

				var jid = options.getAttribute('jid');
				if (!jid)
					return makeError(response, errors.sub.configure.subscriber_jid_required.n);
				if (toBareJID(jid) != toBareJID(to))
					return makeError(response, errors.sub.configure.insufficient_privileges.n);

				var subs = storage.getSubscription(jid, nodeID);
				if (subs == {})
					return makeError(response, errors.sub.configure.no_such_subscriber.n);

				var s = xmpp.stanza('options', {node: nodeID, jid: jid});
				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'});
				var form = forms.build('form', 'subscribe_options', subs.options, true);
				s.cnode(form);
				p.cnode(s);
				response.cnode(p);

			// SECTION 6.4
			} else if (pubsub.getChild('default')) {
				if (!config.enabled('retrieve-default-sub'))
					return makeError(response, errors.sub.default_options.default_subscription_configuration_retrieval_not_supported.n);

				var def = pubsub.getChild('default');

				var nodeID = def.getAttribute('node');
				if (nodeID && !storage.existsNode(nodeID))
					return makeError(response, errors.node_does_not_exist.n);

				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'});
				var s;
				if (nodeID)
					s = xmpp.stanza('default', {node: nodeID});
				else
					s = xmpp.stanza('default');

				var form = forms.build('form', 'subscribe_options', 'default', false);
				s.cnode(form);
				p.cnode(s);
				response.cnode(p);

			// SECTION 6.5
			} else if (pubsub.getChild('items')) {
				if (!config.enabled('retrieve-items'))
					return makeError(response, errors.sub.default_options.node_configuration_not_supported.n);

				var items = pubsub.getChild('items');

				var nodeID = items.getAttribute('node');
				if (!nodeID || nodeID == '')
					return makeError(response, errors.nodeid_required.n);
				if (!storage.existsNode(nodeID))
					return makeError(response, errors.node_does_not_exist.n);

				var configuration = storage.getConfiguration(nodeID);
				if (configuration['pubsub#access_model'] == 'whitelist') {
					var affil = storage.getAffiliation(toBareJID(to), nodeID);
					if (affil != 'super-owner' && affil != 'owner' && affil != 'publisher' && affil != 'member')
						return makeError(response, errors.pub.publish.insufficient_privileges.n);
				}

				var item = [];
				for (var i=0; i<items.children.length; i++) {
					var j = items.children[i];
					if (j.name == 'item' && j.attr['id'] && j.attr['id'] != '')
						item.push(j.attr['id']);
				}

				var max_items = items.getAttribute('max_items');
				if (max_items)
					max_items = Number (max_items);

				if (item.length) {
					var s = xmpp.stanza('items', {node: nodeID});

					for (var i=0; i<item.length; i++) {
						var j = storage.getItem(nodeID, item[i]);
						if (typeof j == 'number')
							return makeError(response, j);
						if (j == errors.success)
							continue;

						var k = xmpp.stanza('item', {id: item[i]})
						k.cnode(j);
						s.cnode(k);
					}
				} else {
					var s = xmpp.stanza('items', {node: nodeID});

					var j;
					if (max_items)
						j = storage.getLastItem(nodeID, max_items);
					else
						j = storage.getItems(nodeID);
					if (typeof j == 'number')
						return makeError(response, j);

					var k = 0;
					for (var i in j) {
						var contentItem = xmpp.stanza('item', {id: i}).t(j[i].content);
						s.cnode(contentItem);
					}
				}
				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'});
				p.cnode(s);
				response.cnode(p);
			} else
				return makeError(response, errors.feature_not_implemented.n);
		} else if (stanza.getChild('pubsub', 'http://jabber.org/protocol/pubsub#owner')) {
			var pubsub = stanza.getChild('pubsub', 'http://jabber.org/protocol/pubsub#owner');

			// SECTION 8.2
			if (pubsub.getChild('configure')) {
				if (!config.enabled('config-node'))
					return makeError(response, errors.owner.configure.node_configuration_not_supported.n);

				var nodeID = pubsub.getChild('configure').getAttribute('node');
				if (!nodeID || nodeID == '')
					return makeError(response, errors.nodeid_required.n);
				if (!storage.existsNode(nodeID))
					return makeError(response, errors.node_does_not_exist.n);

				var affil = storage.getAffiliation(toBareJID(to), nodeID);
				if (affil != 'super-owner' && affil != 'owner' && affil != 'publish-only')
					return makeError(response, errors.pub.publish.insufficient_privileges.n);

				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub#owner'});
				var s = xmpp.stanza('configure', {node: nodeID});
				var form = forms.build('form', 'node_config', 'default', true);
				s.cnode(form);
				p.cnode(s);
				response.cnode(p);

			// SECTION 8.3
			} else if (pubsub.getChild('default')) {
				if (!config.enabled('config-node'))
					return makeError(response, errors.owner.default_options.node_configuration_not_supported.n);

				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub#owner'});
				var s = xmpp.stanza('default');
				var form = forms.build('node_config', service_configuration.node_config, null, true);
				s.cnode(form);
				p.cnode(s);
				response.cnode(p);

			// SECTION 8.8
			} else if (pubsub.getChild('subscriptions')) {
				if (!config.enabled('manage-subscriptions'))
					return makeError(response, errors.owner.manage_subscriptions.not_supported.n);

				var subscriptions = pubsub.getChild('subscriptions');

				var nodeID = subscriptions.getAttribute('node');
				if (!nodeID || nodeID == '')
					return makeError(response, errors.nodeid_required.n);
				if (!storage.existsNode(nodeID))
					return makeError(response, errors.node_does_not_exist.n);

				var affil = storage.getAffiliation(toBareJID(to), nodeID);
				if (affil != 'super-owner' && affil != 'owner')
					return makeError(response, errors.forbidden.n);

				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub#owner'});
				var s = xmpp.stanza('subscriptions', {node: nodeID});

				var subs = storage.getSubscriptionsFromNodeID(nodeID)
				for (var jid in subs)
					s.s('subscription', {jid: jid, subscription: subs[jid].type, subid: subs[jid].subid})

				p.cnode(s);
				response.cnode(p);

			// SECTION 8.9
			} else if (pubsub.getChild('affiliations')) {
				if (!config.enabled('modify-affiliations'))
					return makeError(response, errors.owner.manage_affiliations.not_supported.n);

				var affiliations = pubsub.getChild('affiliations');

				var nodeID = affiliations.getAttribute('node');
				if (!nodeID || nodeID == '')
					return makeError(response, errors.nodeid_required.n);
				if (!storage.existsNode(nodeID))
					return makeError(response, errors.node_does_not_exist.n);

				var affils = storage.getAffiliationsFromNodeID(nodeID);
				var affil = affils[toBareJID(to)];
				if (affil != 'super-owner' && affil != 'owner')
					return makeError(response, errors.owner.manage_affiliations.retrieve_list.entity_is_not_an_owner.n);

				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub#owner'});
				var s = xmpp.stanza('affiliations', {node: nodeID});

				for (var jid in affils)
					s.s('affiliation', {jid: jid, affiliation: affils[jid]})

				p.cnode(s);
				response.cnode(p);
			} else
				return makeError(response, errors.feature_not_implemented.n);
		} else
			return makeError(response, errors.feature_not_implemented.n);
	} else if (type == 'set') {
		if (stanza.getChild('command', 'http://jabber.org/protocol/commands')) {
			// XEP-0050: Ad-Hoc Commands
			var command = stanza.getChild('command', 'http://jabber.org/protocol/commands');

			var action = command.getAttribute('action');
			if (action != 'execute')
				return makeError(response, errors.bad_request.n);

			var node = command.getAttribute('node');
			if (node == 'ping') {
				var cmd = xmpp.stanza('command', {xmlns: 'http://jabber.org/protocol/commands',
//						      sessionid: 'list:20020923T213616Z-700',
						      node: node,
						      'status': 'completed'})
					.c('note', {type: 'info'}).t('pong');
				response.cnode(cmd);
			} else if (node == 'reload') {
				storage.load();
				response.c('command', {xmlns: 'http://jabber.org/protocol/commands',
						       node: node,
						       'status': 'completed'})
					.c('note', {type: 'info'}).t('The server has correctly reloaded.');
			} else
				return makeError(response, errors.bad_request.n);
		} else if (stanza.getChild('pubsub', 'http://jabber.org/protocol/pubsub')) {
			var pubsub = stanza.getChild('pubsub', 'http://jabber.org/protocol/pubsub');

			// SECTION 6.1
			if (pubsub.getChild('subscribe')) {
				if (!config.enabled('subscribe'))
					return makeError(response, errors.sub.subscribe.not_supported.n);

				var subscribe = pubsub.getChild('subscribe');

				var nodeID = subscribe.getAttribute('node');
				if (!nodeID || nodeID == '')
					return makeError(response, errors.nodeid_required.n);
				if (!storage.existsNode(nodeID))
					return makeError(response, errors.node_does_not_exist.n);

				var configuration = storage.getConfiguration(nodeID);
				if (!configuration['pubsub#subscribe'])
					return makeError(response, errors.sub.subscribe.not_supported.n);

				var affil = storage.getAffiliation(toBareJID(to), nodeID);
				if (affil == 'publish-only' || affil == 'outcast')
					return makeError(response, errors.pub.publish.insufficient_privileges.n);

				var jid = subscribe.getAttribute('jid');
				if (!jid || toBareJID(jid) != toBareJID(to))
					return makeError(response, errors.sub.subscribe.jids_do_not_match.n);

				// SECTION 6.3.7
				var options = pubsub.getChild('options');
				if (options && config.enabled('subscription-options')) {
					if (options.getAttribute('node') || options.getAttribute('jid'))
						return makeError(response, errors.bad_request.n);

					var x = options.getChild('x', 'jabber:x:data');
					if (!x || x.getAttribute('type') != 'submit')
						return makeError(response, errors.bad_request.n);

					var form = forms.parse(x, true);
					if (typeof form == 'number')
						return makeError(response, form);

					var conf = form;
				}

				var subID;
				if (configuration['pubsub#access_model'] == 'open') {
					subID = storage.subscribe(nodeID, jid, 'subscribe', conf);
					if (typeof subID == 'number')
						return makeError(response, subID);
				} else if (configuration['pubsub#access_model'] == 'authorize') {
					subID = storage.subscribe(nodeID, jid, 'pending', conf);
					if (typeof subID == 'number')
						return makeError(response, subID);
				} else if (configuration['pubsub#access_model'] == 'whitelist') {
					var affil = storage.getAffiliation(jid, nodeID);
					if (affil != 'super-owner' && affil != 'owner' && affil != 'publisher' && affil != 'member')
						return makeError(response, errors.sub.subscribe.not_on_whitelist.n);

					subID = storage.subscribe(nodeID, jid, conf);
					if (typeof subID == 'number')
						return makeError(response, subID);
				}

				response.c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'})
					.c('subscription', {node: nodeID, jid: jid, subid: subID.subid, subscription: subID.type});

				if (conf)
					response.cnode(options);

				if (config.enabled('get-pending')) {
					var affiliates = storage.getAffiliationsFromNodeID(nodeID);
					var form = forms.build('form', 'subscribe_authorization', {allow: false, node: nodeID, subscriber_jid: jid}, true); //168
					for (var i in affiliates) {
						if (affiliates[i] == 'super-owner' || affiliates[i] == 'owner') {
							var message = xmpp.message({to: i}).cnode(form);
							conn.send(message);
						}
					}
				}

				if (config.enabled('last-published')) {
					var last = storage.getLastItem(nodeID);
					if (typeof last != 'number') {
						var item = storage.getItem(nodeID, last);
						if (typeof item != 'number') {
							var attr = {};
							attr[last] = {content: item};
							sendNotifs(jid, 'items', nodeID, attr);
						}
					}
				}

			// SECTION 6.2
			} else if (pubsub.getChild('unsubscribe')) {
				if (!config.enabled('subscribe'))
					return makeError(response, errors.sub.subscribe.not_supported.n);

				var unsubscribe = pubsub.getChild('unsubscribe');
				var nodeID = unsubscribe.getAttribute('node');
				if (!nodeID || nodeID == '')
					return makeError(response, errors.nodeid_required.n);
				if (!storage.existsNode(nodeID))
					return makeError(response, errors.node_does_not_exist.n);

				var jid = unsubscribe.getAttribute('jid');
				if (!jid || toBareJID(jid) != toBareJID(to))
					return makeError(response, errors.sub.unsubscribe.insufficient_privileges.n);

				var subID = storage.subscribe(nodeID, jid, 'none');
				if (typeof subID == 'number')
					return makeError(response, subID);

			// SECTIONS 6.3.5
			} else if (pubsub.getChild('options')) {
				if (!config.enabled('subscription-options'))
					return makeError(response, errors.sub.subscribe.not_supported.n);

				var options = pubsub.getChild('options');

				var nodeID = unsubscribe.getAttribute('node');
				if (!nodeID || nodeID == '')
					return makeError(response, errors.nodeid_required.n);
				if (!storage.existsNode(nodeID))
					return makeError(response, errors.node_does_not_exist.n);

				var jid = unsubscribe.getAttribute('jid');
				if (!jid || toBareJID(jid) != toBareJID(to))
					return makeError(response, errors.sub.unsubscribe.insufficient_privileges.n);

				var x = options.getChild('x', 'jabber:x:data');
				if (!x || x.getAttribute(type) != 'submit')
					return makeError(response, errors.bad_request);

				var form = forms.parse(x, true);
				if (typeof form == 'number')
					return makeError(response, form);

				var set = storage.configureSubscription(nodeID, jid, form);
				if (typeof form == 'number')
					return makeError(response, form);

			// SECTION 7.1
			} else if (pubsub.getChild('publish')) {
				if (!config.enabled('publish'))
					return makeError(response, errors.pub.publish.item_publication_not_supported.n);

				var publish = pubsub.getChild('publish');
				var nodeID = publish.getAttribute('node');
				if (!nodeID || nodeID == '')
					return makeError(response, errors.nodeid_required.n);

				var autocreate = false;
				if (!storage.existsNode(nodeID)) {
					if (config.enabled('auto-create'))
						autocreate = true;
					else
						return makeError(response, errors.node_does_not_exist.n);
				}

				var affil = storage.getAffiliation(toBareJID(to), nodeID);
				if (typeof affil == 'number' && affil != errors.node_does_not_exist.n)
					return makeError(response, affil);
				if (!autocreate && affil != 'super-owner' && affil != 'owner' && affil != 'publisher' && affil != 'publish-only')
					return makeError(response, errors.forbidden.n);

				var item = publish.getChild('item');
				var itemID = item.getAttribute('id');
				if (!config.enabled('item-ids') && itemID)
					return makeError(response, errors.itemid_required.n);
				itemID = itemID? itemID: utils.makeRandomId();

				if (item.tags.length != 1)
					return makeError(response, errors.pub.publish.bad_payload.n);

				var conf = storage.getConfiguration(nodeID);
				var publishOptions = pubsub.getChild('publish-options');
				if (publishOptions && config.enabled('publish-options')) {
					var x = publishOptions.getChild('x', 'jabber:x:data');
					if (!x || x.getAttribute('type') != 'submit')
						return makeError(response, errors.bad_request.n);

					var form = forms.parse(x, true);
					if (form.access_model != conf['pubsub#access_model'] && !autocreate)
						return makeError(response, errors.pub.configuration.precondition.n);
				}

				if (!config.enabled('persistent-items')) {
					var notifs = storage.purgeNode(nodeID);
					if (typeof notifs == 'number')
						return makeError(response, r);
				}

				if (autocreate) {
					if (!form)
						form = {};
					form['pubsub#creator'] = toBareJID(to);

					var r = storage.createNode(nodeID, form);
					if (typeof r == 'number')
						return makeError(response, r);
				}

				var content = item.getChild();

				subscribers = storage.setItem(nodeID, itemID, content);
				if (typeof subscribers == 'number')
					return makeError(response, subscribers);

				var attrs = {};
				if (content)
					attrs[itemID] = {content: content};
				else
					attrs[itemID] = {};
				sendNotifs(subscribers, 'items', nodeID, attrs);

				response.c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'})
					.c('publish', {node: nodeID})
						.c('item', {id: itemID});

			// SECTION 7.2
			} else if (pubsub.getChild('retract')) {
				if (!config.enabled('retract-items'))
					return makeError(response, errors.pub.retract.item_deletion_not_supported.n);

				var retract = pubsub.getChild('retract');

				var nodeID = retract.getAttribute('node');
				if (!nodeID || nodeID == '')
					return makeError(response, errors.nodeid_required.n);
				if (!storage.existsNode(nodeID))
					return makeError(response, errors.node_does_not_exist.n);

				var item = retract.getChild('item');
				if (!item)
					return makeError(response, errors.pub.retract.item_or_itemid_required.n);

				var itemID = item.getAttribute('id')
				if (!itemID || itemID == '')
					return makeError(response, errors.pub.retract.item_or_itemid_required.n);

				var affil = storage.getAffiliation(toBareJID(to), nodeID);
				if (affil != 'super-owner' && affil != 'owner' && affil != 'publish-only')
					return makeError(response, errors.forbidden.n);

				var subscribers = storage.deleteItem(nodeID, itemID);
				if (typeof subscribers == 'number')
					return makeError(response, subscribers);

				var attrs = {};
				attrs[itemID] = {};
				sendNotifs(subscribers, 'items', nodeID, attrs, 'retract')

			// SECTION 8.1
			} else if (pubsub.getChild('create')) {
				if (!config.enabled('create-nodes'))
					return makeError(response, errors.owner.create.node_creation_not_supported.n);

				var instant = false;

				var nodeID = pubsub.getChild('create').getAttribute('node');
				if (!nodeID || nodeID == '') {
					if (!config.enabled('instant-nodes'))
						return makeError(response, errors.owner.create.instant_nodes_not_supported.n);
					nodeID = utils.makeRandomId();
					instant = true;
				}
				if (storage.existsNode(nodeID))
					return makeError(response, errors.nodeid_already_exists.n);

				var bare = toBareJID(to);
				var right = false;

				// Check for super-owner
				for (var i in config.owner)
					if (config.owner[i] == bare)
						right = true;

				// Check for authorized user
				for (var i in config.allowCreateNode)
					if (config.allowCreateNode[i].exec(bare))
						right = true;

				if (!right)
					return makeError(response, errors.forbidden.n);

				var configure = pubsub.getChild('configure');
				if (configure && config.enabled('create-and-configure')) {
					if (!config.enabled('config-node'))
						return makeError(response, errors.owner.configure.node_configuration_not_supported.n);

					if (configure.getAttribute('node'))
						return makeError(response, errors.bad_request.n);

					var x = configure.getChild('x', 'jabber:x:data');
					if (!x || x.getAttribute('type') != 'submit')
						return makeError(response, errors.bad_request.n);

					var form = forms.parse(x, true);
					if (typeof form == 'number')
						return makeError(response, form);

					var conf = form;
				}

				if (!conf)
					conf = {};
				conf['pubsub#creator'] = toBareJID(to);

				var r = storage.createNode(nodeID, conf);
				if (typeof r == 'number')
					return makeError(response, r);

				if (instant)
					response.c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'})
						.c('create', {node: nodeID});
			} else
				return makeError(response, errors.feature_not_implemented.n);
		} else if (stanza.getChild('pubsub', 'http://jabber.org/protocol/pubsub#owner')) {
			var pubsub = stanza.getChild('pubsub', 'http://jabber.org/protocol/pubsub#owner');

			// SECTION 8.2.4
			if (pubsub.getChild('configure')) {
				if (!config.enabled('config-node'))
					return makeError(response, errors.owner.configure.node_configuration_not_supported.n);

				var nodeID = configure.getAttribute('node');
				if (!nodeID)
					return makeError(response, errors.nodeid_required.n);
				if (!storage.existsNode(nodeID))
					return makeError(response, errors.node_does_not_exist.n);

				var affil = storage.getAffiliation(toBareJID(to), nodeID);
				if (affil != 'super-owner' && affil != 'owner' && affil != 'publish-only')
					return makeError(response, errors.forbidden.n);

				var x = configure.getChild('x', 'jabber:x:data');
				if (!x || x.getAttribute(type) != 'submit')
					return makeError(response, errors.bad_request.n);

				var form = forms.parse(x, true);
				if (typeof form == 'number')
					return makeError(response, form);

				var conf = form;

				var set = storage.configure(nodeID, conf);
				if (typeof set == 'number')
					return makeError(response, set);

			// SECTION 8.4
			} else if (pubsub.getChild('delete')) {
				if (!config.enabled('delete-nodes'))
					return makeError(response, errors.feature_not_implemented.n); //XXX

				var del = pubsub.getChild('delete');

				var nodeID = del.getAttribute('node');
				if (!nodeID)
					return makeError(response, errors.nodeid_required.n);
				if (!storage.existsNode(nodeID))
					return makeError(response, errors.node_does_not_exist.n);

				var affil = storage.getAffiliation(toBareJID(to), nodeID);
				if (affil != 'super-owner' && affil != 'owner')
					return makeError(response, errors.forbidden.n);

				var notifs = storage.deleteNode(nodeID);
				if (typeof notifs == 'number')
					return makeError(response, r);

				sendNotifs(notifs, 'delete', nodeID);

			// SECTION 8.5
			} else if (pubsub.getChild('purge')) {
				if (!config.enabled('purge-nodes'))
					return makeError(response, errors.owner.purge.node_purging_not_supported.n); //XXX

				var purge = pubsub.getChild('purge');

				var nodeID = purge.getAttribute('node');
				if (!nodeID)
					return makeError(response, errors.nodeid_required.n);
				if (!storage.existsNode(nodeID))
					return makeError(response, errors.node_does_not_exist.n);

				var affil = storage.getAffiliation(toBareJID(to), nodeID);
				if (affil != 'super-owner' && affil != 'owner')
					return makeError(response, errors.forbidden.n);

				if (!config.enabled('persistent-items')) //FIXME: autre condition, supporté par le node
					return makeError(response, errors.owner.purge.node_does_not_persist_items.n);

				var notifs = storage.purgeNode(nodeID);
				if (typeof notifs == 'number')
					return makeError(response, r);

				sendNotifs(notifs, 'purge', nodeID);

			// SECTION 8.8.2
			} else if (pubsub.getChild('subscriptions')) {
				if (!config.enabled('manage-subscriptions'))
					return makeError(response, errors.owner.manage_subscriptions.not_supported.n); //XXX

				var subscriptions = pubsub.getChild('subscriptions');

				var nodeID = subscriptions.getAttribute('node');
				if (!nodeID)
					return makeError(response, errors.nodeid_required.n);
				if (!storage.existsNode(nodeID))
					return makeError(response, errors.node_does_not_exist.n);

				var affil = storage.getAffiliation(toBareJID(to), nodeID);
				if (affil != 'super-owner' && affil != 'owner')
					return makeError(response, errors.forbidden.n);

				var e = false;
				for (i in subscriptions.tags) {
					var jid = subscriptions.tags[i].getAttribute('jid');
					var subscription = subscriptions.tags[i].getAttribute('subscription');

					var set = storage.subscribe(nodeID, jid, subscription);
					if (typeof set == 'number')
						e = true;
					else {
						// SECTION 8.8.4
						sendNotifs(jid, 'subscription', nodeID, {jid: jid, subscription: subscription});
						subscriptions.tags.splice(i, 1);
					}
				}

				if (e)
					return makeError(response, errors.owner.manage_subscriptions.modify.multiple_simultaneous_modifications.n, pubsub);

			// SECTION 8.9.2
			} else if (pubsub.getChild('affiliations')) {
				if (!config.enabled('modify-affiliations'))
					return makeError(response, errors.owner.manage_affiliations.not_supported.n); //XXX

				var affiliations = pubsub.getChild('affiliations');

				var nodeID = affiliations.getAttribute('node');
				if (!nodeID)
					return makeError(response, errors.nodeid_required.n);
				if (!storage.existsNode(nodeID))
					return makeError(response, errors.node_does_not_exist.n);

				var affil = storage.getAffiliation(toBareJID(to), nodeID);
				if (affil != 'super-owner' && affil != 'owner')
					return makeError(response, errors.forbidden.n);

				var e = false;
				for (i in affiliations.children) {
					var jid = affiliations.children[i].getAttribute('jid');
					var affiliation = affiliations.children[i].getAttribute('affiliation');

					var set = storage.setAffiliation(nodeID, jid, affiliation);
					if (typeof set == 'number')
						e = true;
					else {
						// SECTION 8.9.4
						sendNotifs(jid, 'affiliations', nodeID, {jid: jid, affiliation: affiliation});
						affiliations.children.splice(i, 1);
					}
				}

				if (e)
					return makeError(response, errors.owner.manage_affiliations.modify.multiple_simultaneous_modifications.n, pubsub);
			} else
				return makeError(response, errors.feature_not_implemented.n);
		} else
			return makeError(response, errors.feature_not_implemented.n);
	} else
		return makeError(response, errors.feature_not_implemented.n);
	conn.send(response);
}

function onMessage(stanza) {
	var from = stanza.getAttribute('to');
	var to = stanza.getAttribute('from');
	var id = stanza.getAttribute('id');

	var response;
	if (id)
		response = xmpp.message({to: to, from: from, id: id});
	else
		response = xmpp.message({to: to, from: from});

	var x = stanza.getChild('x', 'jabber:x:data');
	if (x) {
		var form = forms.parse(x);
		if (form.type == 'submit' && form.fields.FORM_TYPE.value == 'subscribe_authorization') {
			if (form.fields.subid && form.fields.subid.value)
				var subID = form.fields.subid.value;
			if (form.fields.node && form.fields.node.value)
				var nodeID = form.fields.node.value;
			if (form.fields.subscriber_jid && form.fields.subscriber_jid.value)
				var jid = form.fields.subscriber_jid.value;
			if (form.fields.allow && form.fields.allow.value)
				var allow = form.fields.allow.value;

			var type = allow? 'subscribed': 'none';
			var set = storage.subscribe(nodeID, jid, type)
			//if (set.subid != subID) //TODO: support the multi-subscribe feature
			sendNotifs(jid, 'subscription', nodeID, {jid: jid, subscription: type});
		} else
			return makeError(response, errors.feature_not_implemented.n);
	} else
		return makeError(response, errors.feature_not_implemented.n);
	conn.send(response)
}

function onPresence(stanza) {
	var from = stanza.getAttribute('to');
	var to = stanza.getAttribute('from');
	var id = stanza.getAttribute('id');

	var response;
	if (id)
		response = xmpp.presence({to: to, from: from, id: id});
	else
		response = xmpp.presence({to: to, from: from});

	makeError(response, errors.feature_not_implemented.n);
}

function makeError(response, errorNumber, payload) {
	response.attr.type = 'error';
	if (payload)
		response.cnode(payload);

	var e = errors.reverse[errorNumber];
	var error = xmpp.stanza('error', {type: e.type});

	error.s(e.error, {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'})

	if (e.reason) {
		if (e.feature)
			error.s(e.reason, {xmlns: 'http://jabber.org/protocol/pubsub#errors', feature: e.feature});
		else
			error.s(e.reason, {xmlns: 'http://jabber.org/protocol/pubsub#errors'});
	}

	response.cnode(error);
	conn.send(response);
}

function sendNotifs(notifs, type, nodeID, a1, a2) {
	var ev = xmpp.stanza('event', {xmlns: 'http://jabber.org/protocol/pubsub#event'});

	if (type == 'affiliations') {
		ev.attr.xmlns = 'http://jabber.org/protocol/pubsub';

		var args = {};
		for (i in a1) {
			var attr = a1[i];
			if (i == 'affiliation')
				args.affiliation = attr;
			else if (i == 'jid')
				args.jid = attr;
		}
		var affiliations = xmpp.stanza('affiliations', {node: nodeID})
			.c('affiliation', args);
		ev.cnode(affiliations);
	} else if (type == 'collection') {
		var collection = xmpp.stanza('collection', {node: nodeID});
		if (a1 == 'associate')
			collection.cnode('associate', {node: nodeID});
		else
			collection.cnode('disassociate', {node: nodeID});
		ev.cnode(collection);
	} else if (type == 'configuration') {
		if (!config.enabled('config-node')) {
			_('Error #4', 41)
			return;
		}

		var configuration = xmpp.stanza('configuration', {node: nodeID});
		if (a1) {
			var x = forms.build('node_config', service_configuration.node_config, storage.getConfiguration(nodeID));
			if (x)
				configuration.cnode(x); //TODO: voir exemple 150
		}
		ev.cnode(configuration);
	} else if (type == 'delete') {
		var del = xmpp.stanza('delete', {node: nodeID});
		if (a1)
			del.c('redirect', {uri: a1});
		ev.cnode(del);
	} else if (type == 'items') {
		var items = xmpp.stanza(type, {node: nodeID});
		if (a2 == 'retract')
			for (var i in a1)
				items.s('retract', {id: i});
		else {
			for (var i in a1) {
				var item = a1[i];
				var args = {};
				if (i != '')
					args.id = i;
				if (item.node)
					args.node = item.node;
				if (item.publisher)
					args.publisher = item.publisher;
				var it = xmpp.stanza('item', args);
				if (item.content)
					it.cnode(item.content);
				items.cnode(it);
			}
		}
		ev.cnode(items);
	} else if (type == 'purge') {
		ev.c('purge', {node: nodeID});
	} else if (type == 'subscription') {
		if (!config.enabled('subscription-notifications'))
			return;

		var args = {node: nodeID};
		for (i in a1) {
			var attr = a1[i];
			if (i == 'subscription') {
				if (attr == 'none' || attr == 'pending' || attr == 'subscribed' || attr == 'unconfigured')
					args[i] = attr;
				else {
					_('Error #3', 41)
					return;
				}
			} else if (i == 'jid' || i == 'subid')
				args[i] = attr;
			else if (i == 'expiry')
				args[i] = attr.toString();
		}
		if (!args.jid || args.jid == '') {
			_('Error #2', 41)
			return;
		}
		var sub = xmpp.stanza('subscription', args);
		ev.cnode(sub);
	} else {
		_('Error #1', 41)
		return;
	}

	var subs;
	if (typeof notifs == 'string') {
		subs = {};
		subs[notifs] = storage.getSubscription(notifs, nodeID);
	} else
		subs = notifs;

	for (var i in subs) {
		var sub = subs[i];

		if (sub.options) {
			if (typeof sub.options['pubsub#deliver'] != 'undefined' && !sub.options['pubsub#deliver'])
				continue;

			if (typeof sub.options['pubsub#digest'] != 'undefined' && sub.options['pubsub#digest']) {
				if (!sub.digest)
					sub.digest = [];
				sub.digest.push(ev)

				if (sub.digestTimeout)
					continue;

				var freq;
				if (typeof sub.options['pubsub#digest_frequency'] == 'undefined')
					freq = 0;
				else
					freq = parseInt(sub.options['pubsub#digest_frequency']);

				if (freq == 0)
					freq = 24*60*60*1000;

				setTimeout(sendDigest, freq, notifs[i], nodeID);
				sub.digestTimeout = true;
				continue;
			}
		}

		var message = xmpp.message({to: i, from: componentJID, id: conn.getUniqueId()});
		message.cnode(ev);
		conn.send(message);
	}
}

function sendDigest(jid, nodeID) {
	var sub = storage.getSubscription(jid, nodeID);
	if (sub.digestTimeout)
		sub.digestTimeout = false;

	var message = xmpp.message({to: jid, from: componentJID, id: conn.getUniqueId()});
	for (var i in sub.digest)
		message.cnode(sub.digest[i]);
	conn.send(message);
}

conn.connect(componentJID, componentPassword, function (status, condition) {
	if (status == xmpp.Status.CONNECTED) {
		conn.addHandler(onMessage, null, 'message', null, null,  null);
		conn.addHandler(onIq, null, 'iq', null, null,  null);
		conn.addHandler(onPresence, null, 'presence', null, null,  null);

		if (process.argv.length >= 3)
			storage.load(process.argv[2]);
		else
			storage.load();

		var stdin = process.openStdin();
		stdin.setEncoding('utf8');
		stdin.addListener('data', storage.debug);
	} else
		conn.log(xmpp.LogLevel.DEBUG, 'New connection status: ' + status + (condition? (' ('+condition+')'): ''));
});