Prepare required data folder for integration tests
[prosody.git] / util / vcard.lua
blobbb299fab2dd6f0bf1251ca0fe8199a0aa5412a64
1 -- Copyright (C) 2011-2014 Kim Alvefur
2 --
3 -- This project is MIT/X11 licensed. Please see the
4 -- COPYING file in the source package for more information.
5 --
7 -- TODO
8 -- Fix folding.
10 local st = require "util.stanza";
11 local t_insert, t_concat = table.insert, table.concat;
12 local type = type;
13 local pairs, ipairs = pairs, ipairs;
15 local from_text, to_text, from_xep54, to_xep54;
17 local line_sep = "\n";
19 local vCard_dtd; -- See end of file
20 local vCard4_dtd;
22 local function vCard_esc(s)
23 return s:gsub("[,:;\\]", "\\%1"):gsub("\n","\\n");
24 end
26 local function vCard_unesc(s)
27 return s:gsub("\\?[\\nt:;,]", {
28 ["\\\\"] = "\\",
29 ["\\n"] = "\n",
30 ["\\r"] = "\r",
31 ["\\t"] = "\t",
32 ["\\:"] = ":", -- FIXME Shouldn't need to espace : in values, just params
33 ["\\;"] = ";",
34 ["\\,"] = ",",
35 [":"] = "\29",
36 [";"] = "\30",
37 [","] = "\31",
38 });
39 end
41 local function item_to_xep54(item)
42 local t = st.stanza(item.name, { xmlns = "vcard-temp" });
44 local prop_def = vCard_dtd[item.name];
45 if prop_def == "text" then
46 t:text(item[1]);
47 elseif type(prop_def) == "table" then
48 if prop_def.types and item.TYPE then
49 if type(item.TYPE) == "table" then
50 for _,v in pairs(prop_def.types) do
51 for _,typ in pairs(item.TYPE) do
52 if typ:upper() == v then
53 t:tag(v):up();
54 break;
55 end
56 end
57 end
58 else
59 t:tag(item.TYPE:upper()):up();
60 end
61 end
63 if prop_def.props then
64 for _,prop in pairs(prop_def.props) do
65 if item[prop] then
66 for _, v in ipairs(item[prop]) do
67 t:text_tag(prop, v);
68 end
69 end
70 end
71 end
73 if prop_def.value then
74 t:text_tag(prop_def.value, item[1]);
75 elseif prop_def.values then
76 local prop_def_values = prop_def.values;
77 local repeat_last = prop_def_values.behaviour == "repeat-last" and prop_def_values[#prop_def_values];
78 for i=1,#item do
79 t:text_tag(prop_def.values[i] or repeat_last, item[i]);
80 end
81 end
82 end
84 return t;
85 end
87 local function vcard_to_xep54(vCard)
88 local t = st.stanza("vCard", { xmlns = "vcard-temp" });
89 for i=1,#vCard do
90 t:add_child(item_to_xep54(vCard[i]));
91 end
92 return t;
93 end
95 function to_xep54(vCards)
96 if not vCards[1] or vCards[1].name then
97 return vcard_to_xep54(vCards)
98 else
99 local t = st.stanza("xCard", { xmlns = "vcard-temp" });
100 for i=1,#vCards do
101 t:add_child(vcard_to_xep54(vCards[i]));
103 return t;
107 function from_text(data)
108 data = data -- unfold and remove empty lines
109 :gsub("\r\n","\n")
110 :gsub("\n ", "")
111 :gsub("\n\n+","\n");
112 local vCards = {};
113 local current;
114 for line in data:gmatch("[^\n]+") do
115 line = vCard_unesc(line);
116 local name, params, value = line:match("^([-%a]+)(\30?[^\29]*)\29(.*)$");
117 value = value:gsub("\29",":");
118 if #params > 0 then
119 local _params = {};
120 for k,isval,v in params:gmatch("\30([^=]+)(=?)([^\30]*)") do
121 k = k:upper();
122 local _vt = {};
123 for _p in v:gmatch("[^\31]+") do
124 _vt[#_vt+1]=_p
125 _vt[_p]=true;
127 if isval == "=" then
128 _params[k]=_vt;
129 else
130 _params[k]=true;
133 params = _params;
135 if name == "BEGIN" and value == "VCARD" then
136 current = {};
137 vCards[#vCards+1] = current;
138 elseif name == "END" and value == "VCARD" then
139 current = nil;
140 elseif current and vCard_dtd[name] then
141 local dtd = vCard_dtd[name];
142 local item = { name = name };
143 t_insert(current, item);
144 local up = current;
145 current = item;
146 if dtd.types then
147 for _, t in ipairs(dtd.types) do
148 t = t:lower();
149 if ( params.TYPE and params.TYPE[t] == true)
150 or params[t] == true then
151 current.TYPE=t;
155 if dtd.props then
156 for _, p in ipairs(dtd.props) do
157 if params[p] then
158 if params[p] == true then
159 current[p]=true;
160 else
161 for _, prop in ipairs(params[p]) do
162 current[p]=prop;
168 if dtd == "text" or dtd.value then
169 t_insert(current, value);
170 elseif dtd.values then
171 for p in ("\30"..value):gmatch("\30([^\30]*)") do
172 t_insert(current, p);
175 current = up;
178 return vCards;
181 local function item_to_text(item)
182 local value = {};
183 for i=1,#item do
184 value[i] = vCard_esc(item[i]);
186 value = t_concat(value, ";");
188 local params = "";
189 for k,v in pairs(item) do
190 if type(k) == "string" and k ~= "name" then
191 params = params .. (";%s=%s"):format(k, type(v) == "table" and t_concat(v,",") or v);
195 return ("%s%s:%s"):format(item.name, params, value)
198 local function vcard_to_text(vcard)
199 local t={};
200 t_insert(t, "BEGIN:VCARD")
201 for i=1,#vcard do
202 t_insert(t, item_to_text(vcard[i]));
204 t_insert(t, "END:VCARD")
205 return t_concat(t, line_sep);
208 function to_text(vCards)
209 if vCards[1] and vCards[1].name then
210 return vcard_to_text(vCards)
211 else
212 local t = {};
213 for i=1,#vCards do
214 t[i]=vcard_to_text(vCards[i]);
216 return t_concat(t, line_sep);
220 local function from_xep54_item(item)
221 local prop_name = item.name;
222 local prop_def = vCard_dtd[prop_name];
224 local prop = { name = prop_name };
226 if prop_def == "text" then
227 prop[1] = item:get_text();
228 elseif type(prop_def) == "table" then
229 if prop_def.value then --single item
230 prop[1] = item:get_child_text(prop_def.value) or "";
231 elseif prop_def.values then --array
232 local value_names = prop_def.values;
233 if value_names.behaviour == "repeat-last" then
234 for i=1,#item.tags do
235 t_insert(prop, item.tags[i]:get_text() or "");
237 else
238 for i=1,#value_names do
239 t_insert(prop, item:get_child_text(value_names[i]) or "");
242 elseif prop_def.names then
243 local names = prop_def.names;
244 for i=1,#names do
245 if item:get_child(names[i]) then
246 prop[1] = names[i];
247 break;
252 if prop_def.props_verbatim then
253 for k,v in pairs(prop_def.props_verbatim) do
254 prop[k] = v;
258 if prop_def.types then
259 local types = prop_def.types;
260 prop.TYPE = {};
261 for i=1,#types do
262 if item:get_child(types[i]) then
263 t_insert(prop.TYPE, types[i]:lower());
266 if #prop.TYPE == 0 then
267 prop.TYPE = nil;
271 -- A key-value pair, within a key-value pair?
272 if prop_def.props then
273 local params = prop_def.props;
274 for i=1,#params do
275 local name = params[i]
276 local data = item:get_child_text(name);
277 if data then
278 prop[name] = prop[name] or {};
279 t_insert(prop[name], data);
283 else
284 return nil
287 return prop;
290 local function from_xep54_vCard(vCard)
291 local tags = vCard.tags;
292 local t = {};
293 for i=1,#tags do
294 t_insert(t, from_xep54_item(tags[i]));
296 return t
299 function from_xep54(vCard)
300 if vCard.attr.xmlns ~= "vcard-temp" then
301 return nil, "wrong-xmlns";
303 if vCard.name == "xCard" then -- A collection of vCards
304 local t = {};
305 local vCards = vCard.tags;
306 for i=1,#vCards do
307 t[i] = from_xep54_vCard(vCards[i]);
309 return t
310 elseif vCard.name == "vCard" then -- A single vCard
311 return from_xep54_vCard(vCard)
315 local vcard4 = { }
317 function vcard4:text(node, params, value) -- luacheck: ignore 212/params
318 self:tag(node:lower())
319 -- FIXME params
320 if type(value) == "string" then
321 self:text_tag("text", value);
322 elseif vcard4[node] then
323 vcard4[node](value);
325 self:up();
328 function vcard4.N(value)
329 for i, k in ipairs(vCard_dtd.N.values) do
330 value:text_tag(k, value[i]);
334 local xmlns_vcard4 = "urn:ietf:params:xml:ns:vcard-4.0"
336 local function item_to_vcard4(item)
337 local typ = item.name:lower();
338 local t = st.stanza(typ, { xmlns = xmlns_vcard4 });
340 local prop_def = vCard4_dtd[typ];
341 if prop_def == "text" then
342 t:text_tag("text", item[1]);
343 elseif prop_def == "uri" then
344 if item.ENCODING and item.ENCODING[1] == 'b' then
345 t:text_tag("uri", "data:;base64," .. item[1]);
346 else
347 t:text_tag("uri", item[1]);
349 elseif type(prop_def) == "table" then
350 if prop_def.values then
351 for i, v in ipairs(prop_def.values) do
352 t:text_tag(v:lower(), item[i]);
354 else
355 t:tag("unsupported",{xmlns="http://zash.se/protocol/vcardlib"})
357 else
358 t:tag("unsupported",{xmlns="http://zash.se/protocol/vcardlib"})
360 return t;
363 local function vcard_to_vcard4xml(vCard)
364 local t = st.stanza("vcard", { xmlns = xmlns_vcard4 });
365 for i=1,#vCard do
366 t:add_child(item_to_vcard4(vCard[i]));
368 return t;
371 local function vcards_to_vcard4xml(vCards)
372 if not vCards[1] or vCards[1].name then
373 return vcard_to_vcard4xml(vCards)
374 else
375 local t = st.stanza("vcards", { xmlns = xmlns_vcard4 });
376 for i=1,#vCards do
377 t:add_child(vcard_to_vcard4xml(vCards[i]));
379 return t;
383 -- This was adapted from http://xmpp.org/extensions/xep-0054.html#dtd
384 vCard_dtd = {
385 VERSION = "text", --MUST be 3.0, so parsing is redundant
386 FN = "text",
387 N = {
388 values = {
389 "FAMILY",
390 "GIVEN",
391 "MIDDLE",
392 "PREFIX",
393 "SUFFIX",
396 NICKNAME = "text",
397 PHOTO = {
398 props_verbatim = { ENCODING = { "b" } },
399 props = { "TYPE" },
400 value = "BINVAL", --{ "EXTVAL", },
402 BDAY = "text",
403 ADR = {
404 types = {
405 "HOME",
406 "WORK",
407 "POSTAL",
408 "PARCEL",
409 "DOM",
410 "INTL",
411 "PREF",
413 values = {
414 "POBOX",
415 "EXTADD",
416 "STREET",
417 "LOCALITY",
418 "REGION",
419 "PCODE",
420 "CTRY",
423 LABEL = {
424 types = {
425 "HOME",
426 "WORK",
427 "POSTAL",
428 "PARCEL",
429 "DOM",
430 "INTL",
431 "PREF",
433 value = "LINE",
435 TEL = {
436 types = {
437 "HOME",
438 "WORK",
439 "VOICE",
440 "FAX",
441 "PAGER",
442 "MSG",
443 "CELL",
444 "VIDEO",
445 "BBS",
446 "MODEM",
447 "ISDN",
448 "PCS",
449 "PREF",
451 value = "NUMBER",
453 EMAIL = {
454 types = {
455 "HOME",
456 "WORK",
457 "INTERNET",
458 "PREF",
459 "X400",
461 value = "USERID",
463 JABBERID = "text",
464 MAILER = "text",
465 TZ = "text",
466 GEO = {
467 values = {
468 "LAT",
469 "LON",
472 TITLE = "text",
473 ROLE = "text",
474 LOGO = "copy of PHOTO",
475 AGENT = "text",
476 ORG = {
477 values = {
478 behaviour = "repeat-last",
479 "ORGNAME",
480 "ORGUNIT",
483 CATEGORIES = {
484 values = "KEYWORD",
486 NOTE = "text",
487 PRODID = "text",
488 REV = "text",
489 SORTSTRING = "text",
490 SOUND = "copy of PHOTO",
491 UID = "text",
492 URL = "text",
493 CLASS = {
494 names = { -- The item.name is the value if it's one of these.
495 "PUBLIC",
496 "PRIVATE",
497 "CONFIDENTIAL",
500 KEY = {
501 props = { "TYPE" },
502 value = "CRED",
504 DESC = "text",
506 vCard_dtd.LOGO = vCard_dtd.PHOTO;
507 vCard_dtd.SOUND = vCard_dtd.PHOTO;
509 vCard4_dtd = {
510 source = "uri",
511 kind = "text",
512 xml = "text",
513 fn = "text",
514 n = {
515 values = {
516 "family",
517 "given",
518 "middle",
519 "prefix",
520 "suffix",
523 nickname = "text",
524 photo = "uri",
525 bday = "date-and-or-time",
526 anniversary = "date-and-or-time",
527 gender = "text",
528 adr = {
529 values = {
530 "pobox",
531 "ext",
532 "street",
533 "locality",
534 "region",
535 "code",
536 "country",
539 tel = "text",
540 email = "text",
541 impp = "uri",
542 lang = "language-tag",
543 tz = "text",
544 geo = "uri",
545 title = "text",
546 role = "text",
547 logo = "uri",
548 org = "text",
549 member = "uri",
550 related = "uri",
551 categories = "text",
552 note = "text",
553 prodid = "text",
554 rev = "timestamp",
555 sound = "uri",
556 uid = "uri",
557 clientpidmap = "number, uuid",
558 url = "uri",
559 version = "text",
560 key = "uri",
561 fburl = "uri",
562 caladruri = "uri",
563 caluri = "uri",
566 return {
567 from_text = from_text;
568 to_text = to_text;
570 from_xep54 = from_xep54;
571 to_xep54 = to_xep54;
573 to_vcard4 = vcards_to_vcard4xml;