Changeset

441:e4c0b1d7fd6b

util.dataforms: Update from prosody trunk 5fb6563eee1e keeping from_stanza()
author Kim Alvefur <zash@zash.se>
date Mon, 23 Nov 2020 23:43:58 +0100
parents 440:14071b3a46df
children 442:b2ae91f4fec9
files util/dataforms.lua
diffstat 1 files changed, 183 insertions(+), 77 deletions(-) [+]
line wrap: on
line diff
--- a/util/dataforms.lua	Sat Nov 14 15:24:01 2020 +0100
+++ b/util/dataforms.lua	Mon Nov 23 23:43:58 2020 +0100
@@ -1,30 +1,34 @@
 -- Prosody IM
 -- Copyright (C) 2008-2010 Matthew Wild
 -- Copyright (C) 2008-2010 Waqas Hussain
--- 
+--
 -- This project is MIT/X11 licensed. Please see the
 -- COPYING file in the source package for more information.
 --
 
 local setmetatable = setmetatable;
-local pairs, ipairs = pairs, ipairs;
-local tostring, type, next = tostring, type, next;
+local ipairs = ipairs;
+local type, next = type, next;
+local tonumber = tonumber;
+local tostring = tostring;
 local t_concat = table.concat;
 local st = require "util.stanza";
 local jid_prep = require "util.jid".prep;
 
-module "dataforms"
+local _ENV = nil;
+-- luacheck: std none
 
 local xmlns_forms = 'jabber:x:data';
+local xmlns_validate = 'http://jabber.org/protocol/xdata-validate';
 
 local form_t = {};
 local form_mt = { __index = form_t };
 
-function new(layout)
+local function new(layout)
 	return setmetatable(layout, form_mt);
 end
 
-function from_stanza(stanza)
+local function from_stanza(stanza)
 	local layout = {
 		title = stanza:get_child_text("title");
 		instructions = stanza:get_child_text("instructions");
@@ -63,21 +67,82 @@
 end
 
 function form_t.form(layout, data, formtype)
-	local form = st.stanza("x", { xmlns = xmlns_forms, type = formtype or "form" });
-	if layout.title then
-		form:tag("title"):text(layout.title):up();
+	if not formtype then formtype = "form" end
+	local form = st.stanza("x", { xmlns = xmlns_forms, type = formtype });
+	if formtype == "cancel" then
+		return form;
 	end
-	if layout.instructions then
-		form:tag("instructions"):text(layout.instructions):up();
+	if formtype ~= "submit" then
+		if layout.title then
+			form:tag("title"):text(layout.title):up();
+		end
+		if layout.instructions then
+			form:tag("instructions"):text(layout.instructions):up();
+		end
 	end
-	for n, field in ipairs(layout) do
+	for _, field in ipairs(layout) do
 		local field_type = field.type or "text-single";
 		-- Add field tag
-		form:tag("field", { type = field_type, var = field.name, label = field.label });
+		form:tag("field", { type = field_type, var = field.var or field.name, label = formtype ~= "submit" and field.label or nil });
+
+		if formtype ~= "submit" then
+			if field.desc then
+				form:text_tag("desc", field.desc);
+			end
+		end
+
+		if formtype == "form" and field.datatype then
+			form:tag("validate", { xmlns = xmlns_validate, datatype = field.datatype });
+			if field.range_min or field.range_max then
+				form:tag("range", {
+						min = field.range_min and tostring(field.range_min),
+						max = field.range_max and tostring(field.range_max),
+					}):up();
+			end
+			-- <basic/> assumed
+			form:up();
+		end
+
+
+		local value = field.value;
+		local options = field.options;
+
+		if data and data[field.name] ~= nil then
+			value = data[field.name];
 
-		local value = (data and data[field.name]) or field.value;
-		
-		if value then
+			if formtype == "form" and type(value) == "table"
+				and (field_type == "list-single" or field_type == "list-multi") then
+				-- Allow passing dynamically generated options as values
+				options, value = value, nil;
+			end
+		end
+
+		if formtype == "form" and options then
+			local defaults = {};
+			for _, val in ipairs(options) do
+				if type(val) == "table" then
+					form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
+					if val.default then
+						defaults[#defaults+1] = val.value;
+					end
+				else
+					form:tag("option", { label= val }):tag("value"):text(val):up():up();
+				end
+			end
+			if not value then
+				if field_type == "list-single" then
+					value = defaults[1];
+				elseif field_type == "list-multi" then
+					value = defaults;
+				end
+			end
+		end
+
+		if value ~= nil then
+			if type(value) == "number" then
+				-- TODO validate that this is ok somehow, eg check field.datatype
+				value = ("%g"):format(value);
+			end
 			-- Add value, depending on type
 			if field_type == "hidden" then
 				if type(value) == "table" then
@@ -86,12 +151,12 @@
 						:add_child(value)
 						:up();
 				else
-					form:tag("value"):text(tostring(value)):up();
+					form:tag("value"):text(value):up();
 				end
 			elseif field_type == "boolean" then
 				form:tag("value"):text((value and "1") or "0"):up();
 			elseif field_type == "fixed" then
-				
+				form:tag("value"):text(value):up();
 			elseif field_type == "jid-multi" then
 				for _, jid in ipairs(value) do
 					form:tag("value"):text(jid):up();
@@ -106,36 +171,27 @@
 					form:tag("value"):text(line):up();
 				end
 			elseif field_type == "list-single" then
-				local has_default = false;
-				for _, val in ipairs(value) do
-					if type(val) == "table" then
-						form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
-						if val.default and (not has_default) then
-							form:tag("value"):text(val.value):up();
-							has_default = true;
-						end
-					else
-						form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up();
-					end
-				end
+				form:tag("value"):text(value):up();
 			elseif field_type == "list-multi" then
 				for _, val in ipairs(value) do
-					if type(val) == "table" then
-						form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
-						if val.default then
-							form:tag("value"):text(val.value):up();
-						end
-					else
-						form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up();
-					end
+					form:tag("value"):text(val):up();
 				end
 			end
 		end
-		
-		if field.required then
+
+		local media = field.media;
+		if media then
+			form:tag("media", { xmlns = "urn:xmpp:media-element", height = ("%g"):format(media.height), width = ("%g"):format(media.width) });
+			for _, val in ipairs(media) do
+				form:tag("uri", { type = val.type }):text(val.uri):up()
+			end
+			form:up();
+		end
+
+		if formtype == "form" and field.required then
 			form:tag("required"):up();
 		end
-		
+
 		-- Jump back up to list of fields
 		form:up();
 	end
@@ -143,61 +199,75 @@
 end
 
 local field_readers = {};
+local data_validators = {};
 
-function form_t.data(layout, stanza)
+function form_t.data(layout, stanza, current)
 	local data = {};
 	local errors = {};
+	local present = {};
 
 	for _, field in ipairs(layout) do
 		local tag;
-		for field_tag in stanza:childtags() do
-			if field.name == field_tag.attr.var then
+		for field_tag in stanza:childtags("field") do
+			if (field.var or field.name) == field_tag.attr.var then
 				tag = field_tag;
 				break;
 			end
 		end
 
 		if not tag then
-			if field.required then
+			if current and current[field.name] ~= nil then
+				data[field.name] = current[field.name];
+			elseif field.required then
 				errors[field.name] = "Required value missing";
 			end
-		else
+		elseif field.name then
+			present[field.name] = true;
 			local reader = field_readers[field.type];
 			if reader then
-				data[field.name], errors[field.name] = reader(tag, field.required);
+				local value, err = reader(tag, field.required);
+				local validator = field.datatype and data_validators[field.datatype];
+				if value ~= nil and validator then
+					local valid, ret = validator(value, field);
+					if valid then
+						value = ret;
+					else
+						value, err = nil, ret or ("Invalid value for data of type " .. field.datatype);
+					end
+				end
+				data[field.name], errors[field.name] = value, err;
 			end
 		end
 	end
 	if next(errors) then
-		return data, errors;
+		return data, errors, present;
 	end
-	return data;
+	return data, nil, present;
 end
 
-field_readers["text-single"] =
-	function (field_tag, required)
-		local data = field_tag:get_child_text("value");
-		if data and #data > 0 then
-			return data
-		elseif required then
-			return nil, "Required value missing";
-		end
+local function simple_text(field_tag, required)
+	local data = field_tag:get_child_text("value");
+	-- XEP-0004 does not say if an empty string is acceptable for a required value
+	-- so we will follow HTML5 which says that empty string means missing
+	if required and (data == nil or data == "") then
+		return nil, "Required value missing";
 	end
+	return data; -- Return whatever get_child_text returned, even if empty string
+end
 
-field_readers["text-private"] =
-	field_readers["text-single"];
+field_readers["text-single"] = simple_text;
+
+field_readers["text-private"] = simple_text;
 
 field_readers["jid-single"] =
 	function (field_tag, required)
-		local raw_data = field_tag:get_child_text("value")
+		local raw_data, err = simple_text(field_tag, required);
+		if not raw_data then return raw_data, err; end
 		local data = jid_prep(raw_data);
-		if data and #data > 0 then
-			return data
-		elseif raw_data then
+		if not data then
 			return nil, "Invalid JID: " .. raw_data;
-		elseif required then
-			return nil, "Required value missing";
 		end
+		return data;
 	end
 
 field_readers["jid-multi"] =
@@ -225,7 +295,11 @@
 		for value in field_tag:childtags("value") do
 			result[#result+1] = value:get_text();
 		end
-		return result, (required and #result == 0 and "Required value missing" or nil);
+		if #result > 0 then
+			return result;
+		elseif required then
+			return nil, "Required value missing";
+		end
 	end
 
 field_readers["text-multi"] =
@@ -237,8 +311,7 @@
 		return data, err;
 	end
 
-field_readers["list-single"] =
-	field_readers["text-single"];
+field_readers["list-single"] = simple_text;
 
 local boolean_values = {
 	["1"] = true, ["true"] = true,
@@ -247,15 +320,13 @@
 
 field_readers["boolean"] =
 	function (field_tag, required)
-		local raw_value = field_tag:get_child_text("value");
-		local value = boolean_values[raw_value ~= nil and raw_value];
-		if value ~= nil then
-			return value;
-		elseif raw_value then
-			return nil, "Invalid boolean representation";
-		elseif required then
-			return nil, "Required value missing";
+		local raw_value, err = simple_text(field_tag, required);
+		if not raw_value then return raw_value, err; end
+		local value = boolean_values[raw_value];
+		if value == nil then
+			return nil, "Invalid boolean representation:" .. raw_value;
 		end
+		return value;
 	end
 
 field_readers["hidden"] =
@@ -263,7 +334,42 @@
 		return field_tag:get_child_text("value");
 	end
 
-return _M;
+data_validators["xs:integer"] =
+	function (data, field)
+		local n = tonumber(data);
+		if not n then
+			return false, "not a number";
+		elseif n % 1 ~= 0 then
+			return false, "not an integer";
+		end
+		if field.range_max and n > field.range_max then
+			return false, "out of bounds";
+		elseif field.range_min and n < field.range_min then
+			return false, "out of bounds";
+		end
+		return true, n;
+	end
+
+
+local function get_form_type(form)
+	if not st.is_stanza(form) then
+		return nil, "not a stanza object";
+	elseif form.attr.xmlns ~= "jabber:x:data" or form.name ~= "x" then
+		return nil, "not a dataform element";
+	end
+	for field in form:childtags("field") do
+		if field.attr.var == "FORM_TYPE" then
+			return field:get_child_text("value");
+		end
+	end
+	return "";
+end
+
+return {
+	new = new;
+	from_stanza = from_stanza;
+	get_type = get_form_type;
+};
 
 
 --[=[