changeset 753:9d5731af2c27

Merge with Oliver Gerlich
author Matthew Wild <mwild1@gmail.com>
date Fri, 27 Jul 2012 14:29:59 +0100
parents 9bbd99f2057a (current diff) 3c37445f26ac (diff)
children 713c6791fbcc
files mod_adhoc_cmd_admin/mod_adhoc_cmd_admin.lua mod_adhoc_cmd_modules/mod_adhoc_cmd_modules.lua mod_adhoc_cmd_ping/mod_adhoc_cmd_ping.lua mod_adhoc_cmd_uptime/mod_adhoc_cmd_uptime.lua mod_admin_web/admin_web/mod_admin_web.lua mod_admin_web/admin_web/mod_admin_web_timber.lua mod_archive/mod_archive.lua
diffstat 27 files changed, 1529 insertions(+), 1470 deletions(-) [+]
line wrap: on
line diff
--- a/mod_adhoc_cmd_admin/mod_adhoc_cmd_admin.lua	Mon Jun 11 22:32:45 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,512 +0,0 @@
--- Copyright (C) 2009-2010 Florian Zeitz
--- 
--- This file is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-local _G = _G;
-
-local prosody = _G.prosody;
-local hosts = prosody.hosts;
-
-local t_concat = table.concat;
-
-local usermanager_user_exists = require "core.usermanager".user_exists;
-local usermanager_create_user = require "core.usermanager".create_user;
-local usermanager_get_password = require "core.usermanager".get_password;
-local usermanager_set_password = require "core.usermanager".set_password or
-	function (username, password, host) return usermanager_create_user(username, password, host) end;
-local is_admin = require "core.usermanager".is_admin;
-
-local rm_load_roster = require "core.rostermanager".load_roster;
-
-local st, jid, uuid = require "util.stanza", require "util.jid", require "util.uuid";
-local timer_add_task = require "util.timer".add_task;
-local dataforms_new = require "util.dataforms".new;
-module:log("debug", module:get_name());
-local adhoc_new = module:require "adhoc".new;
-
-local add_user_layout = dataforms_new{
-	title = "Adding a User";
-	instructions = "Fill out this form to add a user.";
-
-	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
-	{ name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for the account to be added" };
-	{ name = "password", type = "text-private", label = "The password for this account" };
-	{ name = "password-verify", type = "text-private", label = "Retype password" };
-};
-
-local change_user_password_layout = dataforms_new{
-	title = "Changing a User Password";
-	instructions = "Fill out this form to change a user's password.";
-
-	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
-	{ name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for this account" };
-	{ name = "password", type = "text-private", required = true, label = "The password for this account" };
-};
-
-local delete_user_layout = dataforms_new{
-	title = "Deleting a User";
-	instructions = "Fill out this form to delete a user.";
-
-	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
-	{ name = "accountjids", type = "jid-multi", label = "The Jabber ID(s) to delete" };
-};
-
-local end_user_session_layout = dataforms_new{
-	title = "Ending a User Session";
-	instructions = "Fill out this form to end a user's session.";
-
-	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
-	{ name = "accountjids", type = "jid-multi", label = "The Jabber ID(s) for which to end sessions" };
-};
-
-local get_user_password_layout = dataforms_new{
-	title = "Getting User's Password";
-	instructions = "Fill out this form to get a user's password.";
-
-	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
-	{ name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for which to retrieve the password" };
-};
-
-local get_user_password_result_layout = dataforms_new{
-	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
-	{ name = "accountjid", type = "jid-single", label = "JID" };
-	{ name = "password", type = "text-single", label = "Password" };
-};
-
-local get_user_roster_layout = dataforms_new{
-	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
-	{ name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for which to retrieve the roster" };
-};
-
-local get_user_roster_result_layout = dataforms_new{
-	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
-	{ name = "accountjid", type = "jid-single", label = "This is the roster for" };
-	{ name = "roster", type = "text-multi", label = "Roster XML" };
-};
-
-local get_user_stats_layout = dataforms_new{
-	title = "Get User Statistics";
-	instructions = "Fill out this form to gather user statistics.";
-
-	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
-	{ name = "accountjid", type = "jid-single", required = true, label = "The Jabber ID for statistics" };
-};
-
-local get_user_stats_result_layout = dataforms_new{
-	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
-	{ name = "ipaddresses", type = "text-multi", label = "IP Addresses" };
-	{ name = "rostersize", type = "text-single", label = "Roster size" };
-	{ name = "onlineresources", type = "text-multi", label = "Online Resources" };
-};
-
-local get_online_users_layout = dataforms_new{
-	title = "Getting List of Online Users";
-	instructions = "How many users should be returned at most?";
-
-	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
-	{ name = "max_items", type = "list-single", label = "Maximum number of users",
-		value = { "25", "50", "75", "100", "150", "200", "all" } };
-	{ name = "details", type = "boolean", label = "Show details" };
-};
-
-local get_online_users_result_layout = dataforms_new{
-	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
-	{ name = "onlineuserjids", type = "text-multi", label = "The list of all online users" };
-};
-
-local announce_layout = dataforms_new{
-	title = "Making an Announcement";
-	instructions = "Fill out this form to make an announcement to all\nactive users of this service.";
-
-	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
-	{ name = "subject", type = "text-single", label = "Subject" };
-	{ name = "announcement", type = "text-multi", required = true, label = "Announcement" };
-};
-
-local shut_down_service_layout = dataforms_new{
-	title = "Shutting Down the Service";
-	instructions = "Fill out this form to shut down the service.";
-
-	{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/admin" };
-	{ name = "delay", type = "list-single", label = "Time delay before shutting down",
-		value = { {label = "30 seconds", value = "30"},
-			  {label = "60 seconds", value = "60"},
-			  {label = "90 seconds", value = "90"},
-			  {label = "2 minutes", value = "120"},
-			  {label = "3 minutes", value = "180"},
-			  {label = "4 minutes", value = "240"},
-			  {label = "5 minutes", value = "300"},
-		};
-	};
-	{ name = "announcement", type = "text-multi", label = "Announcement" };
-};
-
-function add_user_command_handler(self, data, state)
-	if state then
-		if data.action == "cancel" then
-			return { status = "canceled" };
-		end
-		local fields = add_user_layout:data(data.form);
-		if not fields.accountjid then
-			return { status = "completed", error = { message = "You need to specify a JID." } };
-		end
-		local username, host, resource = jid.split(fields.accountjid);
-		if data.to ~= host then
-			return { status = "completed", error = { message = "Trying to add a user on " .. host .. " but command was sent to " .. data.to}};
-		end
-		if (fields["password"] == fields["password-verify"]) and username and host then
-			if usermanager_user_exists(username, host) then
-				return { status = "completed", error = { message = "Account already exists" } };
-			else
-				if usermanager_create_user(username, fields.password, host) then
-					module:log("info", "Created new account " .. username.."@"..host);
-					return { status = "completed", info = "Account successfully created" };
-				else
-					return { status = "completed", error = { message = "Failed to write data to disk" } };
-				end
-			end
-		else
-			module:log("debug", (fields.accountjid or "<nil>") .. " " .. (fields.password or "<nil>") .. " "
-				.. (fields["password-verify"] or "<nil>"));
-			return { status = "completed", error = { message = "Invalid data.\nPassword mismatch, or empty username" } };
-		end
-	else
-		return { status = "executing", form = add_user_layout }, "executing";
-	end
-end
-
-function change_user_password_command_handler(self, data, state)
-	if state then
-		if data.action == "cancel" then
-			return { status = "canceled" };
-		end
-		local fields = change_user_password_layout:data(data.form);
-		if not fields.accountjid or fields.accountjid == "" or not fields.password then
-			return { status = "completed", error = { message = "Please specify username and password" } };
-		end
-		local username, host, resource = jid.split(fields.accountjid);
-		if data.to ~= host then
-			return { status = "completed", error = { message = "Trying to change the password of a user on " .. host .. " but command was sent to " .. data.to}};
-		end
-		if usermanager_user_exists(username, host) and usermanager_set_password(username, fields.password, host) then
-			return { status = "completed", info = "Password successfully changed" };
-		else
-			return { status = "completed", error = { message = "User does not exist" } };
-		end
-	else
-		return { status = "executing", form = change_user_password_layout }, "executing";
-	end
-end
-
-function disconnect_user(match_jid)
-	local node, hostname, givenResource = jid.split(match_jid);
-	local host = hosts[hostname];
-	local sessions = host.sessions[node] and host.sessions[node].sessions;
-	for resource, session in pairs(sessions or {}) do
-		if not givenResource or (resource == givenResource) then
-			module:log("debug", "Disconnecting "..node.."@"..hostname.."/"..resource);
-			session:close();
-		end
-	end
-	return true;
-end
-
-function delete_user_command_handler(self, data, state)
-	if state then
-		if data.action == "cancel" then
-			return { status = "canceled" };
-		end
-		local fields = delete_user_layout:data(data.form);
-		local failed = {};
-		local succeeded = {};
-		for _, aJID in ipairs(fields.accountjids) do
-			local username, host, resource = jid.split(aJID);
-			if (host == data.to) and  usermanager_user_exists(username, host) and disconnect_user(aJID) and usermanager_create_user(username, nil, host) then
-				module:log("debug", "User " .. aJID .. " has been deleted");
-				succeeded[#succeeded+1] = aJID;
-			else
-				module:log("debug", "Tried to delete non-existant user "..aJID);
-				failed[#failed+1] = aJID;
-			end
-		end
-		return {status = "completed", info = (#succeeded ~= 0 and
-				"The following accounts were successfully deleted:\n"..t_concat(succeeded, "\n").."\n" or "")..
-				(#failed ~= 0 and
-				"The following accounts could not be deleted:\n"..t_concat(failed, "\n") or "") };
-	else
-		return { status = "executing", form = delete_user_layout }, "executing";
-	end
-end
-
-function end_user_session_handler(self, data, state)
-	if state then
-		if data.action == "cancel" then
-			return { status = "canceled" };
-		end
-
-		local fields = end_user_session_layout:data(data.form);
-		local failed = {};
-		local succeeded = {};
-		for _, aJID in ipairs(fields.accountjids) do
-			local username, host, resource = jid.split(aJID);
-			if (host == data.to) and  usermanager_user_exists(username, host) and disconnect_user(aJID) then
-				succeeded[#succeeded+1] = aJID;
-			else
-				failed[#failed+1] = aJID;
-			end
-		end
-		return {status = "completed", info = (#succeeded ~= 0 and
-				"The following accounts were successfully disconnected:\n"..t_concat(succeeded, "\n").."\n" or "")..
-				(#failed ~= 0 and
-				"The following accounts could not be disconnected:\n"..t_concat(failed, "\n") or "") };
-	else
-		return { status = "executing", form = end_user_session_layout }, "executing";
-	end
-end
-
-function get_user_password_handler(self, data, state)
-	if state then
-		if data.action == "cancel" then
-			return { status = "canceled" };
-		end
-		local fields = get_user_password_layout:data(data.form);
-		if not fields.accountjid then
-			return { status = "completed", error = { message = "Please specify a JID." } };
-		end
-		local user, host, resource = jid.split(fields.accountjid);
-		local accountjid = "";
-		local password = "";
-		if host ~= data.to then
-			return { status = "completed", error = { message = "Tried to get password for a user on " .. host .. " but command was sent to " .. data.to } };
-		elseif usermanager_user_exists(user, host) then
-			accountjid = fields.accountjid;
-			password = usermanager_get_password(user, host);
-		else
-			return { status = "completed", error = { message = "User does not exist" } };
-		end
-		return { status = "completed", result = { layout = get_user_password_result_layout, values = {accountjid = accountjid, password = password} } };
-	else
-		return { status = "executing", form = get_user_password_layout }, "executing";
-	end
-end
-
-function get_user_roster_handler(self, data, state)
-	if state then
-		if data.action == "cancel" then
-			return { status = "canceled" };
-		end
-
-		local fields = get_user_roster_layout:data(data.form);
-
-		if not fields.accountjid then
-			return { status = "completed", error = { message = "Please specify a JID" } };
-		end
-
-		local user, host, resource = jid.split(fields.accountjid);
-		if host ~= data.to then
-			return { status = "completed", error = { message = "Tried to get roster for a user on " .. host .. " but command was sent to " .. data.to } };
-		elseif not usermanager_user_exists(user, host) then
-			return { status = "completed", error = { message = "User does not exist" } };
-		end
-		local roster = rm_load_roster(user, host);
-
-		local query = st.stanza("query", { xmlns = "jabber:iq:roster" });
-		for jid in pairs(roster) do
-			if jid ~= "pending" and jid then
-				query:tag("item", {
-					jid = jid,
-					subscription = roster[jid].subscription,
-					ask = roster[jid].ask,
-					name = roster[jid].name,
-				});
-				for group in pairs(roster[jid].groups) do
-					query:tag("group"):text(group):up();
-				end
-				query:up();
-			end
-		end
-
-		local query_text = query:__tostring(); -- TODO: Use upcoming pretty_print() function
-		query_text = query_text:gsub("><", ">\n<");
-
-		local result = get_user_roster_result_layout:form({ accountjid = user.."@"..host, roster = query_text }, "result");
-		result:add_child(query);
-		return { status = "completed", other = result };
-	else
-		return { status = "executing", form = get_user_roster_layout }, "executing";
-	end
-end
-
-function get_user_stats_handler(self, data, state)
-	if state then
-		if data.action == "cancel" then
-			return { status = "canceled" };
-		end
-
-		local fields = get_user_stats_layout:data(data.form);
-
-		if not fields.accountjid then
-			return { status = "completed", error = { message = "Please specify a JID." } };
-		end
-
-		local user, host, resource = jid.split(fields.accountjid);
-		if host ~= data.to then
-			return { status = "completed", error = { message = "Tried to get stats for a user on " .. host .. " but command was sent to " .. data.to } };
-		elseif not usermanager_user_exists(user, host) then
-			return { status = "completed", error = { message = "User does not exist" } };
-		end
-		local roster = rm_load_roster(user, host);
-		local rostersize = 0;
-		local IPs = "";
-		local resources = "";
-		for jid in pairs(roster) do
-			if jid ~= "pending" and jid then
-				rostersize = rostersize + 1;
-			end
-		end
-		for resource, session in pairs((hosts[host].sessions[user] and hosts[host].sessions[user].sessions) or {}) do
-			resources = resources .. "\n" .. resource;
-			IPs = IPs .. "\n" .. session.ip;
-		end
-		return { status = "completed", result = {layout = get_user_stats_result_layout, values = {ipaddresses = IPs, rostersize = tostring(rostersize),
-			onlineresources = resources}} };
-	else
-		return { status = "executing", form = get_user_stats_layout }, "executing";
-	end
-end
-
-function get_online_users_command_handler(self, data, state)
-	if state then
-		if data.action == "cancel" then
-			return { status = "canceled" };
-		end
-
-		local fields = get_online_users_layout:data(data.form);
-
-		local max_items = nil
-		if fields.max_items ~= "all" then
-			max_items = tonumber(fields.max_items);
-		end
-		local count = 0;
-		local users = {};
-		for username, user in pairs(hosts[data.to].sessions or {}) do
-			if (max_items ~= nil) and (count >= max_items) then
-				break;
-			end
-			users[#users+1] = username.."@"..data.to;
-			count = count + 1;
-			if fields.details then
-				for resource, session in pairs(user.sessions or {}) do
-					local status, priority = "unavailable", tostring(session.priority or "-");
-					if session.presence then
-						status = session.presence:child_with_name("show");
-						if status then
-							status = status:get_text() or "[invalid!]";
-						else
-							status = "available";
-						end
-					end
-					users[#users+1] = " - "..resource..": "..status.."("..priority..")";
-				end
-			end
-		end
-		return { status = "completed", result = {layout = get_online_users_result_layout, values = {onlineuserjids=t_concat(users, "\n")}} };
-	else
-		return { status = "executing", form = get_online_users_layout }, "executing";
-	end
-end
-
-function send_to_online(message, server)
-	if server then
-		sessions = { [server] = hosts[server] };
-	else
-		sessions = hosts;
-	end
-
-	local c = 0;
-	for domain, session in pairs(sessions) do
-		for user in pairs(session.sessions or {}) do
-			c = c + 1;
-			message.attr.from = domain;
-			message.attr.to = user.."@"..domain;
-			core_post_stanza(session, message);
-		end
-	end
-
-	return c;
-end
-
-function announce_handler(self, data, state)
-	if state then
-		if data.action == "cancel" then
-			return { status = "canceled" };
-		end
-
-		local fields = announce_layout:data(data.form);
-
-		if (not fields.announcement) or (#fields.announcement == 0)  then
-			return { status = "completed", error = { message = "Please specify some announcement text." } };
-		end
-
-		module:log("info", "Sending server announcement to all online users");
-		local message = st.message({type = "headline"}, fields.announcement):up()
-			:tag("subject"):text(fields.subject or "Announcement");
-		
-		local count = send_to_online(message, data.to);
-		
-		module:log("info", "Announcement sent to %d online users", count);
-		return { status = "completed", info = "Announcement sent." };
-	else
-		return { status = "executing", form = announce_layout }, "executing";
-	end
-
-	return true;
-end
-
-function shut_down_service_handler(self, data, state)
-	if state then
-		if data.action == "cancel" then
-			return { status = "canceled" };
-		end
-
-		local fields = shut_down_service_layout:data(data.form);
-
-		if fields.announcement and #fields.announcement > 0 then
-			local message = st.message({type = "headline"}, fields.announcement):up()
-				:tag("subject"):text("Server is shutting down");
-			send_to_online(message);
-		end
-
-		timer_add_task(tonumber(fields.delay or "5"), prosody.shutdown);
-
-		return { status = "completed", info = "Server is about to shut down" };
-	else
-		return { status = "executing", form = shut_down_service_layout }, "executing";
-	end
-
-	return true;
-end
-
-local add_user_desc = adhoc_new("Add User", "http://jabber.org/protocol/admin#add-user", add_user_command_handler, "admin");
-local announce_desc = adhoc_new("Send Announcement to Online Users", "http://jabber.org/protocol/admin#announce", announce_handler, "admin");
-local change_user_password_desc = adhoc_new("Change User Password", "http://jabber.org/protocol/admin#change-user-password", change_user_password_command_handler, "admin");
-local delete_user_desc = adhoc_new("Delete User", "http://jabber.org/protocol/admin#delete-user", delete_user_command_handler, "admin");
-local end_user_session_desc = adhoc_new("End User Session", "http://jabber.org/protocol/admin#end-user-session", end_user_session_handler, "admin");
-local get_user_password_desc = adhoc_new("Get User Password", "http://jabber.org/protocol/admin#get-user-password", get_user_password_handler, "admin");
-local get_user_roster_desc = adhoc_new("Get User Roster","http://jabber.org/protocol/admin#get-user-roster", get_user_roster_handler, "admin");
-local get_user_stats_desc = adhoc_new("Get User Statistics","http://jabber.org/protocol/admin#user-stats", get_user_stats_handler, "admin");
-local get_online_users_desc = adhoc_new("Get List of Online Users", "http://jabber.org/protocol/admin#get-online-users", get_online_users_command_handler, "admin"); 
-local shut_down_service_desc = adhoc_new("Shut Down Service", "http://jabber.org/protocol/admin#shutdown", shut_down_service_handler, "admin");
-
-module:add_item("adhoc", add_user_desc);
-module:add_item("adhoc", announce_desc);
-module:add_item("adhoc", change_user_password_desc);
-module:add_item("adhoc", delete_user_desc);
-module:add_item("adhoc", end_user_session_desc);
-module:add_item("adhoc", get_user_password_desc);
-module:add_item("adhoc", get_user_roster_desc);
-module:add_item("adhoc", get_user_stats_desc);
-module:add_item("adhoc", get_online_users_desc);
-module:add_item("adhoc", shut_down_service_desc);
--- a/mod_adhoc_cmd_modules/mod_adhoc_cmd_modules.lua	Mon Jun 11 22:32:45 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,137 +0,0 @@
--- Copyright (C) 2009-2010 Florian Zeitz
---
--- This file is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-local _G = _G;
-
-local prosody = _G.prosody;
-local hosts = prosody.hosts;
-
-require "util.iterators";
-local dataforms_new = require "util.dataforms".new;
-local array = require "util.array";
-local modulemanager = require "modulemanager";
-local adhoc_new = module:require "adhoc".new;
-
-function list_modules_handler(self, data, state)
-	local result = dataforms_new {
-		title = "List of loaded modules";
-
-		{ name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#list" };
-		{ name = "modules", type = "text-multi", label = "The following modules are loaded:" };
-	};
-
-	local modules = array.collect(keys(hosts[data.to].modules)):sort():concat("\n");
-
-	return { status = "completed", result = { layout = result; values = { modules = modules } } };
-end
-
-function load_module_handler(self, data, state)
-	local layout = dataforms_new {
-		title = "Load module";
-		instructions = "Specify the module to be loaded";
-
-		{ name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#load" };
-		{ name = "module", type = "text-single", required = true, label = "Module to be loaded:"};
-	};
-	if state then
-		if data.action == "cancel" then
-			return { status = "canceled" };
-		end
-		local fields = layout:data(data.form);
-		if (not fields.module) or (fields.module == "") then
-			return { status = "completed", error = {
-				message = "Please specify a module."
-			} };
-		end
-		if modulemanager.is_loaded(data.to, fields.module) then
-			return { status = "completed", info = "Module already loaded" };
-		end
-		local ok, err = modulemanager.load(data.to, fields.module);
-		if ok then
-			return { status = "completed", info = 'Module "'..fields.module..'" successfully loaded on host "'..data.to..'".' };
-		else
-			return { status = "completed", error = { message = 'Failed to load module "'..fields.module..'" on host "'..data.to..
-			'". Error was: "'..tostring(err or "<unspecified>")..'"' } };
-		end
-	else
-		local modules = array.collect(keys(hosts[data.to].modules)):sort();
-		return { status = "executing", form = layout }, "executing";
-	end
-end
-
--- TODO: Allow reloading multiple modules (depends on list-multi)
-function reload_modules_handler(self, data, state)
-	local layout = dataforms_new {
-		title = "Reload module";
-		instructions = "Select the module to be reloaded";
-
-		{ name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#reload" };
-		{ name = "module", type = "list-single", required = true, label = "Module to be reloaded:"};
-	};
-	if state then
-		if data.action == "cancel" then
-			return { status = "canceled" };
-		end
-		local fields = layout:data(data.form);
-		if (not fields.module) or (fields.module == "") then
-			return { status = "completed", error = {
-				message = "Please specify a module. (This means your client misbehaved, as this field is required)"
-			} };
-		end
-		local ok, err = modulemanager.reload(data.to, fields.module);
-		if ok then
-			return { status = "completed", info = 'Module "'..fields.module..'" successfully reloaded on host "'..data.to..'".' };
-		else
-			return { status = "completed", error = { message = 'Failed to reload module "'..fields.module..'" on host "'..data.to..
-			'". Error was: "'..tostring(err)..'"' } };
-		end
-	else
-		local modules = array.collect(keys(hosts[data.to].modules)):sort();
-		return { status = "executing", form = { layout = layout; values = { module = modules } } }, "executing";
-	end
-end
-
--- TODO: Allow unloading multiple modules (depends on list-multi)
-function unload_modules_handler(self, data, state)
-	local layout = dataforms_new {
-		title = "Unload module";
-		instructions = "Select the module to be unloaded";
-
-		{ name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/modules#unload" };
-		{ name = "module", type = "list-single", required = true, label = "Module to be unloaded:"};
-	};
-	if state then
-		if data.action == "cancel" then
-			return { status = "canceled" };
-		end
-		local fields = layout:data(data.form);
-		if (not fields.module) or (fields.module == "") then
-			return { status = "completed", error = {
-				message = "Please specify a module. (This means your client misbehaved, as this field is required)"
-			} };
-		end
-		local ok, err = modulemanager.unload(data.to, fields.module);
-		if ok then
-			return { status = "completed", info = 'Module "'..fields.module..'" successfully unloaded on host "'..data.to..'".' };
-		else
-			return { status = "completed", error = { message = 'Failed to unload module "'..fields.module..'" on host "'..data.to..
-			'". Error was: "'..tostring(err)..'"' } };
-		end
-	else
-		local modules = array.collect(keys(hosts[data.to].modules)):sort();
-		return { status = "executing", form = { layout = layout; values = { module = modules } } }, "executing";
-	end
-end
-
-local list_modules_desc = adhoc_new("List loaded modules", "http://prosody.im/protocol/modules#list", list_modules_handler, "admin");
-local load_module_desc = adhoc_new("Load module", "http://prosody.im/protocol/modules#load", load_module_handler, "admin");
-local reload_modules_desc = adhoc_new("Reload module", "http://prosody.im/protocol/modules#reload", reload_modules_handler, "admin");
-local unload_modules_desc = adhoc_new("Unload module", "http://prosody.im/protocol/modules#unload", unload_modules_handler, "admin");
-
-module:add_item("adhoc", list_modules_desc);
-module:add_item("adhoc", load_module_desc);
-module:add_item("adhoc", reload_modules_desc);
-module:add_item("adhoc", unload_modules_desc);
--- a/mod_adhoc_cmd_ping/mod_adhoc_cmd_ping.lua	Mon Jun 11 22:32:45 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
--- Copyright (C) 2009 Thilo Cestonaro
--- 
--- This file is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-local st = require "util.stanza";
-local adhoc_new = module:require "adhoc".new;
-
-function ping_command_handler (self, data, state)
-	local now = os.date("%Y-%m-%dT%X");
-	return { info = "Pong\n"..now, status = "completed" };
-end
-
-local descriptor = adhoc_new("Ping", "ping", ping_command_handler);
-
-module:add_item ("adhoc", descriptor);
-
--- a/mod_adhoc_cmd_uptime/mod_adhoc_cmd_uptime.lua	Mon Jun 11 22:32:45 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
--- Copyright (C) 2009 Florian Zeitz
--- 
--- This file is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
-local _G = _G;
-local prosody = _G.prosody;
-local st = require "util.stanza";
-local adhoc_new = module:require "adhoc".new;
-
-function uptime()
-	local t = os.time()-prosody.start_time;
-	local seconds = t%60;
-	t = (t - seconds)/60;
-	local minutes = t%60;
-	t = (t - minutes)/60;
-	local hours = t%24;
-	t = (t - hours)/24;
-	local days = t;
-	return string.format("This server has been running for %d day%s, %d hour%s and %d minute%s (since %s)", 
-		days, (days ~= 1 and "s") or "", hours, (hours ~= 1 and "s") or "", 
-		minutes, (minutes ~= 1 and "s") or "", os.date("%c", prosody.start_time));
-end
-
-function uptime_command_handler (self, data, state)
-	return { info = uptime(), status = "completed" };
-end
-
-local descriptor = adhoc_new("Get uptime", "uptime", uptime_command_handler);
-
-module:add_item ("adhoc", descriptor);
-
--- a/mod_admin_web/admin_web/mod_admin_web.lua	Mon Jun 11 22:32:45 2012 +0200
+++ b/mod_admin_web/admin_web/mod_admin_web.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -21,7 +21,6 @@
 local uuid_generate = require "util.uuid".generate;
 local is_admin = require "core.usermanager".is_admin;
 local pubsub = require "util.pubsub";
-local httpserver = require "net.httpserver";
 local jid_bare = require "util.jid".bare;
 local lfs = require "lfs";
 local open = io.open;
@@ -31,17 +30,12 @@
 
 local service = {};
 
-local http_base = module.path:gsub("/[^/]+$","") .. "/www_files";
+local http_base = module.path:gsub("/[^/]+$","") .. "/www_files/";
 
 local xmlns_adminsub = "http://prosody.im/adminsub";
 local xmlns_c2s_session = "http://prosody.im/streams/c2s";
 local xmlns_s2s_session = "http://prosody.im/streams/s2s";
 
-local response_301 = { status = "301 Moved Permanently" };
-local response_400 = { status = "400 Bad Request", body = "<h1>Bad Request</h1>Sorry, we didn't understand your request :(" };
-local response_403 = { status = "403 Forbidden", body = "<h1>Forbidden</h1>You don't have permission to view the contents of this directory :(" };
-local response_404 = { status = "404 Not Found", body = "<h1>Page Not Found</h1>Sorry, we couldn't find what you were looking for :(" };
-
 local mime_map = {
 	html = "text/html";
 	xml = "text/xml";
@@ -110,238 +104,222 @@
 	end
 end
 
-local function preprocess_path(path)
-	if path:sub(1,1) ~= "/" then
-		path = "/"..path;
-	end
-	local level = 0;
-	for component in path:gmatch("([^/]+)/") do
-		if component == ".." then
-			level = level - 1;
-		elseif component ~= "." then
-			level = level + 1;
+function serve_file(event, path)
+	local full_path = http_base .. path;
+
+	if stat(full_path, "mode") == "directory" then
+		if stat(full_path.."/index.html", "mode") == "file" then
+			return serve_file(event, path.."/index.html");
 		end
-		if level < 0 then
-			return nil;
-		end
+		return 403;
 	end
-	return path;
-end
 
-function serve_file(path, base)
-	local full_path = http_base..path;
-	if stat(full_path, "mode") == "directory" then
-		if not path:find("/$") then
-			local response = response_301;
-			response.headers = { ["Location"] = base .. "/" };
-			return response;
-		end
-		if stat(full_path.."/index.html", "mode") == "file" then
-			return serve_file(path.."/index.html");
-		end
-		return response_403;
+	local f, err = open(full_path, "rb");
+	if not f then
+		return 404;
 	end
-	local f, err = open(full_path, "rb");
-	if not f then return response_404; end
+
 	local data = f:read("*a");
 	f:close();
 	if not data then
-		return response_403;
+		return 403;
 	end
-	local ext = path:match("%.([^.]*)$");
-	local mime = mime_map[ext]; -- Content-Type should be nil when not known
-	return {
-		headers = { ["Content-Type"] = mime; };
-		body = data;
-	};
-end
 
-local function handle_file_request(method, body, request)
-	local path = preprocess_path(request.url.path);
-	if not path then return response_400; end
-	path_stripped = path:gsub("^/[^/]+", ""); -- Strip /admin/
-	return serve_file(path_stripped, path);
-end
-
-function module.load()
-	local http_conf = config.get("*", "core", "webadmin_http_ports");
-
-	httpserver.new_from_config(http_conf, handle_file_request, { base = "admin" });
+	local ext = path:match("%.([^.]*)$");
+	event.response.headers.content_type = mime_map[ext]; -- Content-Type should be nil when not known
+	return data;
 end
 
-prosody.events.add_handler("server-started", function ()
-	for host_name, host_table in pairs(hosts) do
-		service[host_name] = pubsub.new({
-			broadcaster = function(node, jids, item) return simple_broadcast(node, jids, item, host_name) end;
-			normalize_jid = jid_bare;
-			get_affiliation = function(jid) return get_affiliation(jid, host_name) end;
-			capabilities = {
-				member = {
-					create = false;
-					publish = false;
-					retract = false;
-					get_nodes = true;
+function module.add_host(module)
+	-- Setup HTTP server
+	module:depends("http");
+	module:provides("http", {
+		name = "admin";
+		route = {
+			["GET"] = function(event)
+				event.response.headers.location = event.request.path .. "/";
+				return 301;
+			end;
+			["GET /*"] = serve_file;
+		}
+	});
+
+	-- Setup adminsub service
+	local ok, err;
+	service[module.host] = pubsub.new({
+		broadcaster = function(node, jids, item) return simple_broadcast(node, jids, item, module.host) end;
+		normalize_jid = jid_bare;
+		get_affiliation = function(jid) return get_affiliation(jid, module.host) end;
+		capabilities = {
+			member = {
+				create = false;
+				publish = false;
+				retract = false;
+				get_nodes = true;
 
-					subscribe = true;
-					unsubscribe = true;
-					get_subscription = true;
-					get_subscriptions = true;
-					get_items = true;
+				subscribe = true;
+				unsubscribe = true;
+				get_subscription = true;
+				get_subscriptions = true;
+				get_items = true;
+
+				subscribe_other = false;
+				unsubscribe_other = false;
+				get_subscription_other = false;
+				get_subscriptions_other = false;
+
+				be_subscribed = true;
+				be_unsubscribed = true;
 
-					subscribe_other = false;
-					unsubscribe_other = false;
-					get_subscription_other = false;
-					get_subscriptions_other = false;
+				set_affiliation = false;
+			};
 
-					be_subscribed = true;
-					be_unsubscribed = true;
+			owner = {
+				create = true;
+				publish = true;
+				retract = true;
+				get_nodes = true;
 
-					set_affiliation = false;
-				};
+				subscribe = true;
+				unsubscribe = true;
+				get_subscription = true;
+				get_subscriptions = true;
+				get_items = true;
 
-				owner = {
-					create = true;
-					publish = true;
-					retract = true;
-					get_nodes = true;
+				subscribe_other = true;
+				unsubscribe_other = true;
+				get_subscription_other = true;
+				get_subscriptions_other = true;
+
+				be_subscribed = true;
+				be_unsubscribed = true;
+
+				set_affiliation = true;
+			};
+		};
+	});
 
-					subscribe = true;
-					unsubscribe = true;
-					get_subscription = true;
-					get_subscriptions = true;
-					get_items = true;
+	-- Create node for s2s sessions
+	ok, err = service[module.host]:create(xmlns_s2s_session, true);
+	if not ok then
+		module:log("warn", "Could not create node " .. xmlns_s2s_session .. ": " .. tostring(err));
+	else
+		service[module.host]:set_affiliation(xmlns_s2s_session, true, module.host, "owner")
+	end
 
-					subscribe_other = true;
-					unsubscribe_other = true;
-					get_subscription_other = true;
-					get_subscriptions_other = true;
+	-- Add outgoing s2s sessions 
+	for remotehost, session in pairs(hosts[module.host].s2sout) do
+		if session.type ~= "s2sout_unauthed" then
+			add_host(session, "out", module.host);
+		end
+	end
 
-					be_subscribed = true;
-					be_unsubscribed = true;
+	-- Add incomming s2s sessions 
+	for session in pairs(incoming_s2s) do
+		if session.to_host == module.host then
+			add_host(session, "in", module.host);
+		end
+	end
 
-					set_affiliation = true;
-				};
-			};
-		});
+	-- Create node for c2s sessions
+	ok, err = service[module.host]:create(xmlns_c2s_session, true);
+	if not ok then
+		module:log("warn", "Could not create node " .. xmlns_c2s_session .. ": " .. tostring(err));
+	else
+		service[module.host]:set_affiliation(xmlns_c2s_session, true, module.host, "owner")
+	end
 
-		if not select(2, service[host_name]:get_nodes(true))[xmlns_s2s_session] then
-			local ok, errmsg = service[host_name]:create(xmlns_s2s_session, true);
-			if not ok then
-				module:log("warn", "Could not create node " .. xmlns_s2s_session .. ": " .. tostring(errmsg));
+	-- Add c2s sessions
+	for username, user in pairs(hosts[module.host].sessions or {}) do
+		for resource, session in pairs(user.sessions or {}) do
+			add_client(session, module.host);
+		end
+	end
+
+	-- Register adminsub handler
+	module:hook("iq/host/http://prosody.im/adminsub:adminsub", function(event)
+		local origin, stanza = event.origin, event.stanza;
+		local adminsub = stanza.tags[1];
+		local action = adminsub.tags[1];
+		local reply;
+		if action.name == "subscribe" then
+			local ok, ret = service[module.host]:add_subscription(action.attr.node, stanza.attr.from, stanza.attr.from);
+			if ok then
+				reply = st.reply(stanza)
+					:tag("adminsub", { xmlns = xmlns_adminsub });
 			else
-				service[host_name]:set_affiliation(xmlns_s2s_session, true, host_name, "owner")
-			end
-		end
-
-		for remotehost, session in pairs(host_table.s2sout) do
-			if session.type ~= "s2sout_unauthed" then
-				add_host(session, "out", host_name);
+				reply = st.error_reply(stanza, "cancel", ret);
 			end
-		end
-		for session in pairs(incoming_s2s) do
-			if session.to_host == host_name then
-				add_host(session, "in", host_name);
+		elseif action.name == "unsubscribe" then
+			local ok, ret = service[module.host]:remove_subscription(action.attr.node, stanza.attr.from, stanza.attr.from);
+			if ok then
+				reply = st.reply(stanza)
+					:tag("adminsub", { xmlns = xmlns_adminsub });
+			else
+				reply = st.error_reply(stanza, "cancel", ret);
 			end
-		end
-
-		if not select(2, service[host_name]:get_nodes(true))[xmlns_c2s_session] then
-			local ok, errmsg = service[host_name]:create(xmlns_c2s_session, true);
+		elseif action.name == "items" then
+			local node = action.attr.node;
+			local ok, ret = service[module.host]:get_items(node, stanza.attr.from);
 			if not ok then
-				module:log("warn", "Could not create node " .. xmlns_c2s_session .. ": " .. tostring(errmsg));
-			else
-				service[host_name]:set_affiliation(xmlns_c2s_session, true, host_name, "owner")
+				return origin.send(st.error_reply(stanza, "cancel", ret));
 			end
-		end
-
-		for username, user in pairs(host_table.sessions or {}) do
-			for resource, session in pairs(user.sessions or {}) do
-				add_client(session, host_name);
-			end
-		end
 
-		host_table.events.add_handler("iq/host/http://prosody.im/adminsub:adminsub", function(event)
-			local origin, stanza = event.origin, event.stanza;
-			local adminsub = stanza.tags[1];
-			local action = adminsub.tags[1];
-			local reply;
-			if action.name == "subscribe" then
-				local ok, ret = service[host_name]:add_subscription(action.attr.node, stanza.attr.from, stanza.attr.from);
-				if ok then
-					reply = st.reply(stanza)
-						:tag("adminsub", { xmlns = xmlns_adminsub });
-				else
-					reply = st.error_reply(stanza, "cancel", ret);
-				end
-			elseif action.name == "unsubscribe" then
-				local ok, ret = service[host_name]:remove_subscription(action.attr.node, stanza.attr.from, stanza.attr.from);
-				if ok then
-					reply = st.reply(stanza)
-						:tag("adminsub", { xmlns = xmlns_adminsub });
-				else
-					reply = st.error_reply(stanza, "cancel", ret);
-				end
-			elseif action.name == "items" then
-				local node = action.attr.node;
-				local ok, ret = service[host_name]:get_items(node, stanza.attr.from);
-				if not ok then
-					return origin.send(st.error_reply(stanza, "cancel", ret));
-				end
-
-				local data = st.stanza("items", { node = node });
-				for _, entry in pairs(ret) do
-					data:add_child(entry);
-				end
-				if data then
-					reply = st.reply(stanza)
-						:tag("adminsub", { xmlns = xmlns_adminsub })
-							:add_child(data);
-				else
-					reply = st.error_reply(stanza, "cancel", "item-not-found");
-				end
-			elseif action.name == "adminfor" then
-				local data = st.stanza("adminfor");
-				for host_name in pairs(hosts) do
-					if is_admin(stanza.attr.from, host_name) then
-						data:tag("item"):text(host_name):up();
-					end
-				end
+			local data = st.stanza("items", { node = node });
+			for _, entry in pairs(ret) do
+				data:add_child(entry);
+			end
+			if data then
 				reply = st.reply(stanza)
 					:tag("adminsub", { xmlns = xmlns_adminsub })
 						:add_child(data);
 			else
-				reply = st.error_reply(stanza, "feature-not-implemented");
+				reply = st.error_reply(stanza, "cancel", "item-not-found");
+			end
+		elseif action.name == "adminfor" then
+			local data = st.stanza("adminfor");
+			for host_name in pairs(hosts) do
+				if is_admin(stanza.attr.from, host_name) then
+					data:tag("item"):text(host_name):up();
+				end
 			end
-			return origin.send(reply);
-		end);
+			reply = st.reply(stanza)
+				:tag("adminsub", { xmlns = xmlns_adminsub })
+					:add_child(data);
+		else
+			reply = st.error_reply(stanza, "feature-not-implemented");
+		end
+		return origin.send(reply);
+	end);
 
-		host_table.events.add_handler("resource-bind", function(event)
-			add_client(event.session, host_name);
-		end);
-
-		host_table.events.add_handler("resource-unbind", function(event)
-			del_client(event.session, host_name);
-			service[host_name]:remove_subscription(xmlns_c2s_session, host_name, event.session.full_jid);
-			service[host_name]:remove_subscription(xmlns_s2s_session, host_name, event.session.full_jid);
-		end);
+	-- Add/remove c2s sessions
+	module:hook("resource-bind", function(event)
+		add_client(event.session, module.host);
+	end);
 
-		host_table.events.add_handler("s2sout-established", function(event)
-			add_host(event.session, "out", host_name);
-		end);
+	module:hook("resource-unbind", function(event)
+		del_client(event.session, module.host);
+		service[module.host]:remove_subscription(xmlns_c2s_session, module.host, event.session.full_jid);
+		service[module.host]:remove_subscription(xmlns_s2s_session, module.host, event.session.full_jid);
+	end);
 
-		host_table.events.add_handler("s2sin-established", function(event)
-			add_host(event.session, "in", host_name);
-		end);
+	-- Add/remove s2s sessions
+	module:hook("s2sout-established", function(event)
+		add_host(event.session, "out", module.host);
+	end);
 
-		host_table.events.add_handler("s2sout-destroyed", function(event)
-			del_host(event.session, "out", host_name);
-		end);
+	module:hook("s2sin-established", function(event)
+		add_host(event.session, "in", module.host);
+	end);
 
-		host_table.events.add_handler("s2sin-destroyed", function(event)
-			del_host(event.session, "in", host_name);
-		end);
+	module:hook("s2sout-destroyed", function(event)
+		del_host(event.session, "out", module.host);
+	end);
 
-	end
-end);
+	module:hook("s2sin-destroyed", function(event)
+		del_host(event.session, "in", module.host);
+	end);
+end
 
 function simple_broadcast(node, jids, item, host)
 	item = st.clone(item);
--- a/mod_admin_web/admin_web/mod_admin_web_timber.lua	Mon Jun 11 22:32:45 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,345 +0,0 @@
--- Copyright (C) 2010 Florian Zeitz
---
--- This file is MIT/X11 licensed. Please see the
--- COPYING file in the source package for more information.
---
-
--- <session xmlns="http://prosody.im/streams/c2s" jid="alice@example.com/brussels">
---   <encrypted/>
---   <compressed/>
--- </session>
-
--- <session xmlns="http://prosody.im/streams/s2s" jid="example.com">
---   <encrypted>
---     <valid/> / <invalid/>
---   </encrypted>
---   <compressed/>
---   <in/> / <out/>
--- </session>
-
-local st = require "util.stanza";
-local uuid_generate = require "util.uuid".generate;
-local is_admin = require "core.usermanager".is_admin;
-local pubsub = require "util.pubsub";
-local jid_bare = require "util.jid".bare;
-local lfs = require "lfs";
-local open = io.open;
-local stat = lfs.attributes;
-
-module:set_global();
-
-local service = {};
-
-local http_base = module.path:gsub("/[^/]+$","") .. "/www_files/";
-
-local xmlns_adminsub = "http://prosody.im/adminsub";
-local xmlns_c2s_session = "http://prosody.im/streams/c2s";
-local xmlns_s2s_session = "http://prosody.im/streams/s2s";
-
-local mime_map = {
-	html = "text/html";
-	xml = "text/xml";
-	js = "text/javascript";
-	css = "text/css";
-};
-
-local idmap = {};
-
-function add_client(session, host)
-	local name = session.full_jid;
-	local id = idmap[name];
-	if not id then
-		id = uuid_generate();
-		idmap[name] = id;
-	end
-	local item = st.stanza("item", { id = id }):tag("session", {xmlns = xmlns_c2s_session, jid = name}):up();
-	if session.secure then
-		item:tag("encrypted"):up();
-	end
-	if session.compressed then
-		item:tag("compressed"):up();
-	end
-	service[host]:publish(xmlns_c2s_session, host, id, item);
-	module:log("debug", "Added client " .. name);
-end
-
-function del_client(session, host)
-	local name = session.full_jid;
-	local id = idmap[name];
-	if id then
-		local notifier = st.stanza("retract", { id = id });
-		service[host]:retract(xmlns_c2s_session, host, id, notifier);
-	end
-end
-
-function add_host(session, type, host)
-	local name = (type == "out" and session.to_host) or (type == "in" and session.from_host);
-	local id = idmap[name.."_"..type];
-	if not id then
-		id = uuid_generate();
-		idmap[name.."_"..type] = id;
-	end
-	local item = st.stanza("item", { id = id }):tag("session", {xmlns = xmlns_s2s_session, jid = name})
-		:tag(type):up();
-	if session.secure then
-		if session.cert_identity_status == "valid" then
-			item:tag("encrypted"):tag("valid"):up():up();
-		else
-			item:tag("encrypted"):tag("invalid"):up():up();
-		end
-	end
-	if session.compressed then
-		item:tag("compressed"):up();
-	end
-	service[host]:publish(xmlns_s2s_session, host, id, item);
-	module:log("debug", "Added host " .. name .. " s2s" .. type);
-end
-
-function del_host(session, type, host)
-	local name = (type == "out" and session.to_host) or (type == "in" and session.from_host);
-	local id = idmap[name.."_"..type];
-	if id then
-		local notifier = st.stanza("retract", { id = id });
-		service[host]:retract(xmlns_s2s_session, host, id, notifier);
-	end
-end
-
-function serve_file(event, path)
-	local full_path = http_base .. path;
-
-	if stat(full_path, "mode") == "directory" then
-		if stat(full_path.."/index.html", "mode") == "file" then
-			return serve_file(event, path.."/index.html");
-		end
-		return 403;
-	end
-
-	local f, err = open(full_path, "rb");
-	if not f then
-		return 404;
-	end
-
-	local data = f:read("*a");
-	f:close();
-	if not data then
-		return 403;
-	end
-
-	local ext = path:match("%.([^.]*)$");
-	event.response.headers.content_type = mime_map[ext]; -- Content-Type should be nil when not known
-	return data;
-end
-
-function module.add_host(module)
-	-- Setup HTTP server
-	module:depends("http");
-	module:provides("http", {
-		name = "admin";
-		route = {
-			["GET"] = function(event)
-				event.response.headers.location = event.request.path .. "/";
-				return 301;
-			end;
-			["GET /*"] = serve_file;
-		}
-	});
-
-	-- Setup adminsub service
-	local ok, err;
-	service[module.host] = pubsub.new({
-		broadcaster = function(node, jids, item) return simple_broadcast(node, jids, item, module.host) end;
-		normalize_jid = jid_bare;
-		get_affiliation = function(jid) return get_affiliation(jid, module.host) end;
-		capabilities = {
-			member = {
-				create = false;
-				publish = false;
-				retract = false;
-				get_nodes = true;
-
-				subscribe = true;
-				unsubscribe = true;
-				get_subscription = true;
-				get_subscriptions = true;
-				get_items = true;
-
-				subscribe_other = false;
-				unsubscribe_other = false;
-				get_subscription_other = false;
-				get_subscriptions_other = false;
-
-				be_subscribed = true;
-				be_unsubscribed = true;
-
-				set_affiliation = false;
-			};
-
-			owner = {
-				create = true;
-				publish = true;
-				retract = true;
-				get_nodes = true;
-
-				subscribe = true;
-				unsubscribe = true;
-				get_subscription = true;
-				get_subscriptions = true;
-				get_items = true;
-
-				subscribe_other = true;
-				unsubscribe_other = true;
-				get_subscription_other = true;
-				get_subscriptions_other = true;
-
-				be_subscribed = true;
-				be_unsubscribed = true;
-
-				set_affiliation = true;
-			};
-		};
-	});
-
-	-- Create node for s2s sessions
-	ok, err = service[module.host]:create(xmlns_s2s_session, true);
-	if not ok then
-		module:log("warn", "Could not create node " .. xmlns_s2s_session .. ": " .. tostring(err));
-	else
-		service[module.host]:set_affiliation(xmlns_s2s_session, true, module.host, "owner")
-	end
-
-	-- Add outgoing s2s sessions 
-	for remotehost, session in pairs(hosts[module.host].s2sout) do
-		if session.type ~= "s2sout_unauthed" then
-			add_host(session, "out", module.host);
-		end
-	end
-
-	-- Add incomming s2s sessions 
-	for session in pairs(incoming_s2s) do
-		if session.to_host == module.host then
-			add_host(session, "in", module.host);
-		end
-	end
-
-	-- Create node for c2s sessions
-	ok, err = service[module.host]:create(xmlns_c2s_session, true);
-	if not ok then
-		module:log("warn", "Could not create node " .. xmlns_c2s_session .. ": " .. tostring(err));
-	else
-		service[module.host]:set_affiliation(xmlns_c2s_session, true, module.host, "owner")
-	end
-
-	-- Add c2s sessions
-	for username, user in pairs(hosts[module.host].sessions or {}) do
-		for resource, session in pairs(user.sessions or {}) do
-			add_client(session, module.host);
-		end
-	end
-
-	-- Register adminsub handler
-	module:hook("iq/host/http://prosody.im/adminsub:adminsub", function(event)
-		local origin, stanza = event.origin, event.stanza;
-		local adminsub = stanza.tags[1];
-		local action = adminsub.tags[1];
-		local reply;
-		if action.name == "subscribe" then
-			local ok, ret = service[module.host]:add_subscription(action.attr.node, stanza.attr.from, stanza.attr.from);
-			if ok then
-				reply = st.reply(stanza)
-					:tag("adminsub", { xmlns = xmlns_adminsub });
-			else
-				reply = st.error_reply(stanza, "cancel", ret);
-			end
-		elseif action.name == "unsubscribe" then
-			local ok, ret = service[module.host]:remove_subscription(action.attr.node, stanza.attr.from, stanza.attr.from);
-			if ok then
-				reply = st.reply(stanza)
-					:tag("adminsub", { xmlns = xmlns_adminsub });
-			else
-				reply = st.error_reply(stanza, "cancel", ret);
-			end
-		elseif action.name == "items" then
-			local node = action.attr.node;
-			local ok, ret = service[module.host]:get_items(node, stanza.attr.from);
-			if not ok then
-				return origin.send(st.error_reply(stanza, "cancel", ret));
-			end
-
-			local data = st.stanza("items", { node = node });
-			for _, entry in pairs(ret) do
-				data:add_child(entry);
-			end
-			if data then
-				reply = st.reply(stanza)
-					:tag("adminsub", { xmlns = xmlns_adminsub })
-						:add_child(data);
-			else
-				reply = st.error_reply(stanza, "cancel", "item-not-found");
-			end
-		elseif action.name == "adminfor" then
-			local data = st.stanza("adminfor");
-			for host_name in pairs(hosts) do
-				if is_admin(stanza.attr.from, host_name) then
-					data:tag("item"):text(host_name):up();
-				end
-			end
-			reply = st.reply(stanza)
-				:tag("adminsub", { xmlns = xmlns_adminsub })
-					:add_child(data);
-		else
-			reply = st.error_reply(stanza, "feature-not-implemented");
-		end
-		return origin.send(reply);
-	end);
-
-	-- Add/remove c2s sessions
-	module:hook("resource-bind", function(event)
-		add_client(event.session, module.host);
-	end);
-
-	module:hook("resource-unbind", function(event)
-		del_client(event.session, module.host);
-		service[module.host]:remove_subscription(xmlns_c2s_session, module.host, event.session.full_jid);
-		service[module.host]:remove_subscription(xmlns_s2s_session, module.host, event.session.full_jid);
-	end);
-
-	-- Add/remove s2s sessions
-	module:hook("s2sout-established", function(event)
-		add_host(event.session, "out", module.host);
-	end);
-
-	module:hook("s2sin-established", function(event)
-		add_host(event.session, "in", module.host);
-	end);
-
-	module:hook("s2sout-destroyed", function(event)
-		del_host(event.session, "out", module.host);
-	end);
-
-	module:hook("s2sin-destroyed", function(event)
-		del_host(event.session, "in", module.host);
-	end);
-end
-
-function simple_broadcast(node, jids, item, host)
-	item = st.clone(item);
-	item.attr.xmlns = nil; -- Clear the pubsub namespace
-	local message = st.message({ from = host, type = "headline" })
-		:tag("event", { xmlns = xmlns_adminsub .. "#event" })
-			:tag("items", { node = node })
-				:add_child(item);
-	for jid in pairs(jids) do
-		module:log("debug", "Sending notification to %s", jid);
-		message.attr.to = jid;
-		core_post_stanza(hosts[host], message);
-	end
-end
-
-function get_affiliation(jid, host)
-	local bare_jid = jid_bare(jid);
-	if is_admin(bare_jid, host) then
-		return "member";
-	else
-		return "none";
-	end
-end
--- a/mod_archive/mod_archive.lua	Mon Jun 11 22:32:45 2012 +0200
+++ b/mod_archive/mod_archive.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -37,16 +37,8 @@
     dm.store(node, host, PREFS_DIR, st.preserialize(data));
 end
 
-local function os_date()
-    return os.date("!*t");
-end
-
 local date_time = datetime.datetime;
 
-local function date_format(s)
-	return os.date("%Y-%m-%dT%H:%M:%SZ", s);
-end
-
 local function date_parse(s)
 	local year, month, day, hour, min, sec = s:match("(....)-?(..)-?(..)T(..):(..):(..)Z");
 	return os.time({year=year, month=month, day=day, hour=hour, min=min, sec=sec});
@@ -95,10 +87,9 @@
     local thread = msg:child_with_name("thread");
 	local data = dm.list_load(node, host, ARCHIVE_DIR);
     local tag = isfrom and "from" or "to";
-    local with = isfrom and msg.attr.to or msg.attr.from;
-    local utc = os_date();
-    local utc_secs = os.time(utc);
-    local utc_datetime = date_format(utc_secs);
+    local with = isfrom and msg.attr.from or msg.attr.to;
+    local utc_datetime = date_time();
+    local utc_secs = date_parse(utc_datetime);
     if data then
         -- The collection list are in REVERSE chronological order 
         for k, v in ipairs(data) do
@@ -761,25 +752,29 @@
     return AUTO_ARCHIVING_ENABLED;
 end
 
-local function msg_handler(data)
+local function msg_handler(data, local_jid, other_jid, isfrom)
     module:log("debug", "-- Enter msg_handler()");
     local origin, stanza = data.origin, data.stanza;
     local body = stanza:child_with_name("body");
     local thread = stanza:child_with_name("thread");
     if body then
-        local from_node, from_host = jid.split(stanza.attr.from);
-        local to_node, to_host = jid.split(stanza.attr.to);
-        if hosts[from_host] and um.user_exists(from_node, from_host) and apply_pref(from_node, from_host, stanza.attr.to, thread) then
-            store_msg(stanza, from_node, from_host, true);
-        end
-        if hosts[to_host] and um.user_exists(to_node, to_host) and apply_pref(to_node, to_host, stanza.attr.from, thread) then
-            store_msg(stanza, to_node, to_host, false);
+        local local_node, local_host = jid.split(local_jid);
+        if hosts[local_host] and um.user_exists(local_node, local_host) and apply_pref(local_node, local_host, other_jid, thread) then
+            store_msg(stanza, local_node, local_host, isfrom);
         end
     end
 
     return nil;
 end
 
+local function message_handler(data)
+    msg_handler(data, data.stanza.attr.to,  data.stanza.attr.from, true)
+end
+
+local function premessage_handler(data)
+    msg_handler(data, data.stanza.attr.from,  data.stanza.attr.to, false)
+end
+
 -- Preferences
 module:hook("iq/self/urn:xmpp:archive:pref", preferences_handler);
 module:hook("iq/self/urn:xmpp:archive:itemremove", itemremove_handler);
@@ -794,10 +789,10 @@
 -- Replication
 module:hook("iq/self/urn:xmpp:archive:modified", modified_handler);
 
-module:hook("message/full", msg_handler, 10);
-module:hook("message/bare", msg_handler, 10);
-module:hook("pre-message/full", msg_handler, 10);
-module:hook("pre-message/bare", msg_handler, 10);
+module:hook("message/full", message_handler, 10);
+module:hook("message/bare", message_handler, 10);
+module:hook("pre-message/full", premessage_handler, 10);
+module:hook("pre-message/bare", premessage_handler, 10);
 
 -- TODO exactmatch
 -- TODO <item/> JID match
--- a/mod_auth_joomla/mod_auth_joomla.lua	Mon Jun 11 22:32:45 2012 +0200
+++ b/mod_auth_joomla/mod_auth_joomla.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -12,6 +12,7 @@
 
 local connection;
 local params = module:get_option("sql");
+local prefix = params and params.prefix or "jos_";
 
 local resolve_relative_path = require "core.configmanager".resolve_relative_path;
 
@@ -79,7 +80,7 @@
 end
 
 local function get_password(username)
-	local stmt, err = getsql("SELECT `password` FROM `jos_users` WHERE `username`=?", username);
+	local stmt, err = getsql("SELECT `password` FROM `"..prefix.."users` WHERE `username`=?", username);
 	if stmt then
 		for row in stmt:rows(true) do
 			return row.password;
@@ -89,7 +90,8 @@
 
 
 local function getCryptedPassword(plaintext, salt)
-	return md5(plaintext..salt);
+	local salted = plaintext..salt;
+	return md5(salted, true);
 end
 local function joomlaCheckHash(password, hash)
 	local crypt, salt = hash:match("^([^:]*):(.*)$");
@@ -118,7 +120,7 @@
 end
 function provider.set_password(username, password)
 	local hash = joomlaCreateHash(password);
-	local stmt, err = setsql("UPDATE `jos_users` SET `password`=? WHERE `username`=?", hash, username);
+	local stmt, err = setsql("UPDATE `"..prefix.."users` SET `password`=? WHERE `username`=?", hash, username);
 	return stmt and true, err;
 end
 function provider.create_user(username, password)
--- a/mod_carbons/mod_carbons.lua	Mon Jun 11 22:32:45 2012 +0200
+++ b/mod_carbons/mod_carbons.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -81,7 +81,7 @@
 	end
 
 	local msg = st.clone(stanza);
-	msg.attr.xmlns = msg.attr.xmlns or "jabber:client";
+	msg.attr.xmlns = "jabber:client";
 	local fwd = st.message{ from = bare_jid, type = orig_type, }
 		:tag(c2s and "sent" or "received", { xmlns = xmlns_carbons }):up()
 			:tag("forwarded", { xmlns = xmlns_forward })
--- a/mod_client_certs/mod_client_certs.lua	Mon Jun 11 22:32:45 2012 +0200
+++ b/mod_client_certs/mod_client_certs.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -17,34 +17,6 @@
 local digest_algo = "sha1";
 local base64 = require "util.encodings".base64;
 
-local function enable_cert(username, cert, info)
-	local certs = dm_load(username, module.host, dm_table) or {};
-
-	info.pem = cert:pem();
-	local digest = cert:digest(digest_algo);
-	info.digest = digest;
-	certs[info.id] = info;
-
-	dm_store(username, module.host, dm_table, certs);
-	return true
-end
-
-local function disable_cert(username, name)
-	local certs = dm_load(username, module.host, dm_table) or {};
-
-	local info = certs[name];
-	local cert;
-	if info then
-		certs[name] = nil;
-		cert = x509.cert_from_pem(info.pem);
-	else
-		return nil, "item-not-found"
-	end
-
-	dm_store(username, module.host, dm_table, certs);
-	return cert; -- So we can compare it with stuff
-end
-
 local function get_id_on_xmpp_addrs(cert)
 	local id_on_xmppAddrs = {};
 	for k,ext in pairs(cert:extensions()) do
@@ -61,7 +33,81 @@
 	module:log("debug", "Found JIDs: (%d) %s", #id_on_xmppAddrs, table.concat(id_on_xmppAddrs, ", "));
 	return id_on_xmppAddrs;
 end
-	
+
+local function enable_cert(username, cert, info)
+	-- Check the certificate. Is it not expired? Does it include id-on-xmppAddr?
+
+	--[[ the method expired doesn't exist in luasec .. yet?
+	if cert:expired() then
+	module:log("debug", "This certificate is already expired.");
+	return nil, "This certificate is expired.";
+	end
+	--]]
+
+	if not cert:valid_at(os.time()) then
+		module:log("debug", "This certificate is not valid at this moment.");
+	end
+
+	local valid_id_on_xmppAddrs;
+	local require_id_on_xmppAddr = true;
+	if require_id_on_xmppAddr then
+		valid_id_on_xmppAddrs = get_id_on_xmpp_addrs(cert);
+
+		local found = false;
+		for i,k in pairs(valid_id_on_xmppAddrs) do
+			if jid_bare(k) == (username .. "@" .. module.host) then
+				found = true;
+				break;
+			end
+		end
+
+		if not found then
+			return nil, "This certificate is has no valid id-on-xmppAddr field.";
+		end
+	end
+
+	local certs = dm_load(username, module.host, dm_table) or {};
+
+	info.pem = cert:pem();
+	local digest = cert:digest(digest_algo);
+	info.digest = digest;
+	certs[info.id] = info;
+
+	dm_store(username, module.host, dm_table, certs);
+	return true
+end
+
+local function disable_cert(username, name, disconnect)
+	local certs = dm_load(username, module.host, dm_table) or {};
+
+	local info = certs[name];
+
+	if not info then
+		return nil, "item-not-found"
+	end
+
+	certs[name] = nil;
+
+	if disconnect then
+		module:log("debug", "%s revoked a certificate! Disconnecting all clients that used it", username);
+		local sessions = hosts[module.host].sessions[username].sessions;
+		local disabled_cert_pem = info.pem;
+
+		for _, session in pairs(sessions) do
+			if session and session.conn then
+				local cert = session.conn:socket():getpeercertificate();
+
+				if cert and cert:pem() == disabled_cert_pem then
+					module:log("debug", "Found a session that should be closed: %s", tostring(session));
+					session:close{ condition = "not-authorized", text = "This client side certificate has been revoked."};
+				end
+			end
+		end
+	end
+
+	dm_store(username, module.host, dm_table, certs);
+	return info;
+end
 
 module:hook("iq/self/"..xmlns_saslcert..":items", function(event)
 	local origin, stanza = event.origin, event.stanza;
@@ -106,7 +152,7 @@
 		end
 
 		local can_manage = key_info:get_child("no-cert-management", xmlns_saslcert) ~= nil;
-		local x509cert = key_info:get_child_text("x509cert");
+		local x509cert = key_info:get_child_text("x509cert"):gsub("^%s*(.-)%s*$", "%1");
 
 		local cert = x509.cert_from_pem(
 		"-----BEGIN CERTIFICATE-----\n"
@@ -119,46 +165,18 @@
 			return true;
 		end
 
-		-- Check the certificate. Is it not expired? Does it include id-on-xmppAddr?
-
-		--[[ the method expired doesn't exist in luasec .. yet?
-		if cert:expired() then
-			module:log("debug", "This certificate is already expired.");
-			origin.send(st.error_reply(stanza, "cancel", "bad-request", "This certificate is expired."));
-			return true
-		end
-		--]]
-
-		if not cert:valid_at(os.time()) then
-			module:log("debug", "This certificate is not valid at this moment.");
-		end
-
-		local valid_id_on_xmppAddrs;
-		local require_id_on_xmppAddr = true;
-		if require_id_on_xmppAddr then
-			valid_id_on_xmppAddrs = get_id_on_xmpp_addrs(cert);
-
-			local found = false;
-			for i,k in pairs(valid_id_on_xmppAddrs) do
-				if jid_bare(k) == jid_bare(origin.full_jid) then
-					found = true;
-					break;
-				end
-			end
-
-			if not found then
-				origin.send(st.error_reply(stanza, "cancel", "bad-request", "This certificate is has no valid id-on-xmppAddr field."));
-				return true -- REJECT?!
-			end
-		end
-
-		enable_cert(origin.username, cert, {
+		local ok, err = enable_cert(origin.username, cert, {
 			id = id,
 			name = name,
 			x509cert = x509cert,
 			no_cert_management = can_manage,
 		});
 
+		if not ok then
+			origin.send(st.error_reply(stanza, "cancel", "bad-request", err));
+			return true -- REJECT?!
+		end
+
 		module:log("debug", "%s added certificate named %s", origin.full_jid, name);
 
 		origin.send(st.reply(stanza));
@@ -182,24 +200,8 @@
 			return true
 		end
 
-		local disabled_cert = disable_cert(origin.username, name);
-
-		if disabled_cert and disable.name == "revoke" then
-			module:log("debug", "%s revoked a certificate! Disconnecting all clients that used it", origin.full_jid);
-			local sessions = hosts[module.host].sessions[origin.username].sessions;
-			local disabled_cert_pem = disabled_cert:pem();
+		disable_cert(origin.username, name, disable.name == "revoke");
 
-			for _, session in pairs(sessions) do
-				if session and session.conn then
-					local cert = session.conn:socket():getpeercertificate();
-				
-					if cert and cert:pem() == disabled_cert_pem then
-						module:log("debug", "Found a session that should be closed: %s", tostring(session));
-						session:close{ condition = "not-authorized", text = "This client side certificate has been revoked."};
-					end
-				end
-			end
-		end
 		origin.send(st.reply(stanza));
 
 		return true
@@ -209,6 +211,151 @@
 module:hook("iq/self/"..xmlns_saslcert..":disable", handle_disable);
 module:hook("iq/self/"..xmlns_saslcert..":revoke", handle_disable);
 
+-- Ad-hoc command
+local adhoc_new = module:require "adhoc".new;
+local dataforms_new = require "util.dataforms".new;
+
+local function generate_error_message(errors)
+	local errmsg = {};
+	for name, err in pairs(errors) do
+		errmsg[#errmsg + 1] = name .. ": " .. err;
+	end
+	return table.concat(errmsg, "\n");
+end
+
+local choose_subcmd_layout = dataforms_new {
+	title = "Certificate management";
+	instructions = "What action do you want to perform?";
+
+	{ name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/certs#subcmd" };
+	{ name = "subcmd", type = "list-single", label = "Actions", required = true,
+		value = { {label = "Add certificate", value = "add"},
+			  {label = "List certificates", value = "list"},
+			  {label = "Disable certificate", value = "disable"},
+			  {label = "Revoke certificate", value = "revoke"},
+		};
+	};
+};
+
+local add_layout = dataforms_new {
+	title = "Adding a certificate";
+	instructions = "Enter the certificate in PEM format";
+
+	{ name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/certs#add" };
+	{ name = "name", type = "text-single", label = "Name", required = true };
+	{ name = "cert", type = "text-multi", label = "PEM certificate", required = true };
+	{ name = "manage", type = "boolean", label = "Can manage certificates", value = true };
+};
+
+
+local disable_layout_stub = dataforms_new { { name = "cert", type = "list-single", label = "Certificate", required = true } };
+
+
+local function adhoc_handler(self, data, state)
+	if data.action == "cancel" then return { status = "canceled" }; end
+
+	if not state or data.action == "prev" then
+		return { status = "executing", form = choose_subcmd_layout, actions = { "next" } }, {};
+	end
+
+	if not state.subcmd then
+		local fields, errors = choose_subcmd_layout:data(data.form);
+		if errors then
+			return { status = "completed", error = { message = generate_error_message(errors) } };
+		end
+		local subcmd = fields.subcmd
+
+		if subcmd == "add" then
+			return { status = "executing", form = add_layout, actions = { "prev", "next", "complete" } }, { subcmd = "add" };
+		elseif subcmd == "list" then
+			local list_layout = dataforms_new {
+				title = "List of certificates";
+			};
+
+			local certs = dm_load(jid_split(data.from), module.host, dm_table) or {};
+
+			for digest, info in pairs(certs) do
+				list_layout[#list_layout + 1] = { name = info.id, type = "text-multi", label = info.name, value = info.x509cert };
+			end
+
+			return { status = "completed", result = list_layout };
+		else
+			local layout = dataforms_new {
+				{ name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/certs#" .. subcmd };
+				{ name = "cert", type = "list-single", label = "Certificate", required = true };
+			};
+
+			if subcmd == "disable" then
+				layout.title = "Disabling a certificate";
+				layout.instructions = "Select the certificate to disable";
+			elseif subcmd == "revoke" then
+				layout.title = "Revoking a certificate";
+				layout.instructions = "Select the certificate to revoke";
+			end
+
+			local certs = dm_load(jid_split(data.from), module.host, dm_table) or {};
+
+			local values = {};
+			for digest, info in pairs(certs) do
+				values[#values + 1] = { label = info.name, value = info.id };
+			end
+
+			return { status = "executing", form = { layout = layout, values = { cert = values } }, actions = { "prev", "next", "complete" } },
+				{ subcmd = subcmd };
+		end
+	end
+
+	if state.subcmd == "add" then
+		local fields, errors = add_layout:data(data.form);
+		if errors then
+			return { status = "completed", error = { message = generate_error_message(errors) } };
+		end
+
+		local name = fields.name;
+		local x509cert = fields.cert:gsub("^%s*(.-)%s*$", "%1");
+
+		local cert = x509.cert_from_pem(
+		"-----BEGIN CERTIFICATE-----\n"
+		.. x509cert ..
+		"\n-----END CERTIFICATE-----\n");
+
+		if not cert then
+			return { status = "completed", error = { message = "Could not parse X.509 certificate" } };
+		end
+
+		local ok, err = enable_cert(jid_split(data.from), cert, {
+			id = cert:digest(digest_algo),
+			name = name,
+			x509cert = x509cert,
+			no_cert_management = not fields.manage
+		});
+
+		if not ok then
+			return { status = "completed", error = { message = err } };
+		end
+
+		module:log("debug", "%s added certificate named %s", data.from, name);
+
+		return { status = "completed", info = "Successfully added certificate " .. name .. "." };
+	else
+		local fields, errors = disable_layout_stub:data(data.form);
+		if errors then
+			return { status = "completed", error = { message = generate_error_message(errors) } };
+		end
+
+		local info = disable_cert(jid_split(data.from), fields.cert, state.subcmd == "revoke" );
+
+		if state.subcmd == "revoke" then
+			return { status = "completed", info = "Revoked certificate " .. info.name .. "."  };
+		else
+			return { status = "completed", info = "Disabled certificate " .. info.name .. "."  };
+		end
+	end
+end
+
+local cmd_desc = adhoc_new("Manage certificates", "http://prosody.im/protocol/certs", adhoc_handler, "user");
+module:provides("adhoc", cmd_desc);
+
 -- Here comes the SASL EXTERNAL stuff
 
 local now = os.time;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_compat_bind/mod_compat_bind.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -0,0 +1,13 @@
+-- Compatibility with clients that set 'to' on resource bind requests
+--
+-- http://xmpp.org/rfcs/rfc3920.html#bind
+-- http://xmpp.org/rfcs/rfc6120.html#bind-servergen-success
+
+local st = require "util.stanza";
+
+module:hook("iq/host/urn:ietf:params:xml:ns:xmpp-bind:bind", function(event)
+	local fixed_stanza = st.clone(event.stanza);
+	fixed_stanza.attr.to = nil;
+	core_process_stanza(event.origin, fixed_stanza);
+	return true;
+end);
--- a/mod_compat_muc_admin/mod_compat_muc_admin.lua	Mon Jun 11 22:32:45 2012 +0200
+++ b/mod_compat_muc_admin/mod_compat_muc_admin.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -23,6 +23,10 @@
 	["service-unavailable"] = true;
 	["malformed error"] = true;
 };
+local function get_error_condition(stanza)
+	local _, condition = stanza:get_error();
+ 	return condition or "malformed error";
+end
 local function is_kickable_error(stanza)
 	local cond = get_error_condition(stanza);
 	return kickable_error_conditions[cond] and cond;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_compat_vcard/mod_compat_vcard.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -0,0 +1,19 @@
+-- Compatibility with clients and servers (i.e. ejabberd) that send vcard
+-- requests to the full JID
+--
+-- https://support.process-one.net/browse/EJAB-1045
+
+local jid_bare = require "util.jid".bare;
+local st = require "util.stanza";
+local core_process_stanza = prosody.core_process_stanza;
+
+module:hook("iq/full", function(event)
+	local stanza = event.stanza;
+	local payload = stanza.tags[1];
+	if payload.name == "vCard" and stanza.attr.type == "get" and payload.attr.xmlns == "vcard-temp" then
+		local fixed_stanza = st.clone(event.stanza);
+		fixed_stanza.attr.to = jid_bare(stanza.attr.to);
+		core_process_stanza(event.origin, fixed_stanza);
+		return true;
+	end
+end, 1);
--- a/mod_host_guard/mod_host_guard.lua	Mon Jun 11 22:32:45 2012 +0200
+++ b/mod_host_guard/mod_host_guard.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -8,7 +8,7 @@
 local guard_protect = module:get_option_set("host_guard_selective", {})
 local guard_block_bl = module:get_option_set("host_guard_blacklist", {})
 
-local config = require "core.configmanager"
+local config = configmanager
 local error_reply = require "util.stanza".error_reply
 
 local function s2s_hook (event)
@@ -40,34 +40,38 @@
 	return nil
 end
 
-local function handle_activation (host)
+local function handle_activation (host, u)
 	if guard_blockall:contains(host) or guard_protect:contains(host) then
 		if hosts[host] and hosts[host].events then
 			hosts[host].events.add_handler("s2sin-established", s2s_hook, 500)
 			hosts[host].events.add_handler("route/remote", rr_hook, 500)
 			hosts[host].events.add_handler("stanza/jabber:server:dialback:result", s2s_hook, 500)
-                	module:log ("debug", "adding host protection for: "..host)
+                	if not u then 
+				module:log ("debug", "adding host protection for: "..host)
+			else
+				module:log ("debug", "updating or adding host protection for: "..host)
+			end
 		end
 	end
 end
 
-local function handle_deactivation (host)
+local function handle_deactivation (host, u, i)
 	if guard_blockall:contains(host) or guard_protect:contains(host) then
 		if hosts[host] and hosts[host].events then
 			hosts[host].events.remove_handler("s2sin-established", s2s_hook)
 			hosts[host].events.remove_handler("route/remote", rr_hook)
 			hosts[host].events.remove_handler("stanza/jabber:server:dialback:result", s2s_hook)
-                	module:log ("debug", "removing host protection for: "..host)
+                	if not u and not i then module:log ("debug", "removing host protection for: "..host) end
 		end
 	end
 end
 
-local function init_hosts()
-	for n,table in pairs(hosts) do
-		hosts[n].events.remove_handler("s2sin-established", s2s_hook)
-		hosts[n].events.remove_handler("route/remote", rr_hook)
-		hosts[n].events.remove_handler("stanza/jabber:server:dialback:result", s2s_hook)
-		if guard_blockall:contains(n) or guard_protect:contains(n) then	handle_activation(n) end
+local function init_hosts(u, i)
+	for n in pairs(hosts) do
+		if guard_blockall:contains(n) or guard_protect:contains(n) then
+			handle_deactivation(n, u, i)
+			handle_activation(n, u) 
+		end
 	end
 end
 
@@ -78,7 +82,7 @@
 	guard_protect = module:get_option_set("host_guard_selective", {})
 	guard_block_bl = module:get_option_set("host_guard_blacklist", {})
 
-	init_hosts()
+	init_hosts(true)
 end
 
 local function setup()
@@ -87,7 +91,16 @@
         module:hook ("host-deactivated", handle_deactivation)
         module:hook ("config-reloaded", reload)
 
-        init_hosts()
+        init_hosts(false, true)
+end
+
+function module.unload()
+	module:log ("debug", "removing host handlers as module is being unloaded...")
+	for n in pairs(hosts) do
+		hosts[n].events.remove_handler("s2sin-established", s2s_hook)
+		hosts[n].events.remove_handler("route/remote", rr_hook)
+		hosts[n].events.remove_handler("stanza/jabber:server:dialback:result", s2s_hook)
+	end
 end
 
 if prosody.start_time then
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_http_favicon/mod_http_favicon.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -0,0 +1,42 @@
+module:depends("http");
+
+local favicon = require"util.encodings".base64.decode[[
+AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAA
+AAAAAAD///8AsuD6TGrE95RiwfabYsH2m2TB9pmU1Phq+vz9A/38+wPx07xq67+emeq+nZvqvp2b
+68GilPTfz0z///8AsuD6TACb8v8Am/L/AJvy/wCb8v8Am/L/AJvy/3TI94ntxqiJ35dh/9+XYf/f
+l2H/35dh/9+XYf/fl2H/9N/PTGrE95QAm/L/AJvy/wCb8v8Am/L/AJvy/wCb8v8qq/PU5Kh61N+X
+Yf/fl2H/35dh/9+XYf/fl2H/35dh/+vBopRiwfabAJvy/wCb8v8Am/L/AJvy/wCb8v8Am/L/Iqjz
+3OOkdtzfl2H/35dh/9+XYf/fl2H/35dh/9+XYf/qvp2bYsH2mwCb8v8Am/L/AJvy/wCb8v8Am/L/
+AJvy/yKo89zjpHbc35dh/9+XYf/fl2H/35dh/9+XYf/fl2H/6r6dm2TB9pkAm/L/AJvy/wCb8v8A
+m/L/AJvy/wCb8v8kqfPa46V32t+XYf/fl2H/35dh/9+XYf/fl2H/35dh/+u/npmU1PhqAJvy/wCb
+8v8Am/L/AJvy/wCb8v8Am/L/Vrz2p+m5lqffl2H/35dh/9+XYf/fl2H/35dh/9+XYf/x07xq+vz9
+A3TI94kqq/PUIqjz3CKo89wkqfPaVrz2p+b0/Bf79O8X6bmWp+Old9rjpHbc46R23OSoetTtxqiJ
+/fz7A/38+wPtxqiJ5Kh61OOkdtzjpHbc46V32um5lqf79O8X5vT8F1a89qckqfPaIqjz3CKo89wq
+q/PUdMj3ifr8/QPx07xq35dh/9+XYf/fl2H/35dh/9+XYf/fl2H/6bmWp1a89qcAm/L/AJvy/wCb
+8v8Am/L/AJvy/wCb8v+U1Phq67+emd+XYf/fl2H/35dh/9+XYf/fl2H/35dh/+Old9okqfPaAJvy
+/wCb8v8Am/L/AJvy/wCb8v8Am/L/ZMH2meq+nZvfl2H/35dh/9+XYf/fl2H/35dh/9+XYf/jpHbc
+Iqjz3ACb8v8Am/L/AJvy/wCb8v8Am/L/AJvy/2LB9pvqvp2b35dh/9+XYf/fl2H/35dh/9+XYf/f
+l2H/46R23CKo89wAm/L/AJvy/wCb8v8Am/L/AJvy/wCb8v9iwfab68GilN+XYf/fl2H/35dh/9+X
+Yf/fl2H/35dh/+SoetQqq/PUAJvy/wCb8v8Am/L/AJvy/wCb8v8Am/L/asT3lPTfz0zfl2H/35dh
+/9+XYf/fl2H/35dh/9+XYf/txqiJdMj3iQCb8v8Am/L/AJvy/wCb8v8Am/L/AJvy/7Lg+kz///8A
+9N/PTOvBopTqvp2b6r6dm+u/npnx07xq/fz7A/r8/QOU1PhqZMH2mWLB9ptiwfabasT3lLLg+kz/
+//8Aw8MAAIABAAAAAAAAAAAAAAAAAAAAAAAAgAEAAIGBAACBgQAAgAEAAAAAAAAAAAAAAAAAAAAA
+AACAAQAAw8MAAA==]];
+
+local filename = module:get_option_string("favicon");
+if filename then
+	local fd = assert(module:load_resource(filename));
+	favicon = assert(fd:read("*a"));
+end
+
+module:provides("http", {
+	default_path = "/favicon.ico";
+	route = {
+		GET = {
+			headers = {
+				content_type = "image/x-icon";
+			};
+			body = favicon;
+		}
+	}
+});
--- a/mod_inotify_reload/mod_inotify_reload.lua	Mon Jun 11 22:32:45 2012 +0200
+++ b/mod_inotify_reload/mod_inotify_reload.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -44,16 +44,19 @@
 	local k = host.."\0"..name;
 	watches[k] = { id = id, path = path, name = name, host = host };
 	watch_ids[id] = k;
+	module:log("debug", "Watching %s:%s with id %d", name, host, id);
 	return true;
 end
 
 function unwatch_module(name, host)
 	local k = host.."\0"..name;
 	if not watches[k] then
+		module:log("warn", "Not watching %s:%s", name, host);
 		return nil, "not-watching";
 	end
 	local id = watches[k].id;
 	local ok, err = inh:rmwatch(id);
+	module:log("info", "Removed watch %d", id);
 	watches[k] = nil;
 	watch_ids[id] = nil;
 	return ok, err;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_limits/mod_limits.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -0,0 +1,99 @@
+-- mod_limits: Rate-limiting for Prosody
+-- Version: Alpha
+-- Author: Matthew Wild <mwild1@gmail.com>
+
+-- Because we deal we pre-authed sessions and streams we can't be host-specific
+module:set_global();
+
+local filters = require "util.filters";
+local throttle = require "util.throttle";
+local timer = require "util.timer";
+
+local limits_cfg = module:get_option("limits", {});
+local limits_resolution = module:get_option_number("limits_resolution", 1);
+
+local default_bytes_per_second = 3000;
+local default_burst = 2;
+
+local rate_units = { b = 1, k = 3, m = 6, g = 9, t = 12 } -- Plan for the future.
+local function parse_rate(rate, sess_type)
+	local quantity, unit, exp;
+	if rate then
+		quantity, unit = rate:match("^(%d+) ?([^/]+)/s$");
+		exp = quantity and rate_units[unit:sub(1,1):lower()];
+	end
+	if not exp then
+		module:log("error", "Error parsing rate for %s: %q, using default rate (%d bytes/s)", sess_type, rate, default_bytes_per_second);
+		return default_bytes_per_second;
+	end
+	return quantity*(10^exp);
+end
+
+local function parse_burst(burst, sess_type)
+	if type(burst) == "string" then
+		burst = burst:match("^(%d+) ?s$");
+	end
+	local n_burst = tonumber(burst);
+	if not n_burst then
+		module:log("error", "Unable to parse burst for %s: %q, using default burst interval (%ds)", sess_type, tostring(burst), default_burst);
+	end
+	return n_burst or default_burst;
+end
+
+-- Process config option into limits table:
+-- limits = { c2s = { bytes_per_second = X, burst_seconds = Y } }
+local limits = {};
+
+for sess_type, sess_limits in pairs(limits_cfg) do
+	limits[sess_type] = {
+		bytes_per_second = parse_rate(sess_limits.rate, sess_type);
+		burst_seconds = parse_burst(sess_limits.burst, sess_type);
+	};
+end
+
+local default_filter_set = {};
+
+function default_filter_set.bytes_in(bytes, session)
+	local throttle = session.throttle;
+	if throttle then
+		local ok, balance, outstanding = throttle:poll(#bytes, true);
+		if not ok then
+			session.log("debug", "Session over rate limit (%d) with %d (by %d), pausing", throttle.max, #bytes, outstanding);
+			session.conn:pause(); -- Read no more data from the connection until there is no outstanding data
+			local outstanding_data = bytes:sub(-outstanding);
+			bytes = bytes:sub(1, #bytes-outstanding);
+			timer.add_task(limits_resolution, function ()
+				if not session.conn then return; end
+				if throttle:peek(#outstanding_data) then
+					session.log("debug", "Resuming paused session"); session.conn:resume();
+				end
+				-- Handle what we can of the outstanding data
+				session.data(outstanding_data);
+			end);
+		end
+	end
+	return bytes;
+end
+
+local type_filters = {
+	c2s = default_filter_set;
+	s2sin = default_filter_set;
+	s2sout = default_filter_set;
+};
+
+local function filter_hook(session)
+	local session_type = session.type:match("^[^_]+");
+	local filter_set, opts = type_filters[session_type], limits[session_type];
+	if opts then
+		session.throttle = throttle.create(opts.bytes_per_second * opts.burst_seconds, opts.burst_seconds);
+		filters.add_filter(session, "bytes/in", filter_set.bytes_in, 1000);
+	end
+end
+
+function module.load()
+	filters.add_filter_hook(filter_hook);
+end
+
+function module.unload()
+	filters.remove_filter_hook(filter_hook);
+end
--- a/mod_mam/mod_mam.lua	Mon Jun 11 22:32:45 2012 +0200
+++ b/mod_mam/mod_mam.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -11,6 +11,7 @@
 local rsm = module:require "rsm";
 local jid_bare = require "util.jid".bare;
 local jid_split = require "util.jid".split;
+local jid_prep = require "util.jid".prep;
 local host = module.host;
 
 local dm_load = require "util.datamanager".load;
@@ -125,7 +126,23 @@
 		module:log("debug", "Archive query, id %s with %s from %s until %s)",
 			tostring(qid), qwith or "anyone", qstart or "the dawn of time", qend or "now");
 
-		qstart, qend = (qstart and timestamp_parse(qstart)), (qend and timestamp_parse(qend))
+		if qstart or qend then -- Validate timestamps
+			local vstart, vend = (qstart and timestamp_parse(qstart)), (qend and timestamp_parse(qend))
+			if (qstart and not qwith) or (qend and not vend) then
+				origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid timestamp"))
+				return true
+			end
+			qstart, qend = vstart, vend;
+		end
+
+		if qwith then -- Validate the 'with' jid
+			local pwith = qwith and jid_prep(qwith);
+			if pwith and not qwith then -- it failed prepping
+				origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid JID"))
+				return true
+			end
+			qwith = pwith;
+		end
 
 		-- Load all the data!
 		local data, err = dm_list_load(origin.username, origin.host, archive_store);
@@ -193,10 +210,10 @@
 					module:log("debug", "Start of matching range found");
 					qset_matches = true;
 				end
-				if n >= qmax then
-					module:log("debug", "Max number of items matched");
-					break
-				end
+			end
+			if n >= qmax then
+				module:log("debug", "Max number of items matched");
+				break
 			end
 		end
 		-- That's all folks!
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_mam_adhoc/mod_mam_adhoc.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -0,0 +1,113 @@
+module:depends"adhoc";
+local dataforms_new = require "util.dataforms".new;
+local dm_load = require "util.datamanager".load;
+local dm_store = require "util.datamanager".store;
+local jid_split = require "util.jid".split;
+local t_insert = table.insert;
+
+local mam_prefs_form = dataforms_new{
+	title = "Archive preferences";
+	--instructions = "";
+	{
+		name = "default",
+		label = "Default storage policy",
+		type = "list-single",
+		value = {
+			{ value = "always", label = "Always" },
+			{ value = "never", label = "Never", default = true},
+			{ value = "roster", label = "Roster" },
+		},
+	};
+	{
+		name = "always",
+		label = "Always store messages to/from",
+		type = "jid-multi"
+	};
+	{
+		name = "never",
+		label = "Never store messages to/from",
+		type = "jid-multi"
+	};
+};
+
+local host = module.host;
+
+local default_attrs = {
+	always = true, [true] = "always",
+	never = false, [false] = "never",
+	roster = "roster",
+}
+
+local global_default_policy = module:get_option("default_archive_policy", false);
+local archive_store = "archive2";
+local prefs_store = archive_store .. "_prefs";
+local function get_prefs(user)
+	return dm_load(user, host, prefs_store) or
+		{ [false] = global_default_policy };
+end
+local function set_prefs(user, prefs)
+	return dm_store(user, host, prefs_store, prefs);
+end
+
+local function mam_prefs_handler(self, data, state)
+	local username, hostname = jid_split(data.from);
+	if state then -- the second return value
+		if data.action == "cancel" then
+			return { status = "canceled" };
+		end
+
+		if not username or not hostname or hostname ~= module.host then
+			return { status = "error", error = { type = "cancel",
+				condition = "forbidden", message = "Invalid user or hostname." } };
+		end
+
+		local fields = mam_prefs_form:data(data.form);
+
+		local default, always, never = fields.default, fields.always, fields.never;
+		local prefs = {};
+		if default then
+			prefs[false] = default_attrs[default];
+		end
+		if always then
+			for i=1,#always do
+				prefs[always[i]] = true;
+			end
+		end
+		if never then
+			for i=1,#never do
+				prefs[never[i]] = false;
+			end
+		end
+
+		set_prefs(username, prefs);
+
+		return { status = "completed" }
+	else -- No state, send the form.
+		local prefs = get_prefs(username);
+		local values = {
+			default = {
+				{ value = "always", label = "Always" };
+				{ value = "never", label = "Never" };
+				{ value = "roster", label = "Roster" };
+			};
+			always = {};
+			never = {};
+		};
+
+		for jid, p in pairs(prefs) do
+			if jid then
+				t_insert(values[p and "always" or "never"], jid);
+
+			elseif p == true then -- Yes, this is ugly.  FIXME later.
+				values.default[1].default = true;
+			elseif p == false then
+				values.default[2].default = true;
+			elseif p == "roster" then
+				values.default[3].default = true;
+			end
+		end
+		return { status = "executing", actions  = { "complete" }, form = { layout = mam_prefs_form, values = values } }, true;
+	end
+end
+
+module:provides("adhoc", module:require"adhoc".new("Archive settings", "urn:xmpp:mam#configure", mam_prefs_handler));
--- a/mod_pubsub_feeds/mod_pubsub_feeds.lua	Mon Jun 11 22:32:45 2012 +0200
+++ b/mod_pubsub_feeds/mod_pubsub_feeds.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -5,7 +5,7 @@
 -- Config:
 -- Component "pubsub.example.com" "pubsub"
 -- modules_enabled = {
---   "pubsub_feed";
+--   "pubsub_feeds";
 -- }
 -- feeds = { -- node -> url
 --   prosody_blog = "http://blog.prosody.im/feed/atom.xml";
@@ -69,7 +69,7 @@
 	end
 end
 update_config();
-module:hook("config-reloaded", update_config);
+module:hook_global("config-reloaded", update_config);
 
 local actor = module.host.."/"..module.name;
 
@@ -160,12 +160,13 @@
 	return module:http_url(nil, "/callback") .. "?node=" .. urlencode(node);
 end	
 
-function subscribe(feed)
+function subscribe(feed, want)
+	want = want or "subscribe";
 	feed.token = uuid();
-	feed.secret = uuid();
+	feed.secret = feed.secret or uuid();
 	local body = formencode{
 		["hub.callback"] = format_url(feed.node);
-		["hub.mode"] = "subscribe"; --TODO unsubscribe
+		["hub.mode"] = want;
 		["hub.topic"] = feed.url;
 		["hub.verify"] = "async";
 		["hub.verify_token"] = feed.token;
@@ -176,7 +177,7 @@
 	--module:log("debug", "subscription request, body: %s", body);
 
 	--FIXME The subscription states and related stuff
-	feed.subscription = "subscribe";
+	feed.subscription = want;
 	http.request(feed.hub, { body = body }, function(data, code, req) 
 		module:log("debug", "subscription to %s submitted, status %s", feed.node, tostring(code));
 		if code >= 400 then
@@ -200,8 +201,12 @@
 	--module:log("debug", "Headers: %s", dump(request.headers));
 
 	local feed = feed_list[query.node];
+	if not feed then
+		return 404;
+	end
+
 	if method == "GET" then
-		if query.node and feed then
+		if query.node then
 			if query["hub.topic"] ~= feed.url then
 				module:log("debug", "Invalid topic: %s", tostring(query["hub.topic"]))
 				return 404
@@ -216,15 +221,14 @@
 			end
 			if query["hub.verify_token"] ~= feed.token then
 				module:log("debug", "Invalid verify_token: %s", tostring(query["hub.verify_token"]))
-				return 401
+				return 401;
 			end
 			module:log("debug", "Confirming %s request to %s", feed.subscription, feed.url)
 			return query["hub.challenge"];
 		end
 		return 400;
 	elseif method == "POST" then
-		local body = request.body;
-		if #body > 0 and feed then
+		if #body > 0 then
 			module:log("debug", "got %d bytes PuSHed for %s", #body, query.node);
 			local signature = request.headers.x_hub_signature;
 			if feed.secret then
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_readonly/mod_readonly.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -0,0 +1,26 @@
+local st = require "util.stanza";
+
+local stores = module:get_option("readonly_stores", {
+	vcard = { "vcard-temp", "vCard" };
+});
+
+local namespaces = {};
+for name, namespace in pairs(stores) do
+	namespaces[table.concat(namespace, ":")] = name;
+end
+
+function prevent_write(event)
+	local stanza = event.stanza;
+	if stanza.attr.type ~= "set" then return; end
+	local xmlns_and_tag = stanza.tags[1].attr.xmlns..":"..stanza.tags[1].name;
+	local store_name = namespaces[xmlns_and_tag];
+	if store_name then
+		module:log("warn", "Preventing modification of %s store by %s", store_name, stanza.attr.from);
+		event.origin.send(st.error_reply(stanza, "cancel", "not-allowed", store_name.." data is read-only"));
+		return true; -- Block stanza
+	end
+end
+
+for namespace in pairs(namespaces) do
+	module:hook("iq/bare/"..namespace, prevent_write, 200);
+end
--- a/mod_register_json/mod_register_json.lua	Mon Jun 11 22:32:45 2012 +0200
+++ b/mod_register_json/mod_register_json.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -6,7 +6,7 @@
 
 local jid_prep = require "util.jid".prep
 local jid_split = require "util.jid".split
-local usermanager = require "core.usermanager"
+local usermanager = usermanager
 local b64_decode = require "util.encodings".base64.decode
 local json_decode = require "util.json".decode
 local os_time = os.time
@@ -78,29 +78,31 @@
 			module:log("warn", "%s tried to submit registration data for %s but he's not an admin", user, req_body["host"])
 			return http_response(event, 401, "I obey only to my masters... Have a nice day.")
 		else	
-			-- Checks for both Throttling/Whitelist and Blacklist (basically copycatted from prosody's register.lua code)
+			-- Blacklist can be checked here.
 			if blacklist:contains(req_body["ip"]) then module:log("warn", "Attempt of reg. submission to the JSON servlet from blacklisted address: %s", req_body["ip"]) ; return http_response(403, "The specified address is blacklisted, sorry sorry.") end
-			if throttle_time and not whitelist:contains(req_body["ip"]) then
-				if not recent_ips[req_body["ip"]] then
-					recent_ips[req_body["ip"]] = os_time()
-				else
-					if os_time() - recent_ips[req_body["ip"]] < throttle_time then
-						recent_ips[req_body["ip"]] = os_time()
-						module:log("warn", "JSON Registration request from %s has been throttled.", req_body["ip"])
-						return http_response(event, 503, "Woah... How many users you want to register..? Request throttled, wait a bit and try again.")
-					end
-					recent_ips[req_body["ip"]] = os_time()
-				end
-			end
 
 			-- We first check if the supplied username for registration is already there.
 			-- And nodeprep the username
 			local username = nodeprep(req_body["username"])
-			if not usermanager.user_exists(username, req_body["host"]) then
-				if not username then
-					module:log("debug", "%s supplied an username containing invalid characters: %s", user, username)
-					return http_response(event, 406, "Supplied username contains invalid characters, see RFC 6122.")
-				else
+			if not username then
+				module:log("debug", "%s supplied an username containing invalid characters: %s", user, username)
+				return http_response(event, 406, "Supplied username contains invalid characters, see RFC 6122.")
+			else
+				if not usermanager.user_exists(username, req_body["host"]) then
+					-- if username fails to register successive requests shouldn't be throttled until one is successful.
+					if throttle_time and not whitelist:contains(req_body["ip"]) then
+						if not recent_ips[req_body["ip"]] then
+							recent_ips[req_body["ip"]] = os_time()
+						else
+							if os_time() - recent_ips[req_body["ip"]] < throttle_time then
+								recent_ips[req_body["ip"]] = os_time()
+								module:log("warn", "JSON Registration request from %s has been throttled.", req_body["ip"])
+								return http_response(event, 503, "Woah... How many users you want to register..? Request throttled, wait a bit and try again.")
+							end
+							recent_ips[req_body["ip"]] = os_time()
+						end
+					end
+
 					local ok, error = usermanager.create_user(username, req_body["password"], req_body["host"])
 					if ok then 
 						hosts[req_body["host"]].events.fire_event("user-registered", { username = username, host = req_body["host"], source = "mod_register_json", session = { ip = req_body["ip"] } })
@@ -110,10 +112,10 @@
 						module:log("error", "user creation failed: "..error)
 						return http_response(event, 500, "Encountered server error while creating the user: "..error)
 					end
+				else
+					module:log("debug", "%s registration data submission for %s failed (user already exists)", user, username)
+					return http_response(event, 409, "User already exists.")
 				end
-			else
-				module:log("debug", "%s registration data submission for %s failed (user already exists)", user, username)
-				return http_response(event, 409, "User already exists.")
 			end
 		end
 	end
--- a/mod_register_redirect/mod_register_redirect.lua	Mon Jun 11 22:32:45 2012 +0200
+++ b/mod_register_redirect/mod_register_redirect.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -6,7 +6,7 @@
 -- Redirects IP addresses not in the whitelist to a web page or another method to complete the registration.
 
 local st = require "util.stanza"
-local cman = require "core.configmanager"
+local cman = configmanager
 
 function reg_redirect(event)
 	local stanza, origin = event.stanza, event.origin
--- a/mod_register_web/mod_register_web.lua	Mon Jun 11 22:32:45 2012 +0200
+++ b/mod_register_web/mod_register_web.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -1,4 +1,5 @@
 local captcha_options = module:get_option("captcha_options", {});
+local nodeprep = require "util.encodings".stringprep.nodeprep;
 
 function generate_captcha(display_options)
 	return (([[
@@ -50,10 +51,11 @@
 end
 
 function register_user(form)
-	if usermanager.user_exists(form.username, module.host) then
-		return nil, "user-exists";
-	end
-	return usermanager.create_user(form.username, form.password, module.host);
+        local prepped_username = nodeprep(form.username);
+        if usermanager.user_exists(prepped_username, module.host) then
+                return nil, "user-exists";
+        end
+        return usermanager.create_user(prepped_username, form.password, module.host);
 end
 
 function generate_success(event, form)
--- a/mod_streamstats/mod_streamstats.lua	Mon Jun 11 22:32:45 2012 +0200
+++ b/mod_streamstats/mod_streamstats.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -1,11 +1,14 @@
-local stats = prosody.stats;
+module:set_global();
+local stats = module:shared"stats";
 local iter = require "util.iterators";
 local count, keys = iter.count, iter.keys;
 
-if not stats then
-	stats = {
-		stats = {}; conns = {};
-		
+stats.stats = stats.stats or {};
+stats.conns = stats.conns or {};
+
+setmetatable(stats, {
+	__index = {
+
 		broadcast = function (self, stat)
 			local value = self.stats[stat];
 			for conn in pairs(self.conns) do
@@ -24,75 +27,78 @@
 			self.stats[stat] = value;
 			self:broadcast(stat);
 		end;
-		
+
 		add_conn = function (self, conn)
 			self.conns[conn] = true;
 			for stat, value in pairs(self.stats) do
 				conn:write(stat..":"..value.."\n");
 			end
 		end;
-		
+
 		remove_conn = function (self, conn)
 			self.conns[conn] = nil;
 		end;
 	};
-	prosody.stats = stats;
-	
-	local network = {};
-	
-	function network.onconnect(conn)
-		stats:add_conn(conn);
-	end
-	
-	function network.onincoming(conn, data)
-	end
-	
-	function network.ondisconnect(conn, reason)
-		stats:remove_conn(conn);
-	end
-	
-	require "util.iterators";
-	require "util.timer".add_task(1, function ()
-		stats:set("s2s-in", count(keys(prosody.incoming_s2s)));
-		return math.random(10, 20);
-	end);
-	require "util.timer".add_task(3, function ()
-		local s2sout_count = 0;
-		for _, host in pairs(prosody.hosts) do
-			s2sout_count = s2sout_count + count(keys(host.s2sout));
-		end
-		stats:set("s2s-out", s2sout_count);
-		return math.random(10, 20);
-	end);
-	
-	require "net.connlisteners".register("stats", network);
-	require "net.connlisteners".start("stats", { port = module:get_option("stats_ports") or 5444, interface = "127.0.0.1" });
+});
+
+local network = {};
+
+function network.onconnect(conn)
+	stats:add_conn(conn);
+end
+
+function network.onincoming(conn, data)
+end
+
+function network.ondisconnect(conn, reason)
+	stats:remove_conn(conn);
 end
 
-module:hook("resource-bind", function ()
-	stats:adjust("c2s", 1);
+module:add_timer(1, function ()
+	stats:set("s2s-in", count(keys(prosody.incoming_s2s)));
+	return math.random(10, 20);
 end);
-module:hook("resource-unbind", function ()
-	stats:adjust("c2s", -1);
+module:add_timer(3, function ()
+	local s2sout_count = 0;
+	for _, host in pairs(prosody.hosts) do
+		s2sout_count = s2sout_count + count(keys(host.s2sout));
+	end
+	stats:set("s2s-out", s2sout_count);
+	return math.random(10, 20);
 end);
 
-local c2s_count = 0;
-for username, user in pairs(hosts[module.host].sessions or {}) do
-	for resource, session in pairs(user.sessions or {}) do
-		c2s_count = c2s_count + 1;
+
+function module.add_host(module)
+	module:hook("resource-bind", function ()
+		stats:adjust("c2s", 1);
+	end);
+	module:hook("resource-unbind", function ()
+		stats:adjust("c2s", -1);
+	end);
+
+	local c2s_count = 0;
+	for username, user in pairs(hosts[module.host].sessions or {}) do
+		for resource, session in pairs(user.sessions or {}) do
+			c2s_count = c2s_count + 1;
+		end
 	end
-end
-stats:adjust("c2s", c2s_count);
+	stats:set("c2s", c2s_count);
 
-module:hook("s2sin-established", function (event)
-	stats:adjust("s2s-in", 1);
-end);
-module:hook("s2sin-destroyed", function (event)
-	stats:adjust("s2s-in", -1);
-end);
-module:hook("s2sout-established", function (event)
-	stats:adjust("s2s-out", 1);
-end);
-module:hook("s2sout-destroyed", function (event)
-	stats:adjust("s2s-out", -1);
-end);
+	module:hook("s2sin-established", function (event)
+		stats:adjust("s2s-in", 1);
+	end);
+	module:hook("s2sin-destroyed", function (event)
+		stats:adjust("s2s-in", -1);
+	end);
+	module:hook("s2sout-established", function (event)
+		stats:adjust("s2s-out", 1);
+	end);
+	module:hook("s2sout-destroyed", function (event)
+		stats:adjust("s2s-out", -1);
+	end);
+end
+
+module:provides("net", {
+	default_port = 5444;
+	listener = network;
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_vjud/mod_vjud.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -0,0 +1,155 @@
+local dm_load = require "util.datamanager".load;
+local dm_store = require "util.datamanager".store;
+
+local usermanager = require "core.usermanager";
+local dataforms_new = require "util.dataforms".new;
+local jid_split = require "util.jid".prepped_split;
+local vcard = module:require "vcard";
+local rawget, rawset = rawget, rawset;
+
+local st = require "util.stanza";
+local template = require "util.template";
+
+local get_reply = template[[
+<query xmlns="jabber:iq:search">
+  <instructions>Fill in one or more fields to search for any matching Jabber users.</instructions>
+  <first/>
+  <last/>
+  <nick/>
+  <email/>
+</query>
+]].apply({});
+local item_template = template[[
+<item xmlns="jabber:iq:search" jid="{jid}">
+  <first>{first}</first>
+  <last>{last}</last>
+  <nick>{nick}</nick>
+  <email>{email}</email>
+</item>
+]];
+
+module:add_feature("jabber:iq:search");
+
+local opted_in;
+function module.load()
+	opted_in = dm_load(nil, module.host, "user_index") or {};
+end
+function module.unload()
+	dm_store(nil, module.host, "user_index", opted_in);
+end
+
+local opt_in_layout = dataforms_new{
+	title = "Search settings";
+	instructions = "Do you want to appear in search results?";
+	{
+		name = "searchable",
+		label = "Appear in search results?",
+		type = "boolean",
+	},
+};
+local vCard_mt = {
+	__index = function(t, k)
+		if type(k) ~= "string" then return nil end
+		for i=1,#t do
+			local t_i = rawget(t, i);
+			if t_i and t_i.name == k then
+				rawset(t, k, t_i);
+				return t_i;
+			end
+		end
+	end
+};
+
+local function get_user_vcard(user)
+	local vCard = dm_load(user, module.host, "vcard");
+	if vCard then
+		vCard = st.deserialize(vCard);
+		vCard = vcard.from_xep54(vCard);
+		return setmetatable(vCard, vCard_mt);
+	end
+end
+
+local at_host = "@"..module.host;
+
+module:hook("iq/host/jabber:iq:search:query", function(event)
+	local origin, stanza = event.origin, event.stanza;
+
+	if stanza.attr.type == "get" then
+		origin.send(st.reply(stanza):add_child(get_reply));
+	else -- type == "set"
+		local query = stanza.tags[1];
+		local first, last, nick, email =
+			(query:get_child_text"first" or false),
+			(query:get_child_text"last" or false),
+			(query:get_child_text"nick" or false),
+			(query:get_child_text"email" or false);
+
+		if not ( first or last or nick or email ) then
+			origin.send(st.error_reply(stanza, "modify", "not-acceptable", "All fields were empty"));
+			return true;
+		end
+
+		local reply = st.reply(stanza):query("jabber:iq:search");
+
+		local username, hostname = jid_split(email);
+		if hostname == module.host and username and usermanager.user_exists(username, hostname) then
+			local vCard = get_user_vcard(username);
+			if vCard then
+				reply:add_child(item_template.apply{
+					jid = username..at_host;
+					first = vCard.N and vCard.N[2] or nil;
+					last = vCard.N and vCard.N[1] or nil;
+					nick = vCard.NICKNAME and vCard.NICKNAME[1] or username;
+					email = vCard.EMAIL and vCard.EMAIL[1] or nil;
+				});
+			end
+		else
+			for username in pairs(opted_in) do
+				local vCard = get_user_vcard(username);
+				if vCard and (
+				(vCard.N and vCard.N[2] == first) or
+				(vCard.N and vCard.N[1] == last) or
+				(vCard.NICKNAME and vCard.NICKNAME[1] == nick) or
+				(vCard.EMAIL and vCard.EMAIL[1] == email)) then
+					reply:add_child(item_template.apply{
+						jid = username..at_host;
+						first = vCard.N and vCard.N[2] or nil;
+						last = vCard.N and vCard.N[1] or nil;
+						nick = vCard.NICKNAME and vCard.NICKNAME[1] or username;
+						email = vCard.EMAIL and vCard.EMAIL[1] or nil;
+					});
+				end
+			end
+		end
+		origin.send(reply);
+	end
+	return true;
+end);
+
+local function opt_in_handler(self, data, state)
+	local username, hostname = jid_split(data.from);
+	if state then -- the second return value
+		if data.action == "cancel" then
+			return { status = "canceled" };
+		end
+
+		if not username or not hostname or hostname ~= module.host then
+			return { status = "error", error = { type = "cancel",
+				condition = "forbidden", message = "Invalid user or hostname." } };
+		end
+
+		local fields = opt_in_layout:data(data.form);
+		opted_in[username] = fields.searchable or nil
+
+		return { status = "completed" }
+	else -- No state, send the form.
+		return { status = "executing", actions  = { "complete" },
+			form = { layout = opt_in_layout, data = { searchable = opted_in[username] } } }, true;
+	end
+end
+
+local adhoc_new = module:require "adhoc".new;
+local adhoc_vjudsetup = adhoc_new("Search settings", "vjudsetup", opt_in_handler);--, "self");-- and nil);
+module:depends"adhoc";
+module:provides("adhoc", adhoc_vjudsetup);
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_vjud/vcard.lib.lua	Fri Jul 27 14:29:59 2012 +0100
@@ -0,0 +1,464 @@
+-- Copyright (C) 2011-2012 Kim Alvefur
+-- 
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+-- TODO
+-- Fix folding.
+
+local st = require "util.stanza";
+local t_insert, t_concat = table.insert, table.concat;
+local type = type;
+local next, pairs, ipairs = next, pairs, ipairs;
+
+local from_text, to_text, from_xep54, to_xep54;
+
+local line_sep = "\n";
+
+local vCard_dtd; -- See end of file
+
+local function fold_line()
+	error "Not implemented" --TODO
+end
+local function unfold_line()
+	error "Not implemented"
+	-- gsub("\r?\n[ \t]([^\r\n])", "%1");
+end
+
+local function vCard_esc(s)
+	return s:gsub("[,:;\\]", "\\%1"):gsub("\n","\\n");
+end
+
+local function vCard_unesc(s)
+	return s:gsub("\\?[\\nt:;,]", {
+		["\\\\"] = "\\",
+		["\\n"] = "\n",
+		["\\r"] = "\r",
+		["\\t"] = "\t",
+		["\\:"] = ":", -- FIXME Shouldn't need to espace : in values, just params
+		["\\;"] = ";",
+		["\\,"] = ",",
+		[":"] = "\29",
+		[";"] = "\30",
+		[","] = "\31",
+	});
+end
+
+local function item_to_xep54(item)
+	local t = st.stanza(item.name, { xmlns = "vcard-temp" });
+
+	local prop_def = vCard_dtd[item.name];
+	if prop_def == "text" then
+		t:text(item[1]);
+	elseif type(prop_def) == "table" then
+		if prop_def.types and item.TYPE then
+			if type(item.TYPE) == "table" then
+				for _,v in pairs(prop_def.types) do
+					for _,typ in pairs(item.TYPE) do
+						if typ:upper() == v then
+							t:tag(v):up();
+							break;
+						end
+					end
+				end
+			else
+				t:tag(item.TYPE:upper()):up();
+			end
+		end
+
+		if prop_def.props then
+			for _,v in pairs(prop_def.props) do
+				if item[v] then
+					t:tag(v):up();
+				end
+			end
+		end
+
+		if prop_def.value then
+			t:tag(prop_def.value):text(item[1]):up();
+		elseif prop_def.values then
+			local prop_def_values = prop_def.values;
+			local repeat_last = prop_def_values.behaviour == "repeat-last" and prop_def_values[#prop_def_values];
+			for i=1,#item do
+				t:tag(prop_def.values[i] or repeat_last):text(item[i]):up();
+			end
+		end
+	end
+
+	return t;
+end
+
+local function vcard_to_xep54(vCard)
+	local t = st.stanza("vCard", { xmlns = "vcard-temp" });
+	for i=1,#vCard do
+		t:add_child(item_to_xep54(vCard[i]));
+	end
+	return t;
+end
+
+function to_xep54(vCards)
+	if vCards[1].name then
+		return vcard_to_xep54(vCards)
+	else
+		local t = st.stanza("xCard", { xmlns = "vcard-temp" });
+		for i=1,#vCards do
+			t:add_child(vcard_to_xep54(vCards[i]));
+		end
+		return t;
+	end
+end
+
+function from_text(data)
+	data = data -- unfold and remove empty lines
+		:gsub("\r\n","\n")
+		:gsub("\n ", "")
+		:gsub("\n\n+","\n");
+	local vCards = {};
+	local c; -- current item
+	for line in data:gmatch("[^\n]+") do
+		local line = vCard_unesc(line);
+		local name, params, value = line:match("^([-%a]+)(\30?[^\29]*)\29(.*)$");
+		value = value:gsub("\29",":");
+		if #params > 0 then
+			local _params = {};
+			for k,isval,v in params:gmatch("\30([^=]+)(=?)([^\30]*)") do
+				k = k:upper();
+				local _vt = {};
+				for _p in v:gmatch("[^\31]+") do
+					_vt[#_vt+1]=_p
+					_vt[_p]=true;
+				end
+				if isval == "=" then
+					_params[k]=_vt;
+				else
+					_params[k]=true;
+				end
+			end
+			params = _params;
+		end
+		if name == "BEGIN" and value == "VCARD" then
+			c = {};
+			vCards[#vCards+1] = c;
+		elseif name == "END" and value == "VCARD" then
+			c = nil;
+		elseif vCard_dtd[name] then
+			local dtd = vCard_dtd[name];
+			local p = { name = name };
+			c[#c+1]=p;
+			--c[name]=p;
+			local up = c;
+			c = p;
+			if dtd.types then
+				for _, t in ipairs(dtd.types) do
+					local t = t:lower();
+					if ( params.TYPE and params.TYPE[t] == true)
+							or params[t] == true then
+						c.TYPE=t;
+					end
+				end
+			end
+			if dtd.props then
+				for _, p in ipairs(dtd.props) do
+					if params[p] then
+						if params[p] == true then
+							c[p]=true;
+						else
+							for _, prop in ipairs(params[p]) do
+								c[p]=prop;
+							end
+						end
+					end
+				end
+			end
+			if dtd == "text" or dtd.value then
+				t_insert(c, value);
+			elseif dtd.values then
+				local value = "\30"..value;
+				for p in value:gmatch("\30([^\30]*)") do
+					t_insert(c, p);
+				end
+			end
+			c = up;
+		end
+	end
+	return vCards;
+end
+
+local function item_to_text(item)
+	local value = {};
+	for i=1,#item do
+		value[i] = vCard_esc(item[i]);
+	end
+	value = t_concat(value, ";");
+
+	local params = "";
+	for k,v in pairs(item) do
+		if type(k) == "string" and k ~= "name" then
+			params = params .. (";%s=%s"):format(k, type(v) == "table" and t_concat(v,",") or v);
+		end
+	end
+
+	return ("%s%s:%s"):format(item.name, params, value)
+end
+
+local function vcard_to_text(vcard)
+	local t={};
+	t_insert(t, "BEGIN:VCARD")
+	for i=1,#vcard do
+		t_insert(t, item_to_text(vcard[i]));
+	end
+	t_insert(t, "END:VCARD")
+	return t_concat(t, line_sep);
+end
+
+function to_text(vCards)
+	if vCards[1].name then
+		return vcard_to_text(vCards)
+	else
+		local t = {};
+		for i=1,#vCards do
+			t[i]=vcard_to_text(vCards[i]);
+		end
+		return t_concat(t, line_sep);
+	end
+end
+
+local function from_xep54_item(item)
+	local prop_name = item.name;
+	local prop_def = vCard_dtd[prop_name];
+
+	local prop = { name = prop_name };
+
+	if prop_def == "text" then
+		prop[1] = item:get_text();
+	elseif type(prop_def) == "table" then
+		if prop_def.value then --single item
+			prop[1] = item:get_child_text(prop_def.value) or "";
+		elseif prop_def.values then --array
+			local value_names = prop_def.values;
+			if value_names.behaviour == "repeat-last" then
+				for i=1,#item do
+					t_insert(prop, item[i]:get_text() or "");
+				end
+			else
+				for i=1,#value_names do
+					t_insert(prop, item:get_child_text(value_names[i]) or "");
+				end
+			end
+		elseif prop_def.names then
+			local names = prop_def.names;
+			for i=1,#names do
+				if item:get_child(names[i]) then
+					prop[1] = names[i];
+					break;
+				end
+			end
+		end
+		
+		if prop_def.props_verbatim then
+			for k,v in pairs(prop_def.props_verbatim) do
+				prop[k] = v;
+			end
+		end
+
+		if prop_def.types then
+			local types = prop_def.types;
+			prop.TYPE = {};
+			for i=1,#types do
+				if item:get_child(types[i]) then
+					t_insert(prop.TYPE, types[i]:lower());
+				end
+			end
+			if #prop.TYPE == 0 then
+				prop.TYPE = nil;
+			end
+		end
+
+		-- A key-value pair, within a key-value pair?
+		if prop_def.props then
+			local params = prop_def.props;
+			for i=1,#params do
+				local name = params[i]
+				local data = item:get_child_text(name);
+				if data then
+					prop[name] = prop[name] or {};
+					t_insert(prop[name], data);
+				end
+			end
+		end
+	else
+		return nil
+	end
+
+	return prop;
+end
+
+local function from_xep54_vCard(vCard)
+	local tags = vCard.tags;
+	local t = {};
+	for i=1,#tags do
+		t_insert(t, from_xep54_item(tags[i]));
+	end
+	return t
+end
+
+function from_xep54(vCard)
+	if vCard.attr.xmlns ~= "vcard-temp" then
+		return nil, "wrong-xmlns";
+	end
+	if vCard.name == "xCard" then -- A collection of vCards
+		local t = {};
+		local vCards = vCard.tags;
+		for i=1,#vCards do
+			t[i] = from_xep54_vCard(vCards[i]);
+		end
+		return t
+	elseif vCard.name == "vCard" then -- A single vCard
+		return from_xep54_vCard(vCard)
+	end
+end
+
+-- This was adapted from http://xmpp.org/extensions/xep-0054.html#dtd
+vCard_dtd = {
+	VERSION = "text", --MUST be 3.0, so parsing is redundant
+	FN = "text",
+	N = {
+		values = {
+			"FAMILY",
+			"GIVEN",
+			"MIDDLE",
+			"PREFIX",
+			"SUFFIX",
+		},
+	},
+	NICKNAME = "text",
+	PHOTO = {
+		props_verbatim = { ENCODING = { "b" } },
+		props = { "TYPE" },
+		value = "BINVAL", --{ "EXTVAL", },
+	},
+	BDAY = "text",
+	ADR = {
+		types = {
+			"HOME",
+			"WORK", 
+			"POSTAL", 
+			"PARCEL", 
+			"DOM",
+			"INTL",
+			"PREF", 
+		},
+		values = {
+			"POBOX",
+			"EXTADD",
+			"STREET",
+			"LOCALITY",
+			"REGION",
+			"PCODE",
+			"CTRY",
+		}
+	},
+	LABEL = {
+		types = {
+			"HOME", 
+			"WORK", 
+			"POSTAL", 
+			"PARCEL", 
+			"DOM",
+			"INTL", 
+			"PREF", 
+		},
+		value = "LINE",
+	},
+	TEL = {
+		types = {
+			"HOME", 
+			"WORK", 
+			"VOICE", 
+			"FAX", 
+			"PAGER", 
+			"MSG", 
+			"CELL", 
+			"VIDEO", 
+			"BBS", 
+			"MODEM", 
+			"ISDN", 
+			"PCS", 
+			"PREF", 
+		},
+		value = "NUMBER",
+	},
+	EMAIL = {
+		types = {
+			"HOME", 
+			"WORK", 
+			"INTERNET", 
+			"PREF", 
+			"X400", 
+		},
+		value = "USERID",
+	},
+	JABBERID = "text",
+	MAILER = "text",
+	TZ = "text",
+	GEO = {
+		values = {
+			"LAT",
+			"LON",
+		},
+	},
+	TITLE = "text",
+	ROLE = "text",
+	LOGO = "copy of PHOTO",
+	AGENT = "text",
+	ORG = {
+		values = {
+			behaviour = "repeat-last",
+			"ORGNAME",
+			"ORGUNIT",
+		}
+	},
+	CATEGORIES = {
+		values = "KEYWORD",
+	},
+	NOTE = "text",
+	PRODID = "text",
+	REV = "text",
+	SORTSTRING = "text",
+	SOUND = "copy of PHOTO",
+	UID = "text",
+	URL = "text",
+	CLASS = {
+		names = { -- The item.name is the value if it's one of these.
+			"PUBLIC",
+			"PRIVATE",
+			"CONFIDENTIAL",
+		},
+	},
+	KEY = {
+		props = { "TYPE" },
+		value = "CRED",
+	},
+	DESC = "text",
+};
+vCard_dtd.LOGO = vCard_dtd.PHOTO;
+vCard_dtd.SOUND = vCard_dtd.PHOTO;
+
+return {
+	from_text = from_text;
+	to_text = to_text;
+
+	from_xep54 = from_xep54;
+	to_xep54 = to_xep54;
+
+	-- COMPAT:
+	lua_to_text = to_text;
+	lua_to_xep54 = to_xep54;
+
+	text_to_lua = from_text;
+	text_to_xep54 = function (...) return to_xep54(from_text(...)); end;
+
+	xep54_to_lua = from_xep54;
+	xep54_to_text = function (...) return to_text(from_xep54(...)) end;
+};