util.vcard: Add support for uri types in 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 local vCard4_dtd;
21
22 local function fold_line()
23         error "Not implemented" --TODO
24 end
25 local function unfold_line()
26         error "Not implemented"
27         -- gsub("\r?\n[ \t]([^\r\n])", "%1");
28 end
29
30 local function vCard_esc(s)
31         return s:gsub("[,:;\\]", "\\%1"):gsub("\n","\\n");
32 end
33
34 local function vCard_unesc(s)
35         return s:gsub("\\?[\\nt:;,]", {
36                 ["\\\\"] = "\\",
37                 ["\\n"] = "\n",
38                 ["\\r"] = "\r",
39                 ["\\t"] = "\t",
40                 ["\\:"] = ":", -- FIXME Shouldn't need to espace : in values, just params
41                 ["\\;"] = ";",
42                 ["\\,"] = ",",
43                 [":"] = "\29",
44                 [";"] = "\30",
45                 [","] = "\31",
46         });
47 end
48
49 local function item_to_xep54(item)
50         local t = st.stanza(item.name, { xmlns = "vcard-temp" });
51
52         local prop_def = vCard_dtd[item.name];
53         if prop_def == "text" then
54                 t:text(item[1]);
55         elseif type(prop_def) == "table" then
56                 if prop_def.types and item.TYPE then
57                         if type(item.TYPE) == "table" then
58                                 for _,v in pairs(prop_def.types) do
59                                         for _,typ in pairs(item.TYPE) do
60                                                 if typ:upper() == v then
61                                                         t:tag(v):up();
62                                                         break;
63                                                 end
64                                         end
65                                 end
66                         else
67                                 t:tag(item.TYPE:upper()):up();
68                         end
69                 end
70
71                 if prop_def.props then
72                         for _,v in pairs(prop_def.props) do
73                                 if item[v] then
74                                         t:tag(v):up();
75                                 end
76                         end
77                 end
78
79                 if prop_def.value then
80                         t:tag(prop_def.value):text(item[1]):up();
81                 elseif prop_def.values then
82                         local prop_def_values = prop_def.values;
83                         local repeat_last = prop_def_values.behaviour == "repeat-last" and prop_def_values[#prop_def_values];
84                         for i=1,#item do
85                                 t:tag(prop_def.values[i] or repeat_last):text(item[i]):up();
86                         end
87                 end
88         end
89
90         return t;
91 end
92
93 local function vcard_to_xep54(vCard)
94         local t = st.stanza("vCard", { xmlns = "vcard-temp" });
95         for i=1,#vCard do
96                 t:add_child(item_to_xep54(vCard[i]));
97         end
98         return t;
99 end
100
101 function to_xep54(vCards)
102         if not vCards[1] or vCards[1].name then
103                 return vcard_to_xep54(vCards)
104         else
105                 local t = st.stanza("xCard", { xmlns = "vcard-temp" });
106                 for i=1,#vCards do
107                         t:add_child(vcard_to_xep54(vCards[i]));
108                 end
109                 return t;
110         end
111 end
112
113 function from_text(data)
114         data = data -- unfold and remove empty lines
115                 :gsub("\r\n","\n")
116                 :gsub("\n ", "")
117                 :gsub("\n\n+","\n");
118         local vCards = {};
119         local c; -- current item
120         for line in data:gmatch("[^\n]+") do
121                 local line = vCard_unesc(line);
122                 local name, params, value = line:match("^([-%a]+)(\30?[^\29]*)\29(.*)$");
123                 value = value:gsub("\29",":");
124                 if #params > 0 then
125                         local _params = {};
126                         for k,isval,v in params:gmatch("\30([^=]+)(=?)([^\30]*)") do
127                                 k = k:upper();
128                                 local _vt = {};
129                                 for _p in v:gmatch("[^\31]+") do
130                                         _vt[#_vt+1]=_p
131                                         _vt[_p]=true;
132                                 end
133                                 if isval == "=" then
134                                         _params[k]=_vt;
135                                 else
136                                         _params[k]=true;
137                                 end
138                         end
139                         params = _params;
140                 end
141                 if name == "BEGIN" and value == "VCARD" then
142                         c = {};
143                         vCards[#vCards+1] = c;
144                 elseif name == "END" and value == "VCARD" then
145                         c = nil;
146                 elseif c and vCard_dtd[name] then
147                         local dtd = vCard_dtd[name];
148                         local p = { name = name };
149                         c[#c+1]=p;
150                         --c[name]=p;
151                         local up = c;
152                         c = p;
153                         if dtd.types then
154                                 for _, t in ipairs(dtd.types) do
155                                         local t = t:lower();
156                                         if ( params.TYPE and params.TYPE[t] == true)
157                                                         or params[t] == true then
158                                                 c.TYPE=t;
159                                         end
160                                 end
161                         end
162                         if dtd.props then
163                                 for _, p in ipairs(dtd.props) do
164                                         if params[p] then
165                                                 if params[p] == true then
166                                                         c[p]=true;
167                                                 else
168                                                         for _, prop in ipairs(params[p]) do
169                                                                 c[p]=prop;
170                                                         end
171                                                 end
172                                         end
173                                 end
174                         end
175                         if dtd == "text" or dtd.value then
176                                 t_insert(c, value);
177                         elseif dtd.values then
178                                 local value = "\30"..value;
179                                 for p in value:gmatch("\30([^\30]*)") do
180                                         t_insert(c, p);
181                                 end
182                         end
183                         c = up;
184                 end
185         end
186         return vCards;
187 end
188
189 local function item_to_text(item)
190         local value = {};
191         for i=1,#item do
192                 value[i] = vCard_esc(item[i]);
193         end
194         value = t_concat(value, ";");
195
196         local params = "";
197         for k,v in pairs(item) do
198                 if type(k) == "string" and k ~= "name" then
199                         params = params .. (";%s=%s"):format(k, type(v) == "table" and t_concat(v,",") or v);
200                 end
201         end
202
203         return ("%s%s:%s"):format(item.name, params, value)
204 end
205
206 local function vcard_to_text(vcard)
207         local t={};
208         t_insert(t, "BEGIN:VCARD")
209         for i=1,#vcard do
210                 t_insert(t, item_to_text(vcard[i]));
211         end
212         t_insert(t, "END:VCARD")
213         return t_concat(t, line_sep);
214 end
215
216 function to_text(vCards)
217         if vCards[1] and vCards[1].name then
218                 return vcard_to_text(vCards)
219         else
220                 local t = {};
221                 for i=1,#vCards do
222                         t[i]=vcard_to_text(vCards[i]);
223                 end
224                 return t_concat(t, line_sep);
225         end
226 end
227
228 local function from_xep54_item(item)
229         local prop_name = item.name;
230         local prop_def = vCard_dtd[prop_name];
231
232         local prop = { name = prop_name };
233
234         if prop_def == "text" then
235                 prop[1] = item:get_text();
236         elseif type(prop_def) == "table" then
237                 if prop_def.value then --single item
238                         prop[1] = item:get_child_text(prop_def.value) or "";
239                 elseif prop_def.values then --array
240                         local value_names = prop_def.values;
241                         if value_names.behaviour == "repeat-last" then
242                                 for i=1,#item.tags do
243                                         t_insert(prop, item.tags[i]:get_text() or "");
244                                 end
245                         else
246                                 for i=1,#value_names do
247                                         t_insert(prop, item:get_child_text(value_names[i]) or "");
248                                 end
249                         end
250                 elseif prop_def.names then
251                         local names = prop_def.names;
252                         for i=1,#names do
253                                 if item:get_child(names[i]) then
254                                         prop[1] = names[i];
255                                         break;
256                                 end
257                         end
258                 end
259
260                 if prop_def.props_verbatim then
261                         for k,v in pairs(prop_def.props_verbatim) do
262                                 prop[k] = v;
263                         end
264                 end
265
266                 if prop_def.types then
267                         local types = prop_def.types;
268                         prop.TYPE = {};
269                         for i=1,#types do
270                                 if item:get_child(types[i]) then
271                                         t_insert(prop.TYPE, types[i]:lower());
272                                 end
273                         end
274                         if #prop.TYPE == 0 then
275                                 prop.TYPE = nil;
276                         end
277                 end
278
279                 -- A key-value pair, within a key-value pair?
280                 if prop_def.props then
281                         local params = prop_def.props;
282                         for i=1,#params do
283                                 local name = params[i]
284                                 local data = item:get_child_text(name);
285                                 if data then
286                                         prop[name] = prop[name] or {};
287                                         t_insert(prop[name], data);
288                                 end
289                         end
290                 end
291         else
292                 return nil
293         end
294
295         return prop;
296 end
297
298 local function from_xep54_vCard(vCard)
299         local tags = vCard.tags;
300         local t = {};
301         for i=1,#tags do
302                 t_insert(t, from_xep54_item(tags[i]));
303         end
304         return t
305 end
306
307 function from_xep54(vCard)
308         if vCard.attr.xmlns ~= "vcard-temp" then
309                 return nil, "wrong-xmlns";
310         end
311         if vCard.name == "xCard" then -- A collection of vCards
312                 local t = {};
313                 local vCards = vCard.tags;
314                 for i=1,#vCards do
315                         t[i] = from_xep54_vCard(vCards[i]);
316                 end
317                 return t
318         elseif vCard.name == "vCard" then -- A single vCard
319                 return from_xep54_vCard(vCard)
320         end
321 end
322
323 local vcard4 = { }
324
325 function vcard4:text(node, params, value)
326         self:tag(node:lower())
327         -- FIXME params
328         if type(value) == "string" then
329                 self:tag("text"):text(value):up()
330         elseif vcard4[node] then
331                 vcard4[node](value);
332         end
333         self:up();
334 end
335
336 function vcard4.N(value)
337         for i, k in ipairs(vCard_dtd.N.values) do
338                 value:tag(k):text(value[i]):up();
339         end
340 end
341
342 local xmlns_vcard4 = "urn:ietf:params:xml:ns:vcard-4.0"
343
344 local function item_to_vcard4(item)
345         local typ = item.name:lower();
346         local t = st.stanza(typ, { xmlns = xmlns_vcard4 });
347
348         local prop_def = vCard4_dtd[typ];
349         if prop_def == "text" then
350                 t:tag("text"):text(item[1]):up();
351         elseif prop_def == "uri" then
352                 t:tag("uri"):text(item[1]):up();
353         elseif type(prop_def) == "table" then
354                 if prop_def.values then
355                         for i, v in ipairs(prop_def.values) do
356                                 t:tag(v:lower()):text(item[i] or ""):up();
357                         end
358                 else
359                         t:tag("unsupported",{xmlns="http://zash.se/protocol/vcardlib"})
360                 end
361         else
362                 t:tag("unsupported",{xmlns="http://zash.se/protocol/vcardlib"})
363         end
364         return t;
365 end
366
367 local function vcard_to_vcard4xml(vCard)
368         local t = st.stanza("vcard", { xmlns = xmlns_vcard4 });
369         for i=1,#vCard do
370                 t:add_child(item_to_vcard4(vCard[i]));
371         end
372         return t;
373 end
374
375 local function vcards_to_vcard4xml(vCards)
376         if not vCards[1] or vCards[1].name then
377                 return vcard_to_vcard4xml(vCards)
378         else
379                 local t = st.stanza("vcards", { xmlns = xmlns_vcard4 });
380                 for i=1,#vCards do
381                         t:add_child(vcard_to_vcard4xml(vCards[i]));
382                 end
383                 return t;
384         end
385 end
386
387 -- This was adapted from http://xmpp.org/extensions/xep-0054.html#dtd
388 vCard_dtd = {
389         VERSION = "text", --MUST be 3.0, so parsing is redundant
390         FN = "text",
391         N = {
392                 values = {
393                         "FAMILY",
394                         "GIVEN",
395                         "MIDDLE",
396                         "PREFIX",
397                         "SUFFIX",
398                 },
399         },
400         NICKNAME = "text",
401         PHOTO = {
402                 props_verbatim = { ENCODING = { "b" } },
403                 props = { "TYPE" },
404                 value = "BINVAL", --{ "EXTVAL", },
405         },
406         BDAY = "text",
407         ADR = {
408                 types = {
409                         "HOME",
410                         "WORK",
411                         "POSTAL",
412                         "PARCEL",
413                         "DOM",
414                         "INTL",
415                         "PREF",
416                 },
417                 values = {
418                         "POBOX",
419                         "EXTADD",
420                         "STREET",
421                         "LOCALITY",
422                         "REGION",
423                         "PCODE",
424                         "CTRY",
425                 }
426         },
427         LABEL = {
428                 types = {
429                         "HOME",
430                         "WORK",
431                         "POSTAL",
432                         "PARCEL",
433                         "DOM",
434                         "INTL",
435                         "PREF",
436                 },
437                 value = "LINE",
438         },
439         TEL = {
440                 types = {
441                         "HOME",
442                         "WORK",
443                         "VOICE",
444                         "FAX",
445                         "PAGER",
446                         "MSG",
447                         "CELL",
448                         "VIDEO",
449                         "BBS",
450                         "MODEM",
451                         "ISDN",
452                         "PCS",
453                         "PREF",
454                 },
455                 value = "NUMBER",
456         },
457         EMAIL = {
458                 types = {
459                         "HOME",
460                         "WORK",
461                         "INTERNET",
462                         "PREF",
463                         "X400",
464                 },
465                 value = "USERID",
466         },
467         JABBERID = "text",
468         MAILER = "text",
469         TZ = "text",
470         GEO = {
471                 values = {
472                         "LAT",
473                         "LON",
474                 },
475         },
476         TITLE = "text",
477         ROLE = "text",
478         LOGO = "copy of PHOTO",
479         AGENT = "text",
480         ORG = {
481                 values = {
482                         behaviour = "repeat-last",
483                         "ORGNAME",
484                         "ORGUNIT",
485                 }
486         },
487         CATEGORIES = {
488                 values = "KEYWORD",
489         },
490         NOTE = "text",
491         PRODID = "text",
492         REV = "text",
493         SORTSTRING = "text",
494         SOUND = "copy of PHOTO",
495         UID = "text",
496         URL = "text",
497         CLASS = {
498                 names = { -- The item.name is the value if it's one of these.
499                         "PUBLIC",
500                         "PRIVATE",
501                         "CONFIDENTIAL",
502                 },
503         },
504         KEY = {
505                 props = { "TYPE" },
506                 value = "CRED",
507         },
508         DESC = "text",
509 };
510 vCard_dtd.LOGO = vCard_dtd.PHOTO;
511 vCard_dtd.SOUND = vCard_dtd.PHOTO;
512
513 vCard4_dtd = {
514         source = "uri",
515         kind = "text",
516         xml = "text",
517         fn = "text",
518         n = {
519                 values = {
520                         "family",
521                         "given",
522                         "middle",
523                         "prefix",
524                         "suffix",
525                 },
526         },
527         nickname = "text",
528         photo = "uri",
529         bday = "date-and-or-time",
530         anniversary = "date-and-or-time",
531         gender = "text",
532         adr = {
533                 values = {
534                         "pobox",
535                         "ext",
536                         "street",
537                         "locality",
538                         "region",
539                         "code",
540                         "country",
541                 }
542         },
543         tel = "text",
544         email = "text",
545         impp = "uri",
546         lang = "language-tag",
547         tz = "text",
548         geo = "uri",
549         title = "text",
550         role = "text",
551         logo = "uri",
552         org = "text",
553         member = "uri",
554         related = "uri",
555         categories = "text",
556         note = "text",
557         prodid = "text",
558         rev = "timestamp",
559         sound = "uri",
560         uid = "uri",
561         clientpidmap = "number, uuid",
562         url = "uri",
563         version = "text",
564         key = "uri",
565         fburl = "uri",
566         caladruri = "uri",
567         caluri = "uri",
568 };
569
570 return {
571         from_text = from_text;
572         to_text = to_text;
573
574         from_xep54 = from_xep54;
575         to_xep54 = to_xep54;
576
577         to_vcard4 = vcards_to_vcard4xml;
578 };