Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F256600
rcube_vcard.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
29 KB
Referenced Files
None
Subscribers
None
rcube_vcard.php
View Options
<?php
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2008-2012, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Logical representation of a vcard address record |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Logical representation of a vcard-based address record
* Provides functions to parse and export vCard data format
*
* @package Framework
* @subpackage Addressbook
*/
class
rcube_vcard
{
private
static
$values_decoded
=
false
;
private
$raw
=
array
(
'FN'
=>
array
(),
'N'
=>
array
(
array
(
''
,
''
,
''
,
''
,
''
)),
);
private
static
$fieldmap
=
array
(
'phone'
=>
'TEL'
,
'birthday'
=>
'BDAY'
,
'website'
=>
'URL'
,
'notes'
=>
'NOTE'
,
'email'
=>
'EMAIL'
,
'address'
=>
'ADR'
,
'jobtitle'
=>
'TITLE'
,
'department'
=>
'X-DEPARTMENT'
,
'gender'
=>
'X-GENDER'
,
'maidenname'
=>
'X-MAIDENNAME'
,
'anniversary'
=>
'X-ANNIVERSARY'
,
'assistant'
=>
'X-ASSISTANT'
,
'manager'
=>
'X-MANAGER'
,
'spouse'
=>
'X-SPOUSE'
,
'edit'
=>
'X-AB-EDIT'
,
);
private
$typemap
=
array
(
'IPHONE'
=>
'mobile'
,
'CELL'
=>
'mobile'
,
'WORK,FAX'
=>
'workfax'
,
);
private
$phonetypemap
=
array
(
'HOME1'
=>
'HOME'
,
'BUSINESS1'
=>
'WORK'
,
'BUSINESS2'
=>
'WORK2'
,
'BUSINESSFAX'
=>
'WORK,FAX'
,
'MOBILE'
=>
'CELL'
,
);
private
$addresstypemap
=
array
(
'BUSINESS'
=>
'WORK'
,
);
private
$immap
=
array
(
'X-JABBER'
=>
'jabber'
,
'X-ICQ'
=>
'icq'
,
'X-MSN'
=>
'msn'
,
'X-AIM'
=>
'aim'
,
'X-YAHOO'
=>
'yahoo'
,
'X-SKYPE'
=>
'skype'
,
'X-SKYPE-USERNAME'
=>
'skype'
,
);
public
$business
=
false
;
public
$displayname
;
public
$surname
;
public
$firstname
;
public
$middlename
;
public
$nickname
;
public
$organization
;
public
$email
=
array
();
public
static
$eol
=
"
\r\n
"
;
/**
* Constructor
*/
public
function
__construct
(
$vcard
=
null
,
$charset
=
RCUBE_CHARSET
,
$detect
=
false
,
$fieldmap
=
array
())
{
if
(!
empty
(
$fieldmap
))
{
$this
->
extend_fieldmap
(
$fieldmap
);
}
if
(!
empty
(
$vcard
))
{
$this
->
load
(
$vcard
,
$charset
,
$detect
);
}
}
/**
* Load record from (internal, unfolded) vcard 3.0 format
*
* @param string vCard string to parse
* @param string Charset of string values
* @param boolean True if loading a 'foreign' vcard and extra heuristics for charset detection is required
*/
public
function
load
(
$vcard
,
$charset
=
RCUBE_CHARSET
,
$detect
=
false
)
{
self
::
$values_decoded
=
false
;
$this
->
raw
=
self
::
vcard_decode
(
$vcard
);
// resolve charset parameters
if
(
$charset
==
null
)
{
$this
->
raw
=
self
::
charset_convert
(
$this
->
raw
);
}
// vcard has encoded values and charset should be detected
else
if
(
$detect
&&
self
::
$values_decoded
&&
(
$detected_charset
=
self
::
detect_encoding
(
self
::
vcard_encode
(
$this
->
raw
)))
&&
$detected_charset
!=
RCUBE_CHARSET
)
{
$this
->
raw
=
self
::
charset_convert
(
$this
->
raw
,
$detected_charset
);
}
// consider FN empty if the same as the primary e-mail address
if
(
$this
->
raw
[
'FN'
][
0
][
0
]
==
$this
->
raw
[
'EMAIL'
][
0
][
0
])
{
$this
->
raw
[
'FN'
][
0
][
0
]
=
''
;
}
// find well-known address fields
$this
->
displayname
=
$this
->
raw
[
'FN'
][
0
][
0
];
$this
->
surname
=
$this
->
raw
[
'N'
][
0
][
0
];
$this
->
firstname
=
$this
->
raw
[
'N'
][
0
][
1
];
$this
->
middlename
=
$this
->
raw
[
'N'
][
0
][
2
];
$this
->
nickname
=
$this
->
raw
[
'NICKNAME'
][
0
][
0
];
$this
->
organization
=
$this
->
raw
[
'ORG'
][
0
][
0
];
$this
->
business
=
(
$this
->
raw
[
'X-ABSHOWAS'
][
0
][
0
]
==
'COMPANY'
)
||
(
join
(
''
,
(
array
)
$this
->
raw
[
'N'
][
0
])
==
''
&&
!
empty
(
$this
->
organization
));
foreach
((
array
)
$this
->
raw
[
'EMAIL'
]
as
$i
=>
$raw_email
)
{
$this
->
email
[
$i
]
=
is_array
(
$raw_email
)
?
$raw_email
[
0
]
:
$raw_email
;
}
// make the pref e-mail address the first entry in $this->email
$pref_index
=
$this
->
get_type_index
(
'EMAIL'
,
'pref'
);
if
(
$pref_index
>
0
)
{
$tmp
=
$this
->
email
[
0
];
$this
->
email
[
0
]
=
$this
->
email
[
$pref_index
];
$this
->
email
[
$pref_index
]
=
$tmp
;
}
}
/**
* Return vCard data as associative array to be unsed in Roundcube address books
*
* @return array Hash array with key-value pairs
*/
public
function
get_assoc
()
{
$out
=
array
(
'name'
=>
$this
->
displayname
);
$typemap
=
$this
->
typemap
;
// copy name fields to output array
foreach
(
array
(
'firstname'
,
'surname'
,
'middlename'
,
'nickname'
,
'organization'
)
as
$col
)
{
if
(
strlen
(
$this
->
$col
))
{
$out
[
$col
]
=
$this
->
$col
;
}
}
if
(
$this
->
raw
[
'N'
][
0
][
3
])
$out
[
'prefix'
]
=
$this
->
raw
[
'N'
][
0
][
3
];
if
(
$this
->
raw
[
'N'
][
0
][
4
])
$out
[
'suffix'
]
=
$this
->
raw
[
'N'
][
0
][
4
];
// convert from raw vcard data into associative data for Roundcube
foreach
(
array_flip
(
self
::
$fieldmap
)
as
$tag
=>
$col
)
{
foreach
((
array
)
$this
->
raw
[
$tag
]
as
$i
=>
$raw
)
{
if
(
is_array
(
$raw
))
{
$k
=
-
1
;
$key
=
$col
;
$subtype
=
''
;
if
(!
empty
(
$raw
[
'type'
]))
{
$combined
=
join
(
','
,
self
::
array_filter
((
array
)
$raw
[
'type'
],
'internet,pref'
,
true
));
$combined
=
strtoupper
(
$combined
);
if
(
$typemap
[
$combined
])
{
$subtype
=
$typemap
[
$combined
];
}
else
if
(
$typemap
[
$raw
[
'type'
][++
$k
]])
{
$subtype
=
$typemap
[
$raw
[
'type'
][
$k
]];
}
else
{
$subtype
=
strtolower
(
$raw
[
'type'
][
$k
]);
}
while
(
$k
<
count
(
$raw
[
'type'
])
&&
(
$subtype
==
'internet'
||
$subtype
==
'pref'
))
{
$subtype
=
$typemap
[
$raw
[
'type'
][++
$k
]]
?
$typemap
[
$raw
[
'type'
][
$k
]]
:
strtolower
(
$raw
[
'type'
][
$k
]);
}
}
// read vcard 2.1 subtype
if
(!
$subtype
)
{
foreach
(
$raw
as
$k
=>
$v
)
{
if
(!
is_numeric
(
$k
)
&&
$v
===
true
&&
(
$k
=
strtolower
(
$k
))
&&
!
in_array
(
$k
,
array
(
'pref'
,
'internet'
,
'voice'
,
'base64'
))
)
{
$k_uc
=
strtoupper
(
$k
);
$subtype
=
$typemap
[
$k_uc
]
?
$typemap
[
$k_uc
]
:
$k
;
break
;
}
}
}
// force subtype if none set
if
(!
$subtype
&&
preg_match
(
'/^(email|phone|address|website)/'
,
$key
))
{
$subtype
=
'other'
;
}
if
(
$subtype
)
{
$key
.=
':'
.
$subtype
;
}
// split ADR values into assoc array
if
(
$tag
==
'ADR'
)
{
list
(,,
$value
[
'street'
],
$value
[
'locality'
],
$value
[
'region'
],
$value
[
'zipcode'
],
$value
[
'country'
])
=
$raw
;
$out
[
$key
][]
=
$value
;
}
else
{
$out
[
$key
][]
=
$raw
[
0
];
}
}
else
{
$out
[
$col
][]
=
$raw
;
}
}
}
// handle special IM fields as used by Apple
foreach
(
$this
->
immap
as
$tag
=>
$type
)
{
foreach
((
array
)
$this
->
raw
[
$tag
]
as
$i
=>
$raw
)
{
$out
[
'im:'
.
$type
][]
=
$raw
[
0
];
}
}
// copy photo data
if
(
$this
->
raw
[
'PHOTO'
])
{
$out
[
'photo'
]
=
$this
->
raw
[
'PHOTO'
][
0
][
0
];
}
return
$out
;
}
/**
* Convert the data structure into a vcard 3.0 string
*/
public
function
export
(
$folded
=
true
)
{
$vcard
=
self
::
vcard_encode
(
$this
->
raw
);
return
$folded
?
self
::
rfc2425_fold
(
$vcard
)
:
$vcard
;
}
/**
* Clear the given fields in the loaded vcard data
*
* @param array List of field names to be reset
*/
public
function
reset
(
$fields
=
null
)
{
if
(!
$fields
)
{
$fields
=
array_merge
(
array_values
(
self
::
$fieldmap
),
array_keys
(
$this
->
immap
),
array
(
'FN'
,
'N'
,
'ORG'
,
'NICKNAME'
,
'EMAIL'
,
'ADR'
,
'BDAY'
));
}
foreach
(
$fields
as
$f
)
{
unset
(
$this
->
raw
[
$f
]);
}
if
(!
$this
->
raw
[
'N'
])
{
$this
->
raw
[
'N'
]
=
array
(
array
(
''
,
''
,
''
,
''
,
''
));
}
if
(!
$this
->
raw
[
'FN'
])
{
$this
->
raw
[
'FN'
]
=
array
();
}
$this
->
email
=
array
();
}
/**
* Setter for address record fields
*
* @param string Field name
* @param string Field value
* @param string Type/section name
*/
public
function
set
(
$field
,
$value
,
$type
=
'HOME'
)
{
$field
=
strtolower
(
$field
);
$type_uc
=
strtoupper
(
$type
);
switch
(
$field
)
{
case
'name'
:
case
'displayname'
:
$this
->
raw
[
'FN'
][
0
][
0
]
=
$this
->
displayname
=
$value
;
break
;
case
'surname'
:
$this
->
raw
[
'N'
][
0
][
0
]
=
$this
->
surname
=
$value
;
break
;
case
'firstname'
:
$this
->
raw
[
'N'
][
0
][
1
]
=
$this
->
firstname
=
$value
;
break
;
case
'middlename'
:
$this
->
raw
[
'N'
][
0
][
2
]
=
$this
->
middlename
=
$value
;
break
;
case
'prefix'
:
$this
->
raw
[
'N'
][
0
][
3
]
=
$value
;
break
;
case
'suffix'
:
$this
->
raw
[
'N'
][
0
][
4
]
=
$value
;
break
;
case
'nickname'
:
$this
->
raw
[
'NICKNAME'
][
0
][
0
]
=
$this
->
nickname
=
$value
;
break
;
case
'organization'
:
$this
->
raw
[
'ORG'
][
0
][
0
]
=
$this
->
organization
=
$value
;
break
;
case
'photo'
:
if
(
strpos
(
$value
,
'http:'
)
===
0
)
{
// TODO: fetch file from URL and save it locally?
$this
->
raw
[
'PHOTO'
][
0
]
=
array
(
0
=>
$value
,
'url'
=>
true
);
}
else
{
$this
->
raw
[
'PHOTO'
][
0
]
=
array
(
0
=>
$value
,
'base64'
=>
(
bool
)
preg_match
(
'![^a-z0-9/=+-]!i'
,
$value
));
}
break
;
case
'email'
:
$this
->
raw
[
'EMAIL'
][]
=
array
(
0
=>
$value
,
'type'
=>
array_filter
(
array
(
'INTERNET'
,
$type_uc
)));
$this
->
email
[]
=
$value
;
break
;
case
'im'
:
// save IM subtypes into extension fields
$typemap
=
array_flip
(
$this
->
immap
);
if
(
$field
=
$typemap
[
strtolower
(
$type
)])
{
$this
->
raw
[
$field
][]
=
array
(
0
=>
$value
);
}
break
;
case
'birthday'
:
case
'anniversary'
:
if
((
$val
=
rcube_utils
::
strtotime
(
$value
))
&&
(
$fn
=
self
::
$fieldmap
[
$field
]))
{
$this
->
raw
[
$fn
][]
=
array
(
0
=>
date
(
'Y-m-d'
,
$val
),
'value'
=>
array
(
'date'
));
}
break
;
case
'address'
:
if
(
$this
->
addresstypemap
[
$type_uc
])
{
$type
=
$this
->
addresstypemap
[
$type_uc
];
}
$value
=
$value
[
0
]
?
$value
:
array
(
''
,
''
,
$value
[
'street'
],
$value
[
'locality'
],
$value
[
'region'
],
$value
[
'zipcode'
],
$value
[
'country'
]);
// fall through if not empty
if
(!
strlen
(
join
(
''
,
$value
)))
{
break
;
}
default
:
if
(
$field
==
'phone'
&&
$this
->
phonetypemap
[
$type_uc
])
{
$type
=
$this
->
phonetypemap
[
$type_uc
];
}
if
((
$tag
=
self
::
$fieldmap
[
$field
])
&&
(
is_array
(
$value
)
||
strlen
(
$value
)))
{
$index
=
count
(
$this
->
raw
[
$tag
]);
$this
->
raw
[
$tag
][
$index
]
=
(
array
)
$value
;
if
(
$type
)
{
$typemap
=
array_flip
(
$this
->
typemap
);
$this
->
raw
[
$tag
][
$index
][
'type'
]
=
explode
(
','
,
(
$typemap
[
$type_uc
]
?
$typemap
[
$type_uc
]
:
$type
));
}
}
break
;
}
}
/**
* Setter for individual vcard properties
*
* @param string VCard tag name
* @param array Value-set of this vcard property
* @param boolean Set to true if the value-set should be appended instead of replacing any existing value-set
*/
public
function
set_raw
(
$tag
,
$value
,
$append
=
false
)
{
$index
=
$append
?
count
(
$this
->
raw
[
$tag
])
:
0
;
$this
->
raw
[
$tag
][
$index
]
=
(
array
)
$value
;
}
/**
* Find index with the '$type' attribute
*
* @param string Field name
* @return int Field index having $type set
*/
private
function
get_type_index
(
$field
,
$type
=
'pref'
)
{
$result
=
0
;
if
(
$this
->
raw
[
$field
])
{
foreach
(
$this
->
raw
[
$field
]
as
$i
=>
$data
)
{
if
(
is_array
(
$data
[
'type'
])
&&
in_array_nocase
(
'pref'
,
$data
[
'type'
]))
{
$result
=
$i
;
}
}
}
return
$result
;
}
/**
* Convert a whole vcard (array) to UTF-8.
* If $force_charset is null, each member value that has a charset parameter will be converted
*/
private
static
function
charset_convert
(
$card
,
$force_charset
=
null
)
{
foreach
(
$card
as
$key
=>
$node
)
{
foreach
(
$node
as
$i
=>
$subnode
)
{
if
(
is_array
(
$subnode
)
&&
((
$charset
=
$force_charset
)
||
(
$subnode
[
'charset'
]
&&
(
$charset
=
$subnode
[
'charset'
][
0
]))))
{
foreach
(
$subnode
as
$j
=>
$value
)
{
if
(
is_numeric
(
$j
)
&&
is_string
(
$value
))
{
$card
[
$key
][
$i
][
$j
]
=
rcube_charset
::
convert
(
$value
,
$charset
);
}
}
unset
(
$card
[
$key
][
$i
][
'charset'
]);
}
}
}
return
$card
;
}
/**
* Extends fieldmap definition
*/
public
function
extend_fieldmap
(
$map
)
{
if
(
is_array
(
$map
))
{
self
::
$fieldmap
=
array_merge
(
$map
,
self
::
$fieldmap
);
}
}
/**
* Factory method to import a vcard file
*
* @param string vCard file content
*
* @return array List of rcube_vcard objects
*/
public
static
function
import
(
$data
)
{
$out
=
array
();
// check if charsets are specified (usually vcard version < 3.0 but this is not reliable)
if
(
preg_match
(
'/charset=/i'
,
substr
(
$data
,
0
,
2048
)))
{
$charset
=
null
;
}
// detect charset and convert to utf-8
else
if
((
$charset
=
self
::
detect_encoding
(
$data
))
&&
$charset
!=
RCUBE_CHARSET
)
{
$data
=
rcube_charset
::
convert
(
$data
,
$charset
);
$data
=
preg_replace
(
array
(
'/^[
\x
FE
\x
FF]{2}/'
,
'/^
\x
EF
\x
BB
\x
BF/'
,
'/^
\x
00+/'
),
''
,
$data
);
// also remove BOM
$charset
=
RCUBE_CHARSET
;
}
$vcard_block
=
''
;
$in_vcard_block
=
false
;
foreach
(
preg_split
(
"/[
\r\n
]+/"
,
$data
)
as
$line
)
{
if
(
$in_vcard_block
&&
!
empty
(
$line
))
{
$vcard_block
.=
$line
.
"
\n
"
;
}
$line
=
trim
(
$line
);
if
(
preg_match
(
'/^END:VCARD$/i'
,
$line
))
{
// parse vcard
$obj
=
new
rcube_vcard
(
self
::
cleanup
(
$vcard_block
),
$charset
,
true
,
self
::
$fieldmap
);
// FN and N is required by vCard format (RFC 2426)
// on import we can be less restrictive, let's addressbook decide
if
(!
empty
(
$obj
->
displayname
)
||
!
empty
(
$obj
->
surname
)
||
!
empty
(
$obj
->
firstname
)
||
!
empty
(
$obj
->
email
))
{
$out
[]
=
$obj
;
}
$in_vcard_block
=
false
;
}
else
if
(
preg_match
(
'/^BEGIN:VCARD$/i'
,
$line
))
{
$vcard_block
=
$line
.
"
\n
"
;
$in_vcard_block
=
true
;
}
}
return
$out
;
}
/**
* Normalize vcard data for better parsing
*
* @param string vCard block
*
* @return string Cleaned vcard block
*/
public
static
function
cleanup
(
$vcard
)
{
// Convert special types (like Skype) to normal type='skype' classes with this simple regex ;)
$vcard
=
preg_replace
(
'/item(
\d
+)
\.
(TEL|EMAIL|URL)([^:]*?):(.*?)item
\1
.X-ABLabel:(?:_
\$
!<)?([
\w
-() ]*)(?:>!
\$
_)?./s'
,
'
\2
;type=
\5\3
:
\4
'
,
$vcard
);
// convert Apple X-ABRELATEDNAMES into X-* fields for better compatibility
$vcard
=
preg_replace_callback
(
'/item(
\d
+)
\.
(X-ABRELATEDNAMES)([^:]*?):(.*?)item
\1
.X-ABLabel:(?:_
\$
!<)?([
\w
-() ]*)(?:>!
\$
_)?./s'
,
array
(
'self'
,
'x_abrelatednames_callback'
),
$vcard
);
// Remove cruft like item1.X-AB*, item1.ADR instead of ADR, and empty lines
$vcard
=
preg_replace
(
array
(
'/^item
\d
*
\.
X-AB.*$/m'
,
'/^item
\d
*
\.
/m'
,
"/
\n
+/"
),
array
(
''
,
''
,
"
\n
"
),
$vcard
);
// convert X-WAB-GENDER to X-GENDER
if
(
preg_match
(
'/X-WAB-GENDER:(
\d
)/'
,
$vcard
,
$matches
))
{
$value
=
$matches
[
1
]
==
'2'
?
'male'
:
'female'
;
$vcard
=
preg_replace
(
'/X-WAB-GENDER:
\d
/'
,
'X-GENDER:'
.
$value
,
$vcard
);
}
// if N doesn't have any semicolons, add some
$vcard
=
preg_replace
(
'/^(N:[^;
\R
]*)$/m'
,
'
\1
;;;;'
,
$vcard
);
return
$vcard
;
}
private
static
function
x_abrelatednames_callback
(
$matches
)
{
return
'X-'
.
strtoupper
(
$matches
[
5
])
.
$matches
[
3
]
.
':'
.
$matches
[
4
];
}
private
static
function
rfc2425_fold_callback
(
$matches
)
{
// chunk_split string and avoid lines breaking multibyte characters
$c
=
71
;
$out
.=
substr
(
$matches
[
1
],
0
,
$c
);
for
(
$n
=
$c
;
$c
<
strlen
(
$matches
[
1
]);
$c
++)
{
// break if length > 75 or mutlibyte character starts after position 71
if
(
$n
>
75
||
(
$n
>
71
&&
ord
(
$matches
[
1
][
$c
])
>>
6
==
3
))
{
$out
.=
"
\r\n
"
;
$n
=
0
;
}
$out
.=
$matches
[
1
][
$c
];
$n
++;
}
return
$out
;
}
public
static
function
rfc2425_fold
(
$val
)
{
return
preg_replace_callback
(
'/([^
\n
]{72,})/'
,
array
(
'self'
,
'rfc2425_fold_callback'
),
$val
);
}
/**
* Decodes a vcard block (vcard 3.0 format, unfolded)
* into an array structure
*
* @param string vCard block to parse
*
* @return array Raw data structure
*/
private
static
function
vcard_decode
(
$vcard
)
{
// Perform RFC2425 line unfolding and split lines
$vcard
=
preg_replace
(
array
(
"/
\r
/"
,
"/
\n\s
+/"
),
''
,
$vcard
);
$lines
=
explode
(
"
\n
"
,
$vcard
);
$data
=
array
();
for
(
$i
=
0
;
$i
<
count
(
$lines
);
$i
++)
{
if
(!
preg_match
(
'/^([^:]+):(.+)$/'
,
$lines
[
$i
],
$line
))
continue
;
if
(
preg_match
(
'/^(BEGIN|END)$/i'
,
$line
[
1
]))
continue
;
// convert 2.1-style "EMAIL;internet;home:" to 3.0-style "EMAIL;TYPE=internet;TYPE=home:"
if
(
$data
[
'VERSION'
][
0
]
==
"2.1"
&&
preg_match
(
'/^([^;]+);([^:]+)/'
,
$line
[
1
],
$regs2
)
&&
!
preg_match
(
'/^TYPE=/i'
,
$regs2
[
2
])
)
{
$line
[
1
]
=
$regs2
[
1
];
foreach
(
explode
(
';'
,
$regs2
[
2
])
as
$prop
)
{
$line
[
1
]
.=
';'
.
(
strpos
(
$prop
,
'='
)
?
$prop
:
'TYPE='
.
$prop
);
}
}
if
(
preg_match_all
(
'/([^
\\
;]+);?/'
,
$line
[
1
],
$regs2
))
{
$entry
=
array
();
$field
=
strtoupper
(
$regs2
[
1
][
0
]);
$enc
=
null
;
foreach
(
$regs2
[
1
]
as
$attrid
=>
$attr
)
{
if
((
list
(
$key
,
$value
)
=
explode
(
'='
,
$attr
))
&&
$value
)
{
$value
=
trim
(
$value
);
if
(
$key
==
'ENCODING'
)
{
$value
=
strtoupper
(
$value
);
// add next line(s) to value string if QP line end detected
if
(
$value
==
'QUOTED-PRINTABLE'
)
{
while
(
preg_match
(
'/=$/'
,
$lines
[
$i
]))
{
$line
[
2
]
.=
"
\n
"
.
$lines
[++
$i
];
}
}
$enc
=
$value
;
}
else
{
$lc_key
=
strtolower
(
$key
);
$entry
[
$lc_key
]
=
array_merge
((
array
)
$entry
[
$lc_key
],
(
array
)
self
::
vcard_unquote
(
$value
,
','
));
}
}
else
if
(
$attrid
>
0
)
{
$entry
[
strtolower
(
$key
)]
=
true
;
// true means attr without =value
}
}
// decode value
if
(
$enc
||
!
empty
(
$entry
[
'base64'
]))
{
// save encoding type (#1488432)
if
(
$enc
==
'B'
)
{
$entry
[
'encoding'
]
=
'B'
;
// should we use vCard 3.0 instead?
// $entry['base64'] = true;
}
$line
[
2
]
=
self
::
decode_value
(
$line
[
2
],
$enc
?
$enc
:
'base64'
);
}
if
(
$enc
!=
'B'
&&
empty
(
$entry
[
'base64'
]))
{
$line
[
2
]
=
self
::
vcard_unquote
(
$line
[
2
]);
}
$entry
=
array_merge
(
$entry
,
(
array
)
$line
[
2
]);
$data
[
$field
][]
=
$entry
;
}
}
unset
(
$data
[
'VERSION'
]);
return
$data
;
}
/**
* Decode a given string with the encoding rule from ENCODING attributes
*
* @param string String to decode
* @param string Encoding type (quoted-printable and base64 supported)
*
* @return string Decoded 8bit value
*/
private
static
function
decode_value
(
$value
,
$encoding
)
{
switch
(
strtolower
(
$encoding
))
{
case
'quoted-printable'
:
self
::
$values_decoded
=
true
;
return
quoted_printable_decode
(
$value
);
case
'base64'
:
case
'b'
:
self
::
$values_decoded
=
true
;
return
base64_decode
(
$value
);
default
:
return
$value
;
}
}
/**
* Encodes an entry for storage in our database (vcard 3.0 format, unfolded)
*
* @param array Raw data structure to encode
*
* @return string vCard encoded string
*/
static
function
vcard_encode
(
$data
)
{
foreach
((
array
)
$data
as
$type
=>
$entries
)
{
// valid N has 5 properties
while
(
$type
==
"N"
&&
is_array
(
$entries
[
0
])
&&
count
(
$entries
[
0
])
<
5
)
{
$entries
[
0
][]
=
""
;
}
// make sure FN is not empty (required by RFC2426)
if
(
$type
==
"FN"
&&
empty
(
$entries
))
{
$entries
[
0
]
=
$data
[
'EMAIL'
][
0
][
0
];
}
foreach
((
array
)
$entries
as
$entry
)
{
$attr
=
''
;
if
(
is_array
(
$entry
))
{
$value
=
array
();
foreach
(
$entry
as
$attrname
=>
$attrvalues
)
{
if
(
is_int
(
$attrname
))
{
if
(!
empty
(
$entry
[
'base64'
])
||
$entry
[
'encoding'
]
==
'B'
)
{
$attrvalues
=
base64_encode
(
$attrvalues
);
}
$value
[]
=
$attrvalues
;
}
else
if
(
is_bool
(
$attrvalues
))
{
// true means just a tag, not tag=value, as in PHOTO;BASE64:...
if
(
$attrvalues
)
{
// vCard v3 uses ENCODING=B (#1489183)
if
(
$attrname
==
'base64'
)
{
$attr
.=
";ENCODING=B"
;
}
else
{
$attr
.=
strtoupper
(
";$attrname"
);
}
}
}
else
{
foreach
((
array
)
$attrvalues
as
$attrvalue
)
{
$attr
.=
strtoupper
(
";$attrname="
)
.
self
::
vcard_quote
(
$attrvalue
,
','
);
}
}
}
}
else
{
$value
=
$entry
;
}
// skip empty entries
if
(
self
::
is_empty
(
$value
))
{
continue
;
}
$vcard
.=
self
::
vcard_quote
(
$type
)
.
$attr
.
':'
.
self
::
vcard_quote
(
$value
)
.
self
::
$eol
;
}
}
return
'BEGIN:VCARD'
.
self
::
$eol
.
'VERSION:3.0'
.
self
::
$eol
.
$vcard
.
'END:VCARD'
;
}
/**
* Join indexed data array to a vcard quoted string
*
* @param array Field data
* @param string Separator
*
* @return string Joined and quoted string
*/
private
static
function
vcard_quote
(
$s
,
$sep
=
';'
)
{
if
(
is_array
(
$s
))
{
foreach
(
$s
as
$part
)
{
$r
[]
=
self
::
vcard_quote
(
$part
,
$sep
);
}
return
(
implode
(
$sep
,
(
array
)
$r
));
}
return
strtr
(
$s
,
array
(
'
\\
'
=>
'
\\\\
'
,
"
\r
"
=>
''
,
"
\n
"
=>
'
\n
'
,
','
=>
'
\,
'
,
';'
=>
'
\;
'
));
}
/**
* Split quoted string
*
* @param string vCard string to split
* @param string Separator char/string
*
* @return array List with splited values
*/
private
static
function
vcard_unquote
(
$s
,
$sep
=
';'
)
{
// break string into parts separated by $sep
if
(!
empty
(
$sep
))
{
// Handle properly backslash escaping (#1488896)
$rep1
=
array
(
"
\\\\
"
=>
"
\0
10"
,
"
\\
$sep"
=>
"
\0
07"
);
$rep2
=
array
(
"
\0
07"
=>
"
\\
$sep"
,
"
\0
10"
=>
"
\\\\
"
);
if
(
count
(
$parts
=
explode
(
$sep
,
strtr
(
$s
,
$rep1
)))
>
1
)
{
foreach
(
$parts
as
$s
)
{
$result
[]
=
self
::
vcard_unquote
(
strtr
(
$s
,
$rep2
));
}
return
$result
;
}
$s
=
strtr
(
$s
,
$rep2
);
}
// some implementations (GMail) use non-standard backslash before colon (#1489085)
// we will handle properly any backslashed character - removing dummy backslahes
// return strtr($s, array("\r" => '', '\\\\' => '\\', '\n' => "\n", '\N' => "\n", '\,' => ',', '\;' => ';'));
$s
=
str_replace
(
"
\r
"
,
''
,
$s
);
$pos
=
0
;
while
((
$pos
=
strpos
(
$s
,
'
\\
'
,
$pos
))
!==
false
)
{
$next
=
substr
(
$s
,
$pos
+
1
,
1
);
if
(
$next
==
'n'
||
$next
==
'N'
)
{
$s
=
substr_replace
(
$s
,
"
\n
"
,
$pos
,
2
);
}
else
{
$s
=
substr_replace
(
$s
,
''
,
$pos
,
1
);
}
$pos
+=
1
;
}
return
$s
;
}
/**
* Check if vCard entry is empty: empty string or an array with
* all entries empty.
*
* @param mixed $value Attribute value (string or array)
*
* @return bool True if the value is empty, False otherwise
*/
private
static
function
is_empty
(
$value
)
{
foreach
((
array
)
$value
as
$v
)
{
if
(((
string
)
$v
)
!==
''
)
{
return
false
;
}
}
return
true
;
}
/**
* Extract array values by a filter
*
* @param array Array to filter
* @param keys Array or comma separated list of values to keep
* @param boolean Invert key selection: remove the listed values
*
* @return array The filtered array
*/
private
static
function
array_filter
(
$arr
,
$values
,
$inverse
=
false
)
{
if
(!
is_array
(
$values
))
{
$values
=
explode
(
','
,
$values
);
}
$result
=
array
();
$keep
=
array_flip
((
array
)
$values
);
foreach
(
$arr
as
$key
=>
$val
)
{
if
(
$inverse
!=
isset
(
$keep
[
strtolower
(
$val
)]))
{
$result
[
$key
]
=
$val
;
}
}
return
$result
;
}
/**
* Returns UNICODE type based on BOM (Byte Order Mark)
*
* @param string Input string to test
*
* @return string Detected encoding
*/
private
static
function
detect_encoding
(
$string
)
{
$fallback
=
rcube
::
get_instance
()->
config
->
get
(
'default_charset'
,
'ISO-8859-1'
);
// fallback to Latin-1
return
rcube_charset
::
detect
(
$string
,
$fallback
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sun, Jun 8, 10:53 AM (6 h, 19 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
196684
Default Alt Text
rcube_vcard.php (29 KB)
Attached To
Mode
R26 chwala
Attached
Detach File
Event Timeline
Log In to Comment