util/dataforms.lua

1
-- Prosody IM
2
-- Copyright (C) 2008-2010 Matthew Wild
3
-- Copyright (C) 2008-2010 Waqas Hussain
4
--
5
-- This project is MIT/X11 licensed. Please see the
6
-- COPYING file in the source package for more information.
7
--
8
 
9
local setmetatable = setmetatable;
10
local ipairs = ipairs;
11
local type, next = type, next;
12
local tonumber = tonumber;
13
local tostring = tostring;
14
local t_concat = table.concat;
15
local st = require "prosody.util.stanza";
16
local jid_prep = require "prosody.util.jid".prep;
17
 
18
local _ENV = nil;
19
-- luacheck: std none
20
 
21
local xmlns_forms = 'jabber:x:data';
22
local xmlns_validate = 'http://jabber.org/protocol/xdata-validate';
23
 
24
local form_t = {};
25
local form_mt = { __index = form_t };
26
 
27
local function new(layout)
28
	return setmetatable(layout, form_mt);
29
end
30
 
31
local function from_stanza(stanza)
32
	local layout = {
33
		title = stanza:get_child_text("title");
34
		instructions = stanza:get_child_text("instructions");
35
	};
36
	for tag in stanza:childtags("field") do
37
		local field = {
38
			name = tag.attr.var;
39
			label = tag.attr.label;
40
			type = tag.attr.type;
41
			required = tag:get_child("required") and true or nil;
42
			value = tag:get_child_text("value");
43
		};
44
		layout[#layout+1] = field;
45
		if field.type then
46
			local value = {};
47
			if field.type:match"list%-" then
48
				for tag in tag:childtags("option") do
49
					value[#value+1] = { label = tag.attr.label, value = tag:get_child_text("value") };
50
				end
51
				for tag in tag:childtags("value") do
52
					value[#value+1] = { label = tag.attr.label, value = tag:get_text(), default = true };
53
				end
54
			elseif field.type:match"%-multi" then
55
				for tag in tag:childtags("value") do
56
					value[#value+1] = tag.attr.label and { label = tag.attr.label, value = tag:get_text() } or tag:get_text();
57
				end
58
				if field.type == "text-multi" then
59
					field.value = t_concat(value, "\n");
60
				else
61
					field.value = value;
62
				end
63
			end
64
		end
65
		local datatype_tag = tag:get_child("validate", xmlns_validate);
66
		if datatype_tag then
67
			field.datatype = datatype.attr.datatype;
68
			local range_tag = datatype_tag:get_child("range");
69
			if range_tag then
70
				field.range_min = tonumber(range_tag.attr.min);
71
				field.range_max = tonumber(range_tag.attr.max);
72
			end
73
		end
74
 
75
	end
76
	return new(layout);
77
end
78
 
79
function form_t.form(layout, data, formtype)
80
	if not formtype then formtype = "form" end
81
	local form = st.stanza("x", { xmlns = xmlns_forms, type = formtype });
82
	if formtype == "cancel" then
83
		return form;
84
	end
85
	if formtype ~= "submit" then
86
		if layout.title then
87
			form:tag("title"):text(layout.title):up();
88
		end
89
		if layout.instructions then
90
			form:tag("instructions"):text(layout.instructions):up();
91
		end
92
	end
93
	for _, field in ipairs(layout) do
94
		local field_type = field.type or "text-single";
95
		-- Add field tag
96
		form:tag("field", { type = field_type, var = field.var or field.name, label = formtype ~= "submit" and field.label or nil });
97
 
98
		if formtype ~= "submit" then
99
			if field.desc then
100
				form:text_tag("desc", field.desc);
101
			end
102
		end
103
 
104
		if formtype == "form" and field.datatype then
105
			form:tag("validate", { xmlns = xmlns_validate, datatype = field.datatype });
106
			if field.range_min or field.range_max then
107
				form:tag("range", {
108
						min = field.range_min and tostring(field.range_min),
109
						max = field.range_max and tostring(field.range_max),
110
					}):up();
111
			end
112
			-- <basic/> assumed
113
			form:up();
114
		end
115
 
116
 
117
		local value = field.value;
118
		local options = field.options;
119
 
120
		if data and data[field.name] ~= nil then
121
			value = data[field.name];
122
 
123
			if formtype == "form" and type(value) == "table"
124
				and (field_type == "list-single" or field_type == "list-multi") then
125
				-- Allow passing dynamically generated options as values
126
				options, value = value, nil;
127
			end
128
		end
129
 
130
		if formtype == "form" and options then
131
			local defaults = {};
132
			for _, val in ipairs(options) do
133
				if type(val) == "table" then
134
					form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up();
135
					if val.default then
136
						defaults[#defaults+1] = val.value;
137
					end
138
				else
139
					form:tag("option", { label= val }):tag("value"):text(val):up():up();
140
				end
141
			end
142
			if not value then
143
				if field_type == "list-single" then
144
					value = defaults[1];
145
				elseif field_type == "list-multi" then
146
					value = defaults;
147
				end
148
			end
149
		end
150
 
151
		if value ~= nil then
152
			if type(value) == "number" then
153
				-- TODO validate that this is ok somehow, eg check field.datatype
154
				value = ("%g"):format(value);
155
			end
156
			-- Add value, depending on type
157
			if field_type == "hidden" then
158
				if type(value) == "table" then
159
					-- Assume an XML snippet
160
					form:tag("value")
161
						:add_child(value)
162
						:up();
163
				else
164
					form:tag("value"):text(value):up();
165
				end
166
			elseif field_type == "boolean" then
167
				form:tag("value"):text((value and "1") or "0"):up();
168
			elseif field_type == "fixed" then
169
				form:tag("value"):text(value):up();
170
			elseif field_type == "jid-multi" then
171
				for _, jid in ipairs(value) do
172
					form:tag("value"):text(jid):up();
173
				end
174
			elseif field_type == "jid-single" then
175
				form:tag("value"):text(value):up();
176
			elseif field_type == "text-single" or field_type == "text-private" then
177
				form:tag("value"):text(value):up();
178
			elseif field_type == "text-multi" then
179
				-- Split into multiple <value> tags, one for each line
180
				for line in value:gmatch("([^\r\n]+)\r?\n*") do
181
					form:tag("value"):text(line):up();
182
				end
183
			elseif field_type == "list-single" then
184
				form:tag("value"):text(value):up();
185
			elseif field_type == "list-multi" then
186
				for _, val in ipairs(value) do
187
					form:tag("value"):text(val):up();
188
				end
189
			end
190
		end
191
 
192
		local media = field.media;
193
		if media then
194
			form:tag("media", { xmlns = "urn:xmpp:media-element", height = ("%g"):format(media.height), width = ("%g"):format(media.width) });
195
			for _, val in ipairs(media) do
196
				form:tag("uri", { type = val.type }):text(val.uri):up()
197
			end
198
			form:up();
199
		end
200
 
201
		if formtype == "form" and field.required then
202
			form:tag("required"):up();
203
		end
204
 
205
		-- Jump back up to list of fields
206
		form:up();
207
	end
208
	return form;
209
end
210
 
211
local field_readers = {};
212
local data_validators = {};
213
 
214
function form_t.data(layout, stanza, current)
215
	local data = {};
216
	local errors = {};
217
	local present = {};
218
 
219
	for _, field in ipairs(layout) do
220
		local tag;
221
		for field_tag in stanza:childtags("field") do
222
			if (field.var or field.name) == field_tag.attr.var then
223
				tag = field_tag;
224
				break;
225
			end
226
		end
227
 
228
		if not tag then
229
			if current and current[field.name] ~= nil then
230
				data[field.name] = current[field.name];
231
			elseif field.required then
232
				errors[field.name] = "Required value missing";
233
			end
234
		elseif field.name then
235
			present[field.name] = true;
236
			local reader = field_readers[field.type];
237
			if reader then
238
				local value, err = reader(tag, field.required);
239
				local validator = field.datatype and data_validators[field.datatype];
240
				if value ~= nil and validator then
241
					local valid, ret = validator(value, field);
242
					if valid then
243
						value = ret;
244
					else
245
						value, err = nil, ret or ("Invalid value for data of type " .. field.datatype);
246
					end
247
				end
248
				data[field.name], errors[field.name] = value, err;
249
			end
250
		end
251
	end
252
	if next(errors) then
253
		return data, errors, present;
254
	end
255
	return data, nil, present;
256
end
257
 
258
local function simple_text(field_tag, required)
259
	local data = field_tag:get_child_text("value");
260
	-- XEP-0004 does not say if an empty string is acceptable for a required value
261
	-- so we will follow HTML5 which says that empty string means missing
262
	if required and (data == nil or data == "") then
263
		return nil, "Required value missing";
264
	end
265
	return data; -- Return whatever get_child_text returned, even if empty string
266
end
267
 
268
field_readers["text-single"] = simple_text;
269
 
270
field_readers["text-private"] = simple_text;
271
 
272
field_readers["jid-single"] =
273
	function (field_tag, required)
274
		local raw_data, err = simple_text(field_tag, required);
275
		if not raw_data then return raw_data, err; end
276
		local data = jid_prep(raw_data);
277
		if not data then
278
			return nil, "Invalid JID: " .. raw_data;
279
		end
280
		return data;
281
	end
282
 
283
field_readers["jid-multi"] =
284
	function (field_tag, required)
285
		local result = {};
286
		local err = {};
287
		for value_tag in field_tag:childtags("value") do
288
			local raw_value = value_tag:get_text();
289
			local value = jid_prep(raw_value);
290
			result[#result+1] = value;
291
			if raw_value and not value then
292
				err[#err+1] = ("Invalid JID: " .. raw_value);
293
			end
294
		end
295
		if #result > 0 then
296
			return result, (#err > 0 and t_concat(err, "\n") or nil);
297
		elseif required then
298
			return nil, "Required value missing";
299
		end
300
	end
301
 
302
field_readers["list-multi"] =
303
	function (field_tag, required)
304
		local result = {};
305
		for value in field_tag:childtags("value") do
306
			result[#result+1] = value:get_text();
307
		end
308
		if #result > 0 then
309
			return result;
310
		elseif required then
311
			return nil, "Required value missing";
312
		end
313
	end
314
 
315
field_readers["text-multi"] =
316
	function (field_tag, required)
317
		local data, err = field_readers["list-multi"](field_tag, required);
318
		if data then
319
			data = t_concat(data, "\n");
320
		end
321
		return data, err;
322
	end
323
 
324
field_readers["list-single"] = simple_text;
325
 
326
local boolean_values = {
327
	["1"] = true, ["true"] = true,
328
	["0"] = false, ["false"] = false,
329
};
330
 
331
field_readers["boolean"] =
332
	function (field_tag, required)
333
		local raw_value, err = simple_text(field_tag, required);
334
		if not raw_value then return raw_value, err; end
335
		local value = boolean_values[raw_value];
336
		if value == nil then
337
			return nil, "Invalid boolean representation:" .. raw_value;
338
		end
339
		return value;
340
	end
341
 
342
field_readers["hidden"] =
343
	function (field_tag)
344
		return field_tag:get_child_text("value");
345
	end
346
 
347
data_validators["xs:integer"] =
348
	function (data, field)
349
		local n = tonumber(data);
350
		if not n then
351
			return false, "not a number";
352
		elseif n % 1 ~= 0 then
353
			return false, "not an integer";
354
		end
355
		if field.range_max and n > field.range_max then
356
			return false, "out of bounds";
357
		elseif field.range_min and n < field.range_min then
358
			return false, "out of bounds";
359
		end
360
		return true, n;
361
	end
362
 
363
 
364
local function get_form_type(form)
365
	if not st.is_stanza(form) then
366
		return nil, "not a stanza object";
367
	elseif form.attr.xmlns ~= "jabber:x:data" or form.name ~= "x" then
368
		return nil, "not a dataform element";
369
	end
370
	for field in form:childtags("field") do
371
		if field.attr.var == "FORM_TYPE" then
372
			return field:get_child_text("value");
373
		end
374
	end
375
	return "";
376
end
377
 
378
return {
379
	new = new;
380
	from_stanza = from_stanza;
381
	get_type = get_form_type;
382
};
383
 
384
 
385
--[=[
386
 
387
Layout:
388
{
389
 
390
	title = "MUC Configuration",
391
	instructions = [[Use this form to configure options for this MUC room.]],
392
 
393
	{ name = "FORM_TYPE", type = "hidden", required = true };
394
	{ name = "field-name", type = "field-type", required = false };
395
}
396
 
397
 
398
--]=]