Mercurial > psgxs
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+')'): '')); +});