util/vcard.lua

1
-- Copyright (C) 2011-2012 Kim Alvefur
2
-- 
3
-- This project is MIT/X11 licensed. Please see the
4
-- COPYING file in the source package for more information.
5
--
6
 
7
-- TODO
8
-- Fix folding.
9
 
10
local st = require "prosody.util.stanza";
11
local t_insert, t_concat = table.insert, table.concat;
12
local type = type;
13
local next, pairs, ipairs = next, pairs, ipairs;
14
 
15
local from_text, to_text, from_xep54, to_xep54;
16
 
17
local line_sep = "\n";
18
 
19
local vCard_dtd; -- See end of file
20
 
21
local function fold_line()
22
	error "Not implemented" --TODO
23
end
24
local function unfold_line()
25
	error "Not implemented"
26
	-- gsub("\r?\n[ \t]([^\r\n])", "%1");
27
end
28
 
29
local function vCard_esc(s)
30
	return s:gsub("[,:;\\]", "\\%1"):gsub("\n","\\n");
31
end
32
 
33
local function vCard_unesc(s)
34
	return s:gsub("\\?[\\nt:;,]", {
35
		["\\\\"] = "\\",
36
		["\\n"] = "\n",
37
		["\\r"] = "\r",
38
		["\\t"] = "\t",
39
		["\\:"] = ":", -- FIXME Shouldn't need to espace : in values, just params
40
		["\\;"] = ";",
41
		["\\,"] = ",",
42
		[":"] = "\29",
43
		[";"] = "\30",
44
		[","] = "\31",
45
	});
46
end
47
 
48
local function item_to_xep54(item)
49
	local t = st.stanza(item.name, { xmlns = "vcard-temp" });
50
 
51
	local prop_def = vCard_dtd[item.name];
52
	if prop_def == "text" then
53
		t:text(item[1]);
54
	elseif type(prop_def) == "table" then
55
		if prop_def.types and item.TYPE then
56
			if type(item.TYPE) == "table" then
57
				for _,v in pairs(prop_def.types) do
58
					for _,typ in pairs(item.TYPE) do
59
						if typ:upper() == v then
60
							t:tag(v):up();
61
							break;
62
						end
63
					end
64
				end
65
			else
66
				t:tag(item.TYPE:upper()):up();
67
			end
68
		end
69
 
70
		if prop_def.props then
71
			for _,v in pairs(prop_def.props) do
72
				if item[v] then
73
					t:tag(v):up();
74
				end
75
			end
76
		end
77
 
78
		if prop_def.value then
79
			t:tag(prop_def.value):text(item[1]):up();
80
		elseif prop_def.values then
81
			local prop_def_values = prop_def.values;
82
			local repeat_last = prop_def_values.behaviour == "repeat-last" and prop_def_values[#prop_def_values];
83
			for i=1,#item do
84
				t:tag(prop_def.values[i] or repeat_last):text(item[i]):up();
85
			end
86
		end
87
	end
88
 
89
	return t;
90
end
91
 
92
local function vcard_to_xep54(vCard)
93
	local t = st.stanza("vCard", { xmlns = "vcard-temp" });
94
	for i=1,#vCard do
95
		t:add_child(item_to_xep54(vCard[i]));
96
	end
97
	return t;
98
end
99
 
100
function to_xep54(vCards)
101
	if not vCards[1] or vCards[1].name then
102
		return vcard_to_xep54(vCards)
103
	else
104
		local t = st.stanza("xCard", { xmlns = "vcard-temp" });
105
		for i=1,#vCards do
106
			t:add_child(vcard_to_xep54(vCards[i]));
107
		end
108
		return t;
109
	end
110
end
111
 
112
function from_text(data)
113
	data = data -- unfold and remove empty lines
114
		:gsub("\r\n","\n")
115
		:gsub("\n ", "")
116
		:gsub("\n\n+","\n");
117
	local vCards = {};
118
	local c; -- current item
119
	for line in data:gmatch("[^\n]+") do
120
		local line = vCard_unesc(line);
121
		local name, params, value = line:match("^([-%a]+)(\30?[^\29]*)\29(.*)$");
122
		value = value:gsub("\29",":");
123
		if #params > 0 then
124
			local _params = {};
125
			for k,isval,v in params:gmatch("\30([^=]+)(=?)([^\30]*)") do
126
				k = k:upper();
127
				local _vt = {};
128
				for _p in v:gmatch("[^\31]+") do
129
					_vt[#_vt+1]=_p
130
					_vt[_p]=true;
131
				end
132
				if isval == "=" then
133
					_params[k]=_vt;
134
				else
135
					_params[k]=true;
136
				end
137
			end
138
			params = _params;
139
		end
140
		if name == "BEGIN" and value == "VCARD" then
141
			c = {};
142
			vCards[#vCards+1] = c;
143
		elseif name == "END" and value == "VCARD" then
144
			c = nil;
145
		elseif c and vCard_dtd[name] then
146
			local dtd = vCard_dtd[name];
147
			local p = { name = name };
148
			c[#c+1]=p;
149
			--c[name]=p;
150
			local up = c;
151
			c = p;
152
			if dtd.types then
153
				for _, t in ipairs(dtd.types) do
154
					local t = t:lower();
155
					if ( params.TYPE and params.TYPE[t] == true)
156
							or params[t] == true then
157
						c.TYPE=t;
158
					end
159
				end
160
			end
161
			if dtd.props then
162
				for _, p in ipairs(dtd.props) do
163
					if params[p] then
164
						if params[p] == true then
165
							c[p]=true;
166
						else
167
							for _, prop in ipairs(params[p]) do
168
								c[p]=prop;
169
							end
170
						end
171
					end
172
				end
173
			end
174
			if dtd == "text" or dtd.value then
175
				t_insert(c, value);
176
			elseif dtd.values then
177
				local value = "\30"..value;
178
				for p in value:gmatch("\30([^\30]*)") do
179
					t_insert(c, p);
180
				end
181
			end
182
			c = up;
183
		end
184
	end
185
	return vCards;
186
end
187
 
188
local function item_to_text(item)
189
	local value = {};
190
	for i=1,#item do
191
		value[i] = vCard_esc(item[i]);
192
	end
193
	value = t_concat(value, ";");
194
 
195
	local params = "";
196
	for k,v in pairs(item) do
197
		if type(k) == "string" and k ~= "name" then
198
			params = params .. (";%s=%s"):format(k, type(v) == "table" and t_concat(v,",") or v);
199
		end
200
	end
201
 
202
	return ("%s%s:%s"):format(item.name, params, value)
203
end
204
 
205
local function vcard_to_text(vcard)
206
	local t={};
207
	t_insert(t, "BEGIN:VCARD")
208
	for i=1,#vcard do
209
		t_insert(t, item_to_text(vcard[i]));
210
	end
211
	t_insert(t, "END:VCARD")
212
	return t_concat(t, line_sep);
213
end
214
 
215
function to_text(vCards)
216
	if vCards[1] and vCards[1].name then
217
		return vcard_to_text(vCards)
218
	else
219
		local t = {};
220
		for i=1,#vCards do
221
			t[i]=vcard_to_text(vCards[i]);
222
		end
223
		return t_concat(t, line_sep);
224
	end
225
end
226
 
227
local function from_xep54_item(item)
228
	local prop_name = item.name;
229
	local prop_def = vCard_dtd[prop_name];
230
 
231
	local prop = { name = prop_name };
232
 
233
	if prop_def == "text" then
234
		prop[1] = item:get_text();
235
	elseif type(prop_def) == "table" then
236
		if prop_def.value then --single item
237
			prop[1] = item:get_child_text(prop_def.value) or "";
238
		elseif prop_def.values then --array
239
			local value_names = prop_def.values;
240
			if value_names.behaviour == "repeat-last" then
241
				for i=1,#item.tags do
242
					t_insert(prop, item.tags[i]:get_text() or "");
243
				end
244
			else
245
				for i=1,#value_names do
246
					t_insert(prop, item:get_child_text(value_names[i]) or "");
247
				end
248
			end
249
		elseif prop_def.names then
250
			local names = prop_def.names;
251
			for i=1,#names do
252
				if item:get_child(names[i]) then
253
					prop[1] = names[i];
254
					break;
255
				end
256
			end
257
		end
258
		
259
		if prop_def.props_verbatim then
260
			for k,v in pairs(prop_def.props_verbatim) do
261
				prop[k] = v;
262
			end
263
		end
264
 
265
		if prop_def.types then
266
			local types = prop_def.types;
267
			prop.TYPE = {};
268
			for i=1,#types do
269
				if item:get_child(types[i]) then
270
					t_insert(prop.TYPE, types[i]:lower());
271
				end
272
			end
273
			if #prop.TYPE == 0 then
274
				prop.TYPE = nil;
275
			end
276
		end
277
 
278
		-- A key-value pair, within a key-value pair?
279
		if prop_def.props then
280
			local params = prop_def.props;
281
			for i=1,#params do
282
				local name = params[i]
283
				local data = item:get_child_text(name);
284
				if data then
285
					prop[name] = prop[name] or {};
286
					t_insert(prop[name], data);
287
				end
288
			end
289
		end
290
	else
291
		return nil
292
	end
293
 
294
	return prop;
295
end
296
 
297
local function from_xep54_vCard(vCard)
298
	local tags = vCard.tags;
299
	local t = {};
300
	for i=1,#tags do
301
		t_insert(t, from_xep54_item(tags[i]));
302
	end
303
	return t
304
end
305
 
306
function from_xep54(vCard)
307
	if vCard.attr.xmlns ~= "vcard-temp" then
308
		return nil, "wrong-xmlns";
309
	end
310
	if vCard.name == "xCard" then -- A collection of vCards
311
		local t = {};
312
		local vCards = vCard.tags;
313
		for i=1,#vCards do
314
			t[i] = from_xep54_vCard(vCards[i]);
315
		end
316
		return t
317
	elseif vCard.name == "vCard" then -- A single vCard
318
		return from_xep54_vCard(vCard)
319
	end
320
end
321
 
322
-- This was adapted from http://xmpp.org/extensions/xep-0054.html#dtd
323
vCard_dtd = {
324
	VERSION = "text", --MUST be 3.0, so parsing is redundant
325
	FN = "text",
326
	N = {
327
		values = {
328
			"FAMILY",
329
			"GIVEN",
330
			"MIDDLE",
331
			"PREFIX",
332
			"SUFFIX",
333
		},
334
	},
335
	NICKNAME = "text",
336
	PHOTO = {
337
		props_verbatim = { ENCODING = { "b" } },
338
		props = { "TYPE" },
339
		value = "BINVAL", --{ "EXTVAL", },
340
	},
341
	BDAY = "text",
342
	ADR = {
343
		types = {
344
			"HOME",
345
			"WORK", 
346
			"POSTAL", 
347
			"PARCEL", 
348
			"DOM",
349
			"INTL",
350
			"PREF", 
351
		},
352
		values = {
353
			"POBOX",
354
			"EXTADD",
355
			"STREET",
356
			"LOCALITY",
357
			"REGION",
358
			"PCODE",
359
			"CTRY",
360
		}
361
	},
362
	LABEL = {
363
		types = {
364
			"HOME", 
365
			"WORK", 
366
			"POSTAL", 
367
			"PARCEL", 
368
			"DOM",
369
			"INTL", 
370
			"PREF", 
371
		},
372
		value = "LINE",
373
	},
374
	TEL = {
375
		types = {
376
			"HOME", 
377
			"WORK", 
378
			"VOICE", 
379
			"FAX", 
380
			"PAGER", 
381
			"MSG", 
382
			"CELL", 
383
			"VIDEO", 
384
			"BBS", 
385
			"MODEM", 
386
			"ISDN", 
387
			"PCS", 
388
			"PREF", 
389
		},
390
		value = "NUMBER",
391
	},
392
	EMAIL = {
393
		types = {
394
			"HOME", 
395
			"WORK", 
396
			"INTERNET", 
397
			"PREF", 
398
			"X400", 
399
		},
400
		value = "USERID",
401
	},
402
	JABBERID = "text",
403
	MAILER = "text",
404
	TZ = "text",
405
	GEO = {
406
		values = {
407
			"LAT",
408
			"LON",
409
		},
410
	},
411
	TITLE = "text",
412
	ROLE = "text",
413
	LOGO = "copy of PHOTO",
414
	AGENT = "text",
415
	ORG = {
416
		values = {
417
			behaviour = "repeat-last",
418
			"ORGNAME",
419
			"ORGUNIT",
420
		}
421
	},
422
	CATEGORIES = {
423
		values = "KEYWORD",
424
	},
425
	NOTE = "text",
426
	PRODID = "text",
427
	REV = "text",
428
	SORTSTRING = "text",
429
	SOUND = "copy of PHOTO",
430
	UID = "text",
431
	URL = "text",
432
	CLASS = {
433
		names = { -- The item.name is the value if it's one of these.
434
			"PUBLIC",
435
			"PRIVATE",
436
			"CONFIDENTIAL",
437
		},
438
	},
439
	KEY = {
440
		props = { "TYPE" },
441
		value = "CRED",
442
	},
443
	DESC = "text",
444
};
445
vCard_dtd.LOGO = vCard_dtd.PHOTO;
446
vCard_dtd.SOUND = vCard_dtd.PHOTO;
447
 
448
return {
449
	from_text = from_text;
450
	to_text = to_text;
451
 
452
	from_xep54 = from_xep54;
453
	to_xep54 = to_xep54;
454
 
455
	-- COMPAT:
456
	lua_to_text = to_text;
457
	lua_to_xep54 = to_xep54;
458
 
459
	text_to_lua = from_text;
460
	text_to_xep54 = function (...) return to_xep54(from_text(...)); end;
461
 
462
	xep54_to_lua = from_xep54;
463
	xep54_to_text = function (...) return to_text(from_xep54(...)) end;
464
};