plugins/mod_storage_xep0227.lua
author Kim Alvefur <zash@zash.se>
Tue, 22 Nov 2022 23:56:01 +0100
branch0.11
changeset 12801 be09ac8300a7
parent 8355 6ff50541d2a6
child 11793 f3085620b6ff
permissions -rw-r--r--
util.stanza: Allow U+7F Allowed by XML despite arguably being a control character. Drops the part of the range meant to rule out octets invalid in UTF-8 (\247 starts a 4-byte sequence), since UTF-8 correctness is validated by util.encodings.utf8.valid().


local ipairs, pairs = ipairs, pairs;
local setmetatable = setmetatable;
local tostring = tostring;
local next = next;
local t_remove = table.remove;
local os_remove = os.remove;
local io_open = io.open;

local paths = require"util.paths";
local st = require "util.stanza";
local parse_xml_real = require "util.xml".parse;

local function getXml(user, host)
	local jid = user.."@"..host;
	local path = paths.join(prosody.paths.data, jid..".xml");
	local f = io_open(path);
	if not f then return; end
	local s = f:read("*a");
	f:close();
	return parse_xml_real(s);
end
local function setXml(user, host, xml)
	local jid = user.."@"..host;
	local path = paths.join(prosody.paths.data, jid..".xml");
	local f, err = io_open(path, "w");
	if not f then return f, err; end
	if xml then
		local s = tostring(xml);
		f:write(s);
		f:close();
		return true;
	else
		f:close();
		return os_remove(path);
	end
end
local function getUserElement(xml)
	if xml and xml.name == "server-data" then
		local host = xml.tags[1];
		if host and host.name == "host" then
			local user = host.tags[1];
			if user and user.name == "user" then
				return user;
			end
		end
	end
end
local function createOuterXml(user, host)
	return st.stanza("server-data", {xmlns='urn:xmpp:pie:0'})
		:tag("host", {jid=host})
			:tag("user", {name = user});
end
local function removeFromArray(array, value)
	for i,item in ipairs(array) do
		if item == value then
			t_remove(array, i);
			return;
		end
	end
end
local function removeStanzaChild(s, child)
	removeFromArray(s.tags, child);
	removeFromArray(s, child);
end

local handlers = {};

-- In order to support mod_auth_internal_hashed
local extended = "http://prosody.im/protocol/extended-xep0227\1";

handlers.accounts = {
	get = function(self, user)
		user = getUserElement(getXml(user, self.host));
		if user and user.attr.password then
			return { password = user.attr.password };
		elseif user then
			local data = {};
			for k, v in pairs(user.attr) do
				if k:sub(1, #extended) == extended then
					data[k:sub(#extended+1)] = v;
				end
			end
			return data;
		end
	end;
	set = function(self, user, data)
		if data then
			local xml = getXml(user, self.host);
			if not xml then xml = createOuterXml(user, self.host); end
			local usere = getUserElement(xml);
			for k, v in pairs(data) do
				if k == "password" then
					usere.attr.password = v;
				else
					usere.attr[extended..k] = v;
				end
			end
			return setXml(user, self.host, xml);
		else
			return setXml(user, self.host, nil);
		end
	end;
};
handlers.vcard = {
	get = function(self, user)
		user = getUserElement(getXml(user, self.host));
		if user then
			local vcard = user:get_child("vCard", 'vcard-temp');
			if vcard then
				return st.preserialize(vcard);
			end
		end
	end;
	set = function(self, user, data)
		local xml = getXml(user, self.host);
		local usere = xml and getUserElement(xml);
		if usere then
			local vcard = usere:get_child("vCard", 'vcard-temp');
			if vcard then
				removeStanzaChild(usere, vcard);
			elseif not data then
				return true;
			end
			if data then
				vcard = st.deserialize(data);
				usere:add_child(vcard);
			end
			return setXml(user, self.host, xml);
		end
		return true;
	end;
};
handlers.private = {
	get = function(self, user)
		user = getUserElement(getXml(user, self.host));
		if user then
			local private = user:get_child("query", "jabber:iq:private");
			if private then
				local r = {};
				for _, tag in ipairs(private.tags) do
					r[tag.name..":"..tag.attr.xmlns] = st.preserialize(tag);
				end
				return r;
			end
		end
	end;
	set = function(self, user, data)
		local xml = getXml(user, self.host);
		local usere = xml and getUserElement(xml);
		if usere then
			local private = usere:get_child("query", 'jabber:iq:private');
			if private then removeStanzaChild(usere, private); end
			if data and next(data) ~= nil then
				private = st.stanza("query", {xmlns='jabber:iq:private'});
				for _,tag in pairs(data) do
					private:add_child(st.deserialize(tag));
				end
				usere:add_child(private);
			end
			return setXml(user, self.host, xml);
		end
		return true;
	end;
};

handlers.roster = {
	get = function(self, user)
		user = getUserElement(getXml(user, self.host));
		if user then
			local roster = user:get_child("query", "jabber:iq:roster");
			if roster then
				local r = {
					[false] = {
						version = roster.attr.version;
						pending = {};
					}
				};
				for item in roster:childtags("item") do
					r[item.attr.jid] = {
						jid = item.attr.jid,
						subscription = item.attr.subscription,
						ask = item.attr.ask,
						name = item.attr.name,
						groups = {};
					};
					for group in item:childtags("group") do
						r[item.attr.jid].groups[group:get_text()] = true;
					end
					for pending in user:childtags("presence", "jabber:client") do
						r[false].pending[pending.attr.from] = true;
					end
				end
				return r;
			end
		end
	end;
	set = function(self, user, data)
		local xml = getXml(user, self.host);
		local usere = xml and getUserElement(xml);
		if usere then
			local roster = usere:get_child("query", 'jabber:iq:roster');
			if roster then removeStanzaChild(usere, roster); end
			usere:maptags(function (tag)
				if tag.attr.xmlns == "jabber:client" and tag.name == "presence" and tag.attr.type == "subscribe" then
					return nil;
				end
				return tag;
			end);
			if data and next(data) ~= nil then
				roster = st.stanza("query", {xmlns='jabber:iq:roster'});
				usere:add_child(roster);
				for jid, item in pairs(data) do
					if jid then
						roster:tag("item", {
							jid = jid,
							subscription = item.subscription,
							ask = item.ask,
							name = item.name,
						});
						for group in pairs(item.groups) do
							roster:tag("group"):text(group):up();
						end
						roster:up(); -- move out from item
					else
						roster.attr.version = item.version;
						for pending_jid in pairs(item.pending) do
							usere:add_child(st.presence({ from = pending_jid, type = "subscribe" }));
						end
					end
				end
			end
			return setXml(user, self.host, xml);
		end
		return true;
	end;
};


-----------------------------
local driver = {};

function driver:open(datastore, typ) -- luacheck: ignore 212/self
	if typ and typ ~= "keyval" then return nil, "unsupported-store"; end
	local handler = handlers[datastore];
	if not handler then return nil, "unsupported-datastore"; end
	local instance = setmetatable({ host = module.host; datastore = datastore; }, { __index = handler });
	if instance.init then instance:init(); end
	return instance;
end

module:provides("storage", driver);