Diff

util/stanza.lua @ 11120:b2331f3dfeea

Merge 0.11->trunk
author Matthew Wild <mwild1@gmail.com>
date Wed, 30 Sep 2020 09:50:33 +0100
parent 11088:1f84d0e4d0c4
child 11206:f051394762ff
line wrap: on
line diff
--- a/util/stanza.lua	Thu Oct 01 15:08:58 2020 +0100
+++ b/util/stanza.lua	Wed Sep 30 09:50:33 2020 +0100
@@ -98,7 +98,7 @@
 end
 
 function stanza_mt:body(text, attr)
-	return self:tag("body", attr):text(text);
+	return self:text_tag("body", text, attr);
 end
 
 function stanza_mt:text_tag(name, text, attr, namespaces)
@@ -270,6 +270,34 @@
 	until not self
 end
 
+local function _clone(stanza, only_top)
+	local attr, tags = {}, {};
+	for k,v in pairs(stanza.attr) do attr[k] = v; end
+	local old_namespaces, namespaces = stanza.namespaces;
+	if old_namespaces then
+		namespaces = {};
+		for k,v in pairs(old_namespaces) do namespaces[k] = v; end
+	end
+	local new = { name = stanza.name, attr = attr, namespaces = namespaces, tags = tags };
+	if not only_top then
+		for i=1,#stanza do
+			local child = stanza[i];
+			if child.name then
+				child = _clone(child);
+				t_insert(tags, child);
+			end
+			t_insert(new, child);
+		end
+	end
+	return setmetatable(new, stanza_mt);
+end
+
+local function clone(stanza, only_top)
+	if not is_stanza(stanza) then
+		error("bad argument to clone: expected stanza, got "..type(stanza));
+	end
+	return _clone(stanza, only_top);
+end
 
 local escape_table = { ["'"] = "&apos;", ["\""] = "&quot;", ["<"] = "&lt;", [">"] = "&gt;", ["&"] = "&amp;" };
 local function xml_escape(str) return (s_gsub(str, "['&<>\"]", escape_table)); end
@@ -310,11 +338,8 @@
 end
 
 function stanza_mt.top_tag(t)
-	local attr_string = "";
-	if t.attr then
-		for k, v in pairs(t.attr) do if type(k) == "string" then attr_string = attr_string .. s_format(" %s='%s'", k, xml_escape(tostring(v))); end end
-	end
-	return s_format("<%s%s>", t.name, attr_string);
+	local top_tag_clone = clone(t, true);
+	return tostring(top_tag_clone):sub(1,-3)..">";
 end
 
 function stanza_mt.get_text(t)
@@ -324,11 +349,11 @@
 end
 
 function stanza_mt.get_error(stanza)
-	local error_type, condition, text;
+	local error_type, condition, text, extra_tag;
 
 	local error_tag = stanza:get_child("error");
 	if not error_tag then
-		return nil, nil, nil;
+		return nil, nil, nil, nil;
 	end
 	error_type = error_tag.attr.type;
 
@@ -339,12 +364,14 @@
 			elseif not condition then
 				condition = child.name;
 			end
-			if condition and text then
-				break;
-			end
+		else
+			extra_tag = child;
+		end
+		if condition and text and extra_tag then
+			break;
 		end
 	end
-	return error_type, condition or "undefined-condition", text;
+	return error_type, condition or "undefined-condition", text, extra_tag;
 end
 
 local function preserialize(stanza)
@@ -388,50 +415,32 @@
 	end
 end
 
-local function _clone(stanza)
-	local attr, tags = {}, {};
-	for k,v in pairs(stanza.attr) do attr[k] = v; end
-	local old_namespaces, namespaces = stanza.namespaces;
-	if old_namespaces then
-		namespaces = {};
-		for k,v in pairs(old_namespaces) do namespaces[k] = v; end
-	end
-	local new = { name = stanza.name, attr = attr, namespaces = namespaces, tags = tags };
-	for i=1,#stanza do
-		local child = stanza[i];
-		if child.name then
-			child = _clone(child);
-			t_insert(tags, child);
-		end
-		t_insert(new, child);
-	end
-	return setmetatable(new, stanza_mt);
-end
-
-local function clone(stanza)
-	if not is_stanza(stanza) then
-		error("bad argument to clone: expected stanza, got "..type(stanza));
-	end
-	return _clone(stanza);
-end
-
 local function message(attr, body)
 	if not body then
 		return new_stanza("message", attr);
 	else
-		return new_stanza("message", attr):tag("body"):text(body):up();
+		return new_stanza("message", attr):text_tag("body", body);
 	end
 end
 local function iq(attr)
-	if not (attr and attr.id) then
+	if not attr then
+		error("iq stanzas require id and type attributes");
+	end
+	if not attr.id then
 		error("iq stanzas require an id attribute");
 	end
+	if not attr.type then
+		error("iq stanzas require a type attribute");
+	end
 	return new_stanza("iq", attr);
 end
 
 local function reply(orig)
+	if not is_stanza(orig) then
+		error("bad argument to reply: expected stanza, got "..type(orig));
+	end
 	return new_stanza(orig.name,
-		orig.attr and {
+		{
 			to = orig.attr.from,
 			from = orig.attr.to,
 			id = orig.attr.id,
@@ -440,12 +449,37 @@
 end
 
 local xmpp_stanzas_attr = { xmlns = xmlns_stanzas };
-local function error_reply(orig, error_type, condition, error_message)
+local function error_reply(orig, error_type, condition, error_message, error_by)
+	if not is_stanza(orig) then
+		error("bad argument to error_reply: expected stanza, got "..type(orig));
+	elseif orig.attr.type == "error" then
+		error("bad argument to error_reply: got stanza of type error which must not be replied to");
+	end
 	local t = reply(orig);
 	t.attr.type = "error";
-	t:tag("error", {type = error_type}) --COMPAT: Some day xmlns:stanzas goes here
-	:tag(condition, xmpp_stanzas_attr):up();
-	if error_message then t:tag("text", xmpp_stanzas_attr):text(error_message):up(); end
+	local extra;
+	if type(error_type) == "table" then -- an util.error or similar object
+		if type(error_type.extra) == "table" then
+			extra = error_type.extra;
+		end
+		if type(error_type.context) == "table" and type(error_type.context.by) == "string" then error_by = error_type.context.by; end
+		error_type, condition, error_message = error_type.type, error_type.condition, error_type.text;
+	end
+	if t.attr.from == error_by then
+		error_by = nil;
+	end
+	t:tag("error", {type = error_type, by = error_by}) --COMPAT: Some day xmlns:stanzas goes here
+	:tag(condition, xmpp_stanzas_attr);
+	if extra and condition == "gone" and type(extra.uri) == "string" then
+		t:text(extra.uri);
+	end
+	t:up();
+	if error_message then t:text_tag("text", error_message, xmpp_stanzas_attr); end
+	if extra and is_stanza(extra.tag) then
+		t:add_child(extra.tag);
+	elseif extra and extra.namespace and extra.condition then
+		t:tag(extra.condition, { xmlns = extra.namespace }):up();
+	end
 	return t; -- stanza ready for adding app-specific errors
 end
 
@@ -493,6 +527,36 @@
 	stanza_mt.pretty_top_tag = stanza_mt.top_tag;
 end
 
+function stanza_mt.indent(t, level, indent)
+	if #t == 0 or (#t == 1 and type(t[1]) == "string") then
+		-- Empty nodes wouldn't have any indentation
+		-- Text-only nodes are preserved as to not alter the text content
+		-- Optimization: Skip clone of these since we don't alter them
+		return t;
+	end
+
+	indent = indent or "\t";
+	level = level or 1;
+	local tag = clone(t, true);
+
+	for child in t:children() do
+		if type(child) == "string" then
+			-- Already indented text would look weird but let's ignore that for now.
+			if child:find("%S") then
+				tag:text("\n" .. indent:rep(level));
+				tag:text(child);
+			end
+		elseif is_stanza(child) then
+			tag:text("\n" .. indent:rep(level));
+			tag:add_direct_child(child:indent(level+1, indent));
+		end
+	end
+	-- before the closing tag
+	tag:text("\n" .. indent:rep((level-1)));
+
+	return tag;
+end
+
 return {
 	stanza_mt = stanza_mt;
 	stanza = new_stanza;