diff psgxs.js @ 0:9ee956af41e3

Initial commit
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Sun, 27 Jun 2010 22:05:12 +0200
parents
children c2954a9e5665
line wrap: on
line diff
new file mode 100755
--- /dev/null
+++ b/psgxs.js
@@ -0,0 +1,1074 @@
+#!/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 sys = require('sys');
+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) { sys.puts(m); };
+
+function _(m, c) {
+	if (c)
+		sys.print('\033[1;'+c+'m');
+	sys.print(sys.inspect(m, false, null));
+	if (c)
+		sys.print('\033[0m');
+	sys.puts('');
+};
+
+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') {
+		if (stanza.getChild('query', 'jabber:iq:version')) {
+			var os = 'GNU/Linux';
+			var query = xmpp.stanza('query', {xmlns: 'jabber:iq:version'})
+				.s('name').t('PubSubJS')
+				.s('version').t('Pas releasé')
+				.s('os').t(os);
+			response.cx(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'})
+				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', 'meta-data', storage.getMetadata(nodeID), true);
+					if (x)
+						q.cx(x);
+				}
+				response.cx(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': '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.cx(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 (!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.cx(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.cx(s);
+				response.cx(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 = 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.cx(s);
+				response.cx(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.cx(form);
+				p.cx(s);
+				response.cx(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.cx(form);
+				p.cx(s);
+				response.cx(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 affil = storage.getAffiliation(toBareJID(to), nodeID);
+				if (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);
+
+						var k = xmpp.stanza('item', {id: item[i]})
+						k.cx(j);
+						s.cx(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.cx(contentItem);
+					}
+				}
+				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'});
+				p.cx(s);
+				response.cx(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
+			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 != '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.cx(form);
+				p.cx(s);
+				response.cx(p);
+
+			// SECTION
+			} 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.cx(form);
+				p.cx(s);
+				response.cx(p);
+
+			// SECTION
+			} 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);
+
+				if (storage.getAffiliation(toBareJID(to), nodeID) != '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.cx(s);
+				response.cx(p);
+
+			// SECTION
+			} 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 affil = storage.getAffiliationsFromNodeID(nodeID);
+				if (affil[toBareJID(to)] != '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 affil)
+					s.s('affiliation', {jid: jid, affiliation: affil[jid]})
+
+				p.cx(s);
+				response.cx(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('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 != '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.cx(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] == 'owner') {
+							var message = xmpp.message({to: i}).cx(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
+			} 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 affil = storage.getAffiliation(toBareJID(to), nodeID);
+				if (typeof affil == 'number')
+					return makeError(response, affil);
+				if (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 autocreate = false;
+				if (!storage.existsNode(nodeID)) {
+					if (config.enabled('auto-create'))
+						autocreate = true;
+					else
+						return makeError(response, errors.node_does_not_exist.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)
+					storage.createNode(nodeID, form);
+
+				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
+			} 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 subscribers = storage.deleteItem(nodeID, itemID);
+				if (typeof subscribers == 'number')
+					return makeError(response, subscribers);
+
+				var attrs = {};
+				attrs[itemID] = {};
+				sendNotifs(subscribers, 'items', nodeID, attrs, 'retract')
+
+			// SECTION
+			} 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 affil = storage.getAffiliation(toBareJID(to), nodeID);
+				if (affil != 'owner' && affil != 'publish-only')
+					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;
+				}
+
+				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
+			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 != '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
+			} 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);
+
+				if (storage.getAffiliation(toBareJID(to), nodeID) != '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
+			} 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);
+
+				if (storage.getAffiliation(toBareJID(to), nodeID) != '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
+			} 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);
+
+				if (storage.getAffiliation(toBareJID(to), nodeID) != '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 {
+						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
+			} 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);
+
+				if (storage.getAffiliation(toBareJID(to), nodeID) != '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
+						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.cx(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.cx(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 == 'collection') {
+		var collection = xmpp.stanza('collection', {node: nodeID});
+		if (a1 == 'associate')
+			collection.cx('associate', {node: nodeID});
+		else
+			collection.cx('disassociate', {node: nodeID});
+		ev.cx(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.cx(x); //TODO: voir exemple 150
+		}
+		ev.cx(configuration);
+	} else if (type == 'delete') {
+		var del = xmpp.stanza('delete', {node: nodeID});
+		if (a1)
+			del.c('redirect', {uri: a1});
+		ev.cx(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.cx(item.content);
+				items.cx(it);
+			}
+		}
+		ev.cx(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.cx(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.cx(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.cx(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+')'): ''));
+});