-- Copyright (C) 2011-2014 Kim Alvefur
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
-- TODO
-- Fix folding.
local st = require " util.stanza " ;
local t_insert , t_concat = table.insert , table.concat ;
local type = type ;
local pairs , ipairs = pairs , ipairs ;
local from_text , to_text , from_xep54 , to_xep54 ;
local line_sep = " \n " ;
local vCard_dtd ; -- See end of file
local vCard4_dtd ;
local function vCard_esc ( s )
return s : gsub ( " [,:; \\ ] " , " \\ %1 " ) : gsub ( " \n " , " \\ n " ) ;
end
local function vCard_unesc ( s )
return s : gsub ( " \\ ?[ \\ nt:;,] " , {
[ " \\ \\ " ] = " \\ " ,
[ " \\ n " ] = " \n " ,
[ " \\ r " ] = " \r " ,
[ " \\ t " ] = " \t " ,
[ " \\ : " ] = " : " , -- FIXME Shouldn't need to espace : in values, just params
[ " \\ ; " ] = " ; " ,
[ " \\ , " ] = " , " ,
[ " : " ] = " \29 " ,
[ " ; " ] = " \30 " ,
[ " , " ] = " \31 " ,
} ) ;
end
local function item_to_xep54 ( item )
local t = st.stanza ( item.name , { xmlns = " vcard-temp " } ) ;
local prop_def = vCard_dtd [ item.name ] ;
if prop_def == " text " then
t : text ( item [ 1 ] ) ;
elseif type ( prop_def ) == " table " then
if prop_def.types and item.TYPE then
if type ( item.TYPE ) == " table " then
for _ , v in pairs ( prop_def.types ) do
for _ , typ in pairs ( item.TYPE ) do
if typ : upper ( ) == v then
t : tag ( v ) : up ( ) ;
break ;
end
end
end
else
t : tag ( item.TYPE : upper ( ) ) : up ( ) ;
end
end
if prop_def.props then
for _ , prop in pairs ( prop_def.props ) do
if item [ prop ] then
for _ , v in ipairs ( item [ prop ] ) do
t : text_tag ( prop , v ) ;
end
end
end
end
if prop_def.value then
t : text_tag ( prop_def.value , item [ 1 ] ) ;
elseif prop_def.values then
local prop_def_values = prop_def.values ;
local repeat_last = prop_def_values.behaviour == " repeat-last " and prop_def_values [ # prop_def_values ] ;
for i = 1 , # item do
t : text_tag ( prop_def.values [ i ] or repeat_last , item [ i ] ) ;
end
end
end
return t ;
end
local function vcard_to_xep54 ( vCard )
local t = st.stanza ( " vCard " , { xmlns = " vcard-temp " } ) ;
for i = 1 , # vCard do
t : add_child ( item_to_xep54 ( vCard [ i ] ) ) ;
end
return t ;
end
function to_xep54 ( vCards )
if not vCards [ 1 ] or vCards [ 1 ] . name then
return vcard_to_xep54 ( vCards )
else
local t = st.stanza ( " xCard " , { xmlns = " vcard-temp " } ) ;
for i = 1 , # vCards do
t : add_child ( vcard_to_xep54 ( vCards [ i ] ) ) ;
end
return t ;
end
end
function from_text ( data )
data = data -- unfold and remove empty lines
: gsub ( " \r \n " , " \n " )
: gsub ( " \n " , " " )
: gsub ( " \n \n + " , " \n " ) ;
local vCards = { } ;
local current ;
for line in data : gmatch ( " [^ \n ]+ " ) do
line = vCard_unesc ( line ) ;
local name , params , value = line : match ( " ^([-%a]+)( \30 ?[^ \29 ]*) \29 (.*)$ " ) ;
value = value : gsub ( " \29 " , " : " ) ;
if # params > 0 then
local _params = { } ;
for k , isval , v in params : gmatch ( " \30 ([^=]+)(=?)([^ \30 ]*) " ) do
k = k : upper ( ) ;
local _vt = { } ;
for _p in v : gmatch ( " [^ \31 ]+ " ) do
_vt [ # _vt + 1 ] = _p
_vt [ _p ] = true ;
end
if isval == " = " then
_params [ k ] = _vt ;
else
_params [ k ] = true ;
end
end
params = _params ;
end
if name == " BEGIN " and value == " VCARD " then
current = { } ;
vCards [ # vCards + 1 ] = current ;
elseif name == " END " and value == " VCARD " then
current = nil ;
elseif current and vCard_dtd [ name ] then
local dtd = vCard_dtd [ name ] ;
local item = { name = name } ;
t_insert ( current , item ) ;
local up = current ;
current = item ;
if dtd.types then
for _ , t in ipairs ( dtd.types ) do
t = t : lower ( ) ;
if ( params.TYPE and params.TYPE [ t ] == true )
or params [ t ] == true then
current.TYPE = t ;
end
end
end
if dtd.props then
for _ , p in ipairs ( dtd.props ) do
if params [ p ] then
if params [ p ] == true then
current [ p ] = true ;
else
for _ , prop in ipairs ( params [ p ] ) do
current [ p ] = prop ;
end
end
end
end
end
if dtd == " text " or dtd.value then
t_insert ( current , value ) ;
elseif dtd.values then
for p in ( " \30 " .. value ) : gmatch ( " \30 ([^ \30 ]*) " ) do
t_insert ( current , p ) ;
end
end
current = up ;
end
end
return vCards ;
end
local function item_to_text ( item )
local value = { } ;
for i = 1 , # item do
value [ i ] = vCard_esc ( item [ i ] ) ;
end
value = t_concat ( value , " ; " ) ;
local params = " " ;
for k , v in pairs ( item ) do
if type ( k ) == " string " and k ~= " name " then
params = params .. ( " ;%s=%s " ) : format ( k , type ( v ) == " table " and t_concat ( v , " , " ) or v ) ;
end
end
return ( " %s%s:%s " ) : format ( item.name , params , value )
end
local function vcard_to_text ( vcard )
local t = { } ;
t_insert ( t , " BEGIN:VCARD " )
for i = 1 , # vcard do
t_insert ( t , item_to_text ( vcard [ i ] ) ) ;
end
t_insert ( t , " END:VCARD " )
return t_concat ( t , line_sep ) ;
end
function to_text ( vCards )
if vCards [ 1 ] and vCards [ 1 ] . name then
return vcard_to_text ( vCards )
else
local t = { } ;
for i = 1 , # vCards do
t [ i ] = vcard_to_text ( vCards [ i ] ) ;
end
return t_concat ( t , line_sep ) ;
end
end
local function from_xep54_item ( item )
local prop_name = item.name ;
local prop_def = vCard_dtd [ prop_name ] ;
local prop = { name = prop_name } ;
if prop_def == " text " then
prop [ 1 ] = item : get_text ( ) ;
elseif type ( prop_def ) == " table " then
if prop_def.value then --single item
prop [ 1 ] = item : get_child_text ( prop_def.value ) or " " ;
elseif prop_def.values then --array
local value_names = prop_def.values ;
if value_names.behaviour == " repeat-last " then
for i = 1 , # item.tags do
t_insert ( prop , item.tags [ i ] : get_text ( ) or " " ) ;
end
else
for i = 1 , # value_names do
t_insert ( prop , item : get_child_text ( value_names [ i ] ) or " " ) ;
end
end
elseif prop_def.names then
local names = prop_def.names ;
for i = 1 , # names do
if item : get_child ( names [ i ] ) then
prop [ 1 ] = names [ i ] ;
break ;
end
end
end
if prop_def.props_verbatim then
for k , v in pairs ( prop_def.props_verbatim ) do
prop [ k ] = v ;
end
end
if prop_def.types then
local types = prop_def.types ;
prop.TYPE = { } ;
for i = 1 , # types do
if item : get_child ( types [ i ] ) then
t_insert ( prop.TYPE , types [ i ] : lower ( ) ) ;
end
end
if # prop.TYPE == 0 then
prop.TYPE = nil ;
end
end
-- A key-value pair, within a key-value pair?
if prop_def.props then
local params = prop_def.props ;
for i = 1 , # params do
local name = params [ i ]
local data = item : get_child_text ( name ) ;
if data then
prop [ name ] = prop [ name ] or { } ;
t_insert ( prop [ name ] , data ) ;
end
end
end
else
return nil
end
return prop ;
end
local function from_xep54_vCard ( vCard )
local tags = vCard.tags ;
local t = { } ;
for i = 1 , # tags do
t_insert ( t , from_xep54_item ( tags [ i ] ) ) ;
end
return t
end
function from_xep54 ( vCard )
if vCard.attr . xmlns ~= " vcard-temp " then
return nil , " wrong-xmlns " ;
end
if vCard.name == " xCard " then -- A collection of vCards
local t = { } ;
local vCards = vCard.tags ;
for i = 1 , # vCards do
t [ i ] = from_xep54_vCard ( vCards [ i ] ) ;
end
return t
elseif vCard.name == " vCard " then -- A single vCard
return from_xep54_vCard ( vCard )
end
end
local vcard4 = { }
function vcard4 : text ( node , params , value ) -- luacheck: ignore 212/params
self : tag ( node : lower ( ) )
-- FIXME params
if type ( value ) == " string " then
self : text_tag ( " text " , value ) ;
elseif vcard4 [ node ] then
vcard4 [ node ] ( value ) ;
end
self : up ( ) ;
end
function vcard4 . N ( value )
for i , k in ipairs ( vCard_dtd.N . values ) do
value : text_tag ( k , value [ i ] ) ;
end
end
local xmlns_vcard4 = " urn:ietf:params:xml:ns:vcard-4.0 "
local function item_to_vcard4 ( item )
local typ = item.name : lower ( ) ;
local t = st.stanza ( typ , { xmlns = xmlns_vcard4 } ) ;
local prop_def = vCard4_dtd [ typ ] ;
if prop_def == " text " then
t : text_tag ( " text " , item [ 1 ] ) ;
elseif prop_def == " uri " then
if item.ENCODING and item.ENCODING [ 1 ] == ' b ' then
t : text_tag ( " uri " , " data:;base64, " .. item [ 1 ] ) ;
else
t : text_tag ( " uri " , item [ 1 ] ) ;
end
elseif type ( prop_def ) == " table " then
if prop_def.values then
for i , v in ipairs ( prop_def.values ) do
t : text_tag ( v : lower ( ) , item [ i ] ) ;
end
else
t : tag ( " unsupported " , { xmlns = " http://zash.se/protocol/vcardlib " } )
end
else
t : tag ( " unsupported " , { xmlns = " http://zash.se/protocol/vcardlib " } )
end
return t ;
end
local function vcard_to_vcard4xml ( vCard )
local t = st.stanza ( " vcard " , { xmlns = xmlns_vcard4 } ) ;
for i = 1 , # vCard do
t : add_child ( item_to_vcard4 ( vCard [ i ] ) ) ;
end
return t ;
end
local function vcards_to_vcard4xml ( vCards )
if not vCards [ 1 ] or vCards [ 1 ] . name then
return vcard_to_vcard4xml ( vCards )
else
local t = st.stanza ( " vcards " , { xmlns = xmlns_vcard4 } ) ;
for i = 1 , # vCards do
t : add_child ( vcard_to_vcard4xml ( vCards [ i ] ) ) ;
end
return t ;
end
end
-- This was adapted from http://xmpp.org/extensions/xep-0054.html#dtd
vCard_dtd = {
VERSION = " text " , --MUST be 3.0, so parsing is redundant
FN = " text " ,
N = {
values = {
" FAMILY " ,
" GIVEN " ,
" MIDDLE " ,
" PREFIX " ,
" SUFFIX " ,
} ,
} ,
NICKNAME = " text " ,
PHOTO = {
props_verbatim = { ENCODING = { " b " } } ,
props = { " TYPE " } ,
value = " BINVAL " , --{ "EXTVAL", },
} ,
BDAY = " text " ,
ADR = {
types = {
" HOME " ,
" WORK " ,
" POSTAL " ,
" PARCEL " ,
" DOM " ,
" INTL " ,
" PREF " ,
} ,
values = {
" POBOX " ,
" EXTADD " ,
" STREET " ,
" LOCALITY " ,
" REGION " ,
" PCODE " ,
" CTRY " ,
}
} ,
LABEL = {
types = {
" HOME " ,
" WORK " ,
" POSTAL " ,
" PARCEL " ,
" DOM " ,
" INTL " ,
" PREF " ,
} ,
value = " LINE " ,
} ,
TEL = {
types = {
" HOME " ,
" WORK " ,
" VOICE " ,
" FAX " ,
" PAGER " ,
" MSG " ,
" CELL " ,
" VIDEO " ,
" BBS " ,
" MODEM " ,
" ISDN " ,
" PCS " ,
" PREF " ,
} ,
value = " NUMBER " ,
} ,
EMAIL = {
types = {
" HOME " ,
" WORK " ,
" INTERNET " ,
" PREF " ,
" X400 " ,
} ,
value = " USERID " ,
} ,
JABBERID = " text " ,
MAILER = " text " ,
TZ = " text " ,
GEO = {
values = {
" LAT " ,
" LON " ,
} ,
} ,
TITLE = " text " ,
ROLE = " text " ,
LOGO = " copy of PHOTO " ,
AGENT = " text " ,
ORG = {
values = {
behaviour = " repeat-last " ,
" ORGNAME " ,
" ORGUNIT " ,
}
} ,
CATEGORIES = {
values = " KEYWORD " ,
} ,
NOTE = " text " ,
PRODID = " text " ,
REV = " text " ,
SORTSTRING = " text " ,
SOUND = " copy of PHOTO " ,
UID = " text " ,
URL = " text " ,
CLASS = {
names = { -- The item.name is the value if it's one of these.
" PUBLIC " ,
" PRIVATE " ,
" CONFIDENTIAL " ,
} ,
} ,
KEY = {
props = { " TYPE " } ,
value = " CRED " ,
} ,
DESC = " text " ,
} ;
vCard_dtd.LOGO = vCard_dtd.PHOTO ;
vCard_dtd.SOUND = vCard_dtd.PHOTO ;
vCard4_dtd = {
source = " uri " ,
kind = " text " ,
xml = " text " ,
fn = " text " ,
n = {
values = {
" family " ,
" given " ,
" middle " ,
" prefix " ,
" suffix " ,
} ,
} ,
nickname = " text " ,
photo = " uri " ,
bday = " date-and-or-time " ,
anniversary = " date-and-or-time " ,
gender = " text " ,
adr = {
values = {
" pobox " ,
" ext " ,
" street " ,
" locality " ,
" region " ,
" code " ,
" country " ,
}
} ,
tel = " text " ,
email = " text " ,
impp = " uri " ,
lang = " language-tag " ,
tz = " text " ,
geo = " uri " ,
title = " text " ,
role = " text " ,
logo = " uri " ,
org = " text " ,
member = " uri " ,
related = " uri " ,
categories = " text " ,
note = " text " ,
prodid = " text " ,
rev = " timestamp " ,
sound = " uri " ,
uid = " uri " ,
clientpidmap = " number, uuid " ,
url = " uri " ,
version = " text " ,
key = " uri " ,
fburl = " uri " ,
caladruri = " uri " ,
caluri = " uri " ,
} ;
return {
from_text = from_text ;
to_text = to_text ;
from_xep54 = from_xep54 ;
to_xep54 = to_xep54 ;
to_vcard4 = vcards_to_vcard4xml ;
} ;