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