util.vcard: Add support for converting to vcard4
[prosody.git] / util / vcard.lua
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 --
6
7 -- TODO
8 -- Fix folding.
9
10 local st = require "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 local vcard4 = { }
323
324 function vcard4:text(node, params, value)
325         self:tag(node:lower())
326         -- FIXME params
327         if type(value) == "string" then
328                 self:tag("text"):text(value):up()
329         elseif vcard4[node] then
330                 vcard4[node](value);
331         end
332         self:up();
333 end
334
335 function vcard4.N(value)
336         for i, k in ipairs(vCard_dtd.N.values) do
337                 value:tag(k):text(value[i]):up();
338         end
339 end
340
341 local xmlns_vcard4 = "urn:ietf:params:xml:ns:vcard-4.0"
342
343 local function item_to_vcard4(item)
344         local typ = item.name:lower();
345         local t = st.stanza(typ, { xmlns = xmlns_vcard4 });
346
347         local prop_def = vCard4_dtd[typ];
348         if prop_def == "text" then
349                 t:tag("text"):text(item[1]):up();
350         elseif type(prop_def) == "table" then
351                 if prop_def.values then
352                         for i, v in ipairs(prop_def.values) do
353                                 t:tag(v:lower()):text(item[i] or ""):up();
354                         end
355                 else
356                         t:tag("unsupported",{xmlns="http://zash.se/protocol/vcardlib"})
357                 end
358         else
359                 t:tag("unsupported",{xmlns="http://zash.se/protocol/vcardlib"})
360         end
361         return t;
362 end
363
364 local function vcard_to_vcard4xml(vCard)
365         local t = st.stanza("vcard", { xmlns = xmlns_vcard4 });
366         for i=1,#vCard do
367                 t:add_child(item_to_vcard4(vCard[i]));
368         end
369         return t;
370 end
371
372 local function vcards_to_vcard4xml(vCards)
373         if not vCards[1] or vCards[1].name then
374                 return vcard_to_vcard4xml(vCards)
375         else
376                 local t = st.stanza("vcards", { xmlns = xmlns_vcard4 });
377                 for i=1,#vCards do
378                         t:add_child(vcard_to_vcard4xml(vCards[i]));
379                 end
380                 return t;
381         end
382 end
383
384 -- This was adapted from http://xmpp.org/extensions/xep-0054.html#dtd
385 vCard_dtd = {
386         VERSION = "text", --MUST be 3.0, so parsing is redundant
387         FN = "text",
388         N = {
389                 values = {
390                         "FAMILY",
391                         "GIVEN",
392                         "MIDDLE",
393                         "PREFIX",
394                         "SUFFIX",
395                 },
396         },
397         NICKNAME = "text",
398         PHOTO = {
399                 props_verbatim = { ENCODING = { "b" } },
400                 props = { "TYPE" },
401                 value = "BINVAL", --{ "EXTVAL", },
402         },
403         BDAY = "text",
404         ADR = {
405                 types = {
406                         "HOME",
407                         "WORK",
408                         "POSTAL",
409                         "PARCEL",
410                         "DOM",
411                         "INTL",
412                         "PREF",
413                 },
414                 values = {
415                         "POBOX",
416                         "EXTADD",
417                         "STREET",
418                         "LOCALITY",
419                         "REGION",
420                         "PCODE",
421                         "CTRY",
422                 }
423         },
424         LABEL = {
425                 types = {
426                         "HOME",
427                         "WORK",
428                         "POSTAL",
429                         "PARCEL",
430                         "DOM",
431                         "INTL",
432                         "PREF",
433                 },
434                 value = "LINE",
435         },
436         TEL = {
437                 types = {
438                         "HOME",
439                         "WORK",
440                         "VOICE",
441                         "FAX",
442                         "PAGER",
443                         "MSG",
444                         "CELL",
445                         "VIDEO",
446                         "BBS",
447                         "MODEM",
448                         "ISDN",
449                         "PCS",
450                         "PREF",
451                 },
452                 value = "NUMBER",
453         },
454         EMAIL = {
455                 types = {
456                         "HOME",
457                         "WORK",
458                         "INTERNET",
459                         "PREF",
460                         "X400",
461                 },
462                 value = "USERID",
463         },
464         JABBERID = "text",
465         MAILER = "text",
466         TZ = "text",
467         GEO = {
468                 values = {
469                         "LAT",
470                         "LON",
471                 },
472         },
473         TITLE = "text",
474         ROLE = "text",
475         LOGO = "copy of PHOTO",
476         AGENT = "text",
477         ORG = {
478                 values = {
479                         behaviour = "repeat-last",
480                         "ORGNAME",
481                         "ORGUNIT",
482                 }
483         },
484         CATEGORIES = {
485                 values = "KEYWORD",
486         },
487         NOTE = "text",
488         PRODID = "text",
489         REV = "text",
490         SORTSTRING = "text",
491         SOUND = "copy of PHOTO",
492         UID = "text",
493         URL = "text",
494         CLASS = {
495                 names = { -- The item.name is the value if it's one of these.
496                         "PUBLIC",
497                         "PRIVATE",
498                         "CONFIDENTIAL",
499                 },
500         },
501         KEY = {
502                 props = { "TYPE" },
503                 value = "CRED",
504         },
505         DESC = "text",
506 };
507 vCard_dtd.LOGO = vCard_dtd.PHOTO;
508 vCard_dtd.SOUND = vCard_dtd.PHOTO;
509
510 vCard4_dtd = {
511         source = "uri",
512         kind = "text",
513         xml = "text",
514         fn = "text",
515         n = {
516                 values = {
517                         "family",
518                         "given",
519                         "middle",
520                         "prefix",
521                         "suffix",
522                 },
523         },
524         nickname = "text",
525         photo = "uri",
526         bday = "date-and-or-time",
527         anniversary = "date-and-or-time",
528         gender = "text",
529         adr = {
530                 values = {
531                         "pobox",
532                         "ext",
533                         "street",
534                         "locality",
535                         "region",
536                         "code",
537                         "country",
538                 }
539         },
540         tel = "text",
541         email = "text",
542         impp = "uri",
543         lang = "language-tag",
544         tz = "text",
545         geo = "uri",
546         title = "text",
547         role = "text",
548         logo = "uri",
549         org = "text",
550         member = "uri",
551         related = "uri",
552         categories = "text",
553         note = "text",
554         prodid = "text",
555         rev = "timestamp",
556         sound = "uri",
557         uid = "uri",
558         clientpidmap = "number, uuid",
559         url = "uri",
560         version = "text",
561         key = "uri",
562         fburl = "uri",
563         caladruri = "uri",
564         caluri = "uri",
565 };
566
567 return {
568         from_text = from_text;
569         to_text = to_text;
570
571         from_xep54 = from_xep54;
572         to_xep54 = to_xep54;
573
574         to_vcard4 = vcards_to_vcard4xml;
575 };