e300580137295252d9991a029754d6d7bd519fda
[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 -- 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 };