2013-05-20 23:09:51 +02:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* vCard class for parsing a vCard and/or creating one
|
|
|
|
*
|
|
|
|
* @link https://github.com/nuovo/vCard-parser
|
|
|
|
* @author Martins Pilsetnieks, Roberts Bruveris
|
|
|
|
* @see RFC 2426, RFC 2425
|
2016-06-13 10:18:47 +02:00
|
|
|
* @version 0.4.9
|
|
|
|
*/
|
2013-05-20 23:09:51 +02:00
|
|
|
class vCard implements Countable, Iterator
|
|
|
|
{
|
|
|
|
const MODE_ERROR = 'error';
|
|
|
|
const MODE_SINGLE = 'single';
|
|
|
|
const MODE_MULTIPLE = 'multiple';
|
|
|
|
|
|
|
|
const endl = "\n";
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var string Current object mode - error, single or multiple (for a single vCard within a file and multiple combined vCards)
|
|
|
|
*/
|
|
|
|
private $Mode; //single, multiple, error
|
|
|
|
|
|
|
|
private $Path = '';
|
|
|
|
private $RawData = '';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array Internal options container. Options:
|
|
|
|
* bool Collapse: If true, elements that can have multiple values but have only a single value are returned as that value instead of an array
|
|
|
|
* If false, an array is returned even if it has only one value.
|
|
|
|
*/
|
|
|
|
private $Options = array(
|
|
|
|
'Collapse' => false
|
|
|
|
);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var array Internal data container. Contains vCard objects for multiple vCards and just the data for single vCards.
|
|
|
|
*/
|
|
|
|
private $Data = array();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @static Parts of structured elements according to the spec.
|
|
|
|
*/
|
|
|
|
private static $Spec_StructuredElements = array(
|
2016-06-13 10:18:47 +02:00
|
|
|
'n' => array('lastname', 'firstname', 'additionalnames', 'prefixes', 'suffixes'),
|
|
|
|
'adr' => array('pobox', 'extendedaddress', 'streetaddress', 'locality', 'region', 'postalcode', 'country'),
|
|
|
|
'geo' => array('latitude', 'longitude'),
|
|
|
|
'org' => array('name', 'unit1', 'unit2')
|
2013-05-20 23:09:51 +02:00
|
|
|
);
|
|
|
|
private static $Spec_MultipleValueElements = array('nickname', 'categories');
|
|
|
|
|
|
|
|
private static $Spec_ElementTypes = array(
|
2016-06-13 10:18:47 +02:00
|
|
|
'email' => array('internet', 'x400', 'pref', 'home', 'work'),
|
2013-05-20 23:09:51 +02:00
|
|
|
'adr' => array('dom', 'intl', 'postal', 'parcel', 'home', 'work', 'pref'),
|
|
|
|
'label' => array('dom', 'intl', 'postal', 'parcel', 'home', 'work', 'pref'),
|
|
|
|
'tel' => array('home', 'msg', 'work', 'pref', 'voice', 'fax', 'cell', 'video', 'pager', 'bbs', 'modem', 'car', 'isdn', 'pcs'),
|
|
|
|
'impp' => array('personal', 'business', 'home', 'work', 'mobile', 'pref')
|
|
|
|
);
|
|
|
|
|
|
|
|
private static $Spec_FileElements = array('photo', 'logo', 'sound');
|
|
|
|
|
|
|
|
/**
|
|
|
|
* vCard constructor
|
|
|
|
*
|
|
|
|
* @param string Path to file, optional.
|
|
|
|
* @param string Raw data, optional.
|
|
|
|
* @param array Additional options, optional. Currently supported options:
|
|
|
|
* bool Collapse: If true, elements that can have multiple values but have only a single value are returned as that value instead of an array
|
|
|
|
* If false, an array is returned even if it has only one value.
|
|
|
|
*
|
|
|
|
* One of these parameters must be provided, otherwise an exception is thrown.
|
|
|
|
*/
|
|
|
|
public function __construct($Path = false, $RawData = false, array $Options = null)
|
|
|
|
{
|
|
|
|
// Checking preconditions for the parser.
|
|
|
|
// If path is given, the file should be accessible.
|
|
|
|
// If raw data is given, it is taken as it is.
|
|
|
|
// In both cases the real content is put in $this -> RawData
|
|
|
|
if ($Path)
|
|
|
|
{
|
|
|
|
if (!is_readable($Path))
|
|
|
|
{
|
|
|
|
throw new Exception('vCard: Path not accessible ('.$Path.')');
|
|
|
|
}
|
|
|
|
|
|
|
|
$this -> Path = $Path;
|
|
|
|
$this -> RawData = file_get_contents($this -> Path);
|
|
|
|
}
|
|
|
|
elseif ($RawData)
|
|
|
|
{
|
|
|
|
$this -> RawData = $RawData;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
//throw new Exception('vCard: No content provided');
|
|
|
|
// Not necessary anymore as possibility to create vCards is added
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$this -> Path && !$this -> RawData)
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($Options)
|
|
|
|
{
|
|
|
|
$this -> Options = array_merge($this -> Options, $Options);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Counting the begin/end separators. If there aren't any or the count doesn't match, there is a problem with the file.
|
|
|
|
// If there is only one, this is a single vCard, if more, multiple vCards are combined.
|
|
|
|
$Matches = array();
|
|
|
|
$vCardBeginCount = preg_match_all('{^BEGIN\:VCARD}miS', $this -> RawData, $Matches);
|
|
|
|
$vCardEndCount = preg_match_all('{^END\:VCARD}miS', $this -> RawData, $Matches);
|
|
|
|
|
|
|
|
if (($vCardBeginCount != $vCardEndCount) || !$vCardBeginCount)
|
|
|
|
{
|
|
|
|
$this -> Mode = vCard::MODE_ERROR;
|
|
|
|
throw new Exception('vCard: invalid vCard');
|
|
|
|
}
|
|
|
|
|
|
|
|
$this -> Mode = $vCardBeginCount == 1 ? vCard::MODE_SINGLE : vCard::MODE_MULTIPLE;
|
|
|
|
|
|
|
|
// Removing/changing inappropriate newlines, i.e., all CRs or multiple newlines are changed to a single newline
|
2016-06-13 10:18:47 +02:00
|
|
|
|
|
|
|
// MCA: removed, this break crlf vcard specification, all line dilimiter are CRLF
|
|
|
|
//$this -> RawData = str_replace("\r", "\n", $this -> RawData);
|
|
|
|
//$this -> RawData = preg_replace('{(\n)+}', "\n", $this -> RawData);
|
2013-05-20 23:09:51 +02:00
|
|
|
|
|
|
|
// In multiple card mode the raw text is split at card beginning markers and each
|
|
|
|
// fragment is parsed in a separate vCard object.
|
|
|
|
if ($this -> Mode == self::MODE_MULTIPLE)
|
|
|
|
{
|
2016-06-13 10:18:47 +02:00
|
|
|
//Cannot use "explode", because we need to ignore, for example, 'AGENT:BEGIN:VCARD'
|
|
|
|
$this -> RawData = preg_split('{^BEGIN\:VCARD}miS', $this -> RawData);
|
2013-05-20 23:09:51 +02:00
|
|
|
$this -> RawData = array_filter($this -> RawData);
|
|
|
|
|
|
|
|
foreach ($this -> RawData as $SinglevCardRawData)
|
|
|
|
{
|
2016-06-13 10:18:47 +02:00
|
|
|
// mca: remove \n and \r at start
|
|
|
|
//$SinglevCardRawData=ltrim($SinglevCardRawData);
|
2013-05-20 23:09:51 +02:00
|
|
|
// Prepending "BEGIN:VCARD" to the raw string because we exploded on that one.
|
|
|
|
// If there won't be the BEGIN marker in the new object, it will fail.
|
2016-06-13 10:18:47 +02:00
|
|
|
$SinglevCardRawData = 'BEGIN:VCARD'.$SinglevCardRawData;
|
2013-05-20 23:09:51 +02:00
|
|
|
|
|
|
|
$ClassName = get_class($this);
|
|
|
|
$this -> Data[] = new $ClassName(false, $SinglevCardRawData);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// Joining multiple lines that are split with a hard wrap and indicated by an equals sign at the end of line
|
|
|
|
// (quoted-printable-encoded values in v2.1 vCards)
|
2016-06-13 10:18:47 +02:00
|
|
|
$this -> RawData = str_replace("=\r\n", '', $this -> RawData);
|
|
|
|
|
|
|
|
// Protect the BASE64 final = sign (detected by the line beginning with whitespace), otherwise the next replace will get rid of it
|
|
|
|
$this -> RawData = preg_replace('{(\r\n\s.+)=(\r\n)}', '$1-base64=-$2', $this -> RawData);
|
2013-05-20 23:09:51 +02:00
|
|
|
|
|
|
|
// Joining multiple lines that are split with a soft wrap (space or tab on the beginning of the next line
|
2016-06-13 10:18:47 +02:00
|
|
|
$this -> RawData = str_replace(array("\r\n ", "\r\n\t"), '-wrap-', $this -> RawData);
|
2013-05-20 23:09:51 +02:00
|
|
|
|
|
|
|
// Restoring the BASE64 final equals sign (see a few lines above)
|
2016-06-13 10:18:47 +02:00
|
|
|
$this -> RawData = str_replace("-base64=-\r\n", "=\r\n", $this -> RawData);
|
2013-05-20 23:09:51 +02:00
|
|
|
|
2016-06-13 10:18:47 +02:00
|
|
|
$Lines = explode("\r\n", $this -> RawData);
|
2013-05-20 23:09:51 +02:00
|
|
|
|
|
|
|
foreach ($Lines as $Line)
|
|
|
|
{
|
|
|
|
// Lines without colons are skipped because, most likely, they contain no data.
|
|
|
|
if (strpos($Line, ':') === false)
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Each line is split into two parts. The key contains the element name and additional parameters, if present,
|
|
|
|
// value is just the value
|
|
|
|
list($Key, $Value) = explode(':', $Line, 2);
|
|
|
|
|
|
|
|
// Key is transformed to lowercase because, even though the element and parameter names are written in uppercase,
|
|
|
|
// it is quite possible that they will be in lower- or mixed case.
|
|
|
|
// The key is trimmed to allow for non-significant WSP characters as allowed by v2.1
|
|
|
|
$Key = strtolower(trim(self::Unescape($Key)));
|
|
|
|
|
|
|
|
// These two lines can be skipped as they aren't necessary at all.
|
|
|
|
if ($Key == 'begin' || $Key == 'end')
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((strpos($Key, 'agent') === 0) && (stripos($Value, 'begin:vcard') !== false))
|
|
|
|
{
|
|
|
|
$ClassName = get_class($this);
|
|
|
|
$Value = new $ClassName(false, str_replace('-wrap-', "\n", $Value));
|
|
|
|
if (!isset($this -> Data[$Key]))
|
|
|
|
{
|
|
|
|
$this -> Data[$Key] = array();
|
|
|
|
}
|
|
|
|
$this -> Data[$Key][] = $Value;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
$Value = str_replace('-wrap-', '', $Value);
|
|
|
|
}
|
|
|
|
|
|
|
|
$Value = trim(self::Unescape($Value));
|
|
|
|
$Type = array();
|
|
|
|
|
|
|
|
// Here additional parameters are parsed
|
|
|
|
$KeyParts = explode(';', $Key);
|
|
|
|
$Key = $KeyParts[0];
|
|
|
|
$Encoding = false;
|
|
|
|
|
|
|
|
if (strpos($Key, 'item') === 0)
|
|
|
|
{
|
|
|
|
$TmpKey = explode('.', $Key, 2);
|
|
|
|
$Key = $TmpKey[1];
|
|
|
|
$ItemIndex = (int)str_ireplace('item', '', $TmpKey[0]);
|
|
|
|
}
|
|
|
|
|
2016-06-13 10:18:47 +02:00
|
|
|
|
2013-05-20 23:09:51 +02:00
|
|
|
if (count($KeyParts) > 1)
|
|
|
|
{
|
|
|
|
$Parameters = self::ParseParameters($Key, array_slice($KeyParts, 1));
|
|
|
|
|
|
|
|
foreach ($Parameters as $ParamKey => $ParamValue)
|
|
|
|
{
|
|
|
|
switch ($ParamKey)
|
|
|
|
{
|
|
|
|
case 'encoding':
|
|
|
|
$Encoding = $ParamValue;
|
|
|
|
if (in_array($ParamValue, array('b', 'base64')))
|
|
|
|
{
|
|
|
|
//$Value = base64_decode($Value);
|
|
|
|
}
|
|
|
|
elseif ($ParamValue == 'quoted-printable') // v2.1
|
|
|
|
{
|
|
|
|
$Value = quoted_printable_decode($Value);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'charset': // v2.1
|
|
|
|
if ($ParamValue != 'utf-8' && $ParamValue != 'utf8')
|
|
|
|
{
|
|
|
|
$Value = mb_convert_encoding($Value, 'UTF-8', $ParamValue);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'type':
|
|
|
|
$Type = $ParamValue;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Checking files for colon-separated additional parameters (Apple's Address Book does this), for example, "X-ABCROP-RECTANGLE" for photos
|
|
|
|
if (in_array($Key, self::$Spec_FileElements) && isset($Parameters['encoding']) && in_array($Parameters['encoding'], array('b', 'base64')))
|
|
|
|
{
|
|
|
|
// If colon is present in the value, it must contain Address Book parameters
|
|
|
|
// (colon is an invalid character for base64 so it shouldn't appear in valid files)
|
|
|
|
if (strpos($Value, ':') !== false)
|
|
|
|
{
|
|
|
|
$Value = explode(':', $Value);
|
|
|
|
$Value = array_pop($Value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Values are parsed according to their type
|
|
|
|
if (isset(self::$Spec_StructuredElements[$Key]))
|
|
|
|
{
|
|
|
|
$Value = self::ParseStructuredValue($Value, $Key);
|
|
|
|
if ($Type)
|
|
|
|
{
|
2016-06-13 10:18:47 +02:00
|
|
|
$Value['type'] = $Type;
|
2013-05-20 23:09:51 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (in_array($Key, self::$Spec_MultipleValueElements))
|
|
|
|
{
|
|
|
|
$Value = self::ParseMultipleTextValue($Value, $Key);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($Type)
|
|
|
|
{
|
|
|
|
$Value = array(
|
2016-06-13 10:18:47 +02:00
|
|
|
'value' => $Value,
|
|
|
|
'type' => $Type
|
2013-05-20 23:09:51 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (is_array($Value) && $Encoding)
|
|
|
|
{
|
2016-06-13 10:18:47 +02:00
|
|
|
$Value['encoding'] = $Encoding;
|
2013-05-20 23:09:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!isset($this -> Data[$Key]))
|
|
|
|
{
|
|
|
|
$this -> Data[$Key] = array();
|
|
|
|
}
|
|
|
|
|
|
|
|
$this -> Data[$Key][] = $Value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-13 10:18:47 +02:00
|
|
|
/**
|
|
|
|
* method to get key list of the current vcard
|
|
|
|
*
|
|
|
|
* @return array list of key
|
|
|
|
*/
|
|
|
|
public function getKeyList()
|
|
|
|
{
|
|
|
|
$keylist=array();
|
|
|
|
if (isset($this -> Data))
|
|
|
|
{
|
|
|
|
foreach($this -> Data as $key => $val)
|
|
|
|
$keylist[]=$key;
|
|
|
|
}
|
|
|
|
return $keylist;
|
|
|
|
}
|
|
|
|
|
2013-05-20 23:09:51 +02:00
|
|
|
/**
|
|
|
|
* Magic method to get the various vCard values as object members, e.g.
|
|
|
|
* a call to $vCard -> N gets the "N" value
|
|
|
|
*
|
|
|
|
* @param string Key
|
|
|
|
*
|
|
|
|
* @return mixed Value
|
|
|
|
*/
|
|
|
|
public function __get($Key)
|
|
|
|
{
|
|
|
|
$Key = strtolower($Key);
|
|
|
|
if (isset($this -> Data[$Key]))
|
|
|
|
{
|
|
|
|
if ($Key == 'agent')
|
|
|
|
{
|
|
|
|
return $this -> Data[$Key];
|
|
|
|
}
|
|
|
|
elseif (in_array($Key, self::$Spec_FileElements))
|
|
|
|
{
|
|
|
|
$Value = $this -> Data[$Key];
|
|
|
|
foreach ($Value as $K => $V)
|
|
|
|
{
|
2016-06-13 10:18:47 +02:00
|
|
|
if (isset($V['Value']) && stripos($V['Value'], 'uri:') === 0)
|
2013-05-20 23:09:51 +02:00
|
|
|
{
|
2016-06-13 10:18:47 +02:00
|
|
|
$Value[$K]['value'] = substr($V, 4);
|
|
|
|
$Value[$K]['encoding'] = 'uri';
|
2013-05-20 23:09:51 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return $Value;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($this -> Options['Collapse'] && is_array($this -> Data[$Key]) && (count($this -> Data[$Key]) == 1))
|
|
|
|
{
|
|
|
|
return $this -> Data[$Key][0];
|
|
|
|
}
|
|
|
|
return $this -> Data[$Key];
|
|
|
|
}
|
|
|
|
elseif ($Key == 'Mode')
|
|
|
|
{
|
|
|
|
return $this -> Mode;
|
|
|
|
}
|
|
|
|
return array();
|
|
|
|
}
|
|
|
|
|
2016-06-13 10:18:47 +02:00
|
|
|
/**
|
|
|
|
* Magic method to check isset for the various vCard values as object members, e.g.
|
|
|
|
* a call to isset( $vCard -> fn ) checks existence of a value.
|
|
|
|
*
|
|
|
|
* @param string Key
|
|
|
|
*
|
|
|
|
* @return bool isset
|
|
|
|
*/
|
|
|
|
public function __isset($Key) {
|
|
|
|
$Key = strtolower($Key);
|
|
|
|
$val = $this->$Key;
|
|
|
|
return isset($val);
|
|
|
|
}
|
|
|
|
|
2013-05-20 23:09:51 +02:00
|
|
|
/**
|
|
|
|
* Saves an embedded file
|
|
|
|
*
|
|
|
|
* @param string Key
|
|
|
|
* @param int Index of the file, defaults to 0
|
|
|
|
* @param string Target path where the file should be saved, including the filename
|
|
|
|
*
|
|
|
|
* @return bool Operation status
|
|
|
|
*/
|
|
|
|
public function SaveFile($Key, $Index = 0, $TargetPath = '')
|
|
|
|
{
|
|
|
|
if (!isset($this -> Data[$Key]))
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (!isset($this -> Data[$Key][$Index]))
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returing false if it is an image URL
|
2016-06-13 10:18:47 +02:00
|
|
|
if (stripos($this -> Data[$Key][$Index]['value'], 'uri:') === 0)
|
2013-05-20 23:09:51 +02:00
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (is_writable($TargetPath) || (!file_exists($TargetPath) && is_writable(dirname($TargetPath))))
|
|
|
|
{
|
2016-06-13 10:18:47 +02:00
|
|
|
$RawContent = $this -> Data[$Key][$Index]['value'];
|
|
|
|
if (isset($this -> Data[$Key][$Index]['encoding']) && $this -> Data[$Key][$Index]['encoding'] == 'b')
|
2013-05-20 23:09:51 +02:00
|
|
|
{
|
|
|
|
$RawContent = base64_decode($RawContent);
|
|
|
|
}
|
|
|
|
$Status = file_put_contents($TargetPath, $RawContent);
|
|
|
|
return (bool)$Status;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
throw new Exception('vCard: Cannot save file ('.$Key.'), target path not writable ('.$TargetPath.')');
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Magic method for adding data to the vCard
|
|
|
|
*
|
|
|
|
* @param string Key
|
|
|
|
* @param string Method call arguments. First element is value.
|
|
|
|
*
|
|
|
|
* @return vCard Current object for method chaining
|
|
|
|
*/
|
|
|
|
public function __call($Key, $Arguments)
|
|
|
|
{
|
|
|
|
$Key = strtolower($Key);
|
|
|
|
|
|
|
|
if (!isset($this -> Data[$Key]))
|
|
|
|
{
|
|
|
|
$this -> Data[$Key] = array();
|
|
|
|
}
|
|
|
|
|
|
|
|
$Value = isset($Arguments[0]) ? $Arguments[0] : false;
|
|
|
|
|
|
|
|
if (count($Arguments) > 1)
|
|
|
|
{
|
2016-06-13 10:18:47 +02:00
|
|
|
$Types = array_map('strtolower', array_values(array_slice($Arguments, 1)));
|
2013-05-20 23:09:51 +02:00
|
|
|
|
|
|
|
if (isset(self::$Spec_StructuredElements[$Key]) &&
|
2016-06-13 10:18:47 +02:00
|
|
|
in_array(strtolower($Arguments[1]), self::$Spec_StructuredElements[$Key])
|
2013-05-20 23:09:51 +02:00
|
|
|
)
|
|
|
|
{
|
|
|
|
$LastElementIndex = 0;
|
|
|
|
|
|
|
|
if (count($this -> Data[$Key]))
|
|
|
|
{
|
|
|
|
$LastElementIndex = count($this -> Data[$Key]) - 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isset($this -> Data[$Key][$LastElementIndex]))
|
|
|
|
{
|
|
|
|
if (empty($this -> Data[$Key][$LastElementIndex][$Types[0]]))
|
|
|
|
{
|
|
|
|
$this -> Data[$Key][$LastElementIndex][$Types[0]] = $Value;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
$LastElementIndex++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isset($this -> Data[$Key][$LastElementIndex]))
|
|
|
|
{
|
|
|
|
$this -> Data[$Key][$LastElementIndex] = array(
|
|
|
|
$Types[0] => $Value
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elseif (isset(self::$Spec_ElementTypes[$Key]))
|
|
|
|
{
|
|
|
|
$this -> Data[$Key][] = array(
|
2016-06-13 10:18:47 +02:00
|
|
|
'value' => $Value,
|
|
|
|
'type' => $Types
|
2013-05-20 23:09:51 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elseif ($Value)
|
|
|
|
{
|
|
|
|
$this -> Data[$Key][] = $Value;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Magic method for getting vCard content out
|
|
|
|
*
|
|
|
|
* @return string Raw vCard content
|
|
|
|
*/
|
|
|
|
public function __toString()
|
|
|
|
{
|
|
|
|
$Text = 'BEGIN:VCARD'.self::endl;
|
|
|
|
$Text .= 'VERSION:3.0'.self::endl;
|
|
|
|
|
|
|
|
foreach ($this -> Data as $Key => $Values)
|
|
|
|
{
|
|
|
|
$KeyUC = strtoupper($Key);
|
|
|
|
$Key = strtolower($Key);
|
|
|
|
|
|
|
|
if (in_array($KeyUC, array('PHOTO', 'VERSION')))
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($Values as $Index => $Value)
|
|
|
|
{
|
|
|
|
$Text .= $KeyUC;
|
2016-06-13 10:18:47 +02:00
|
|
|
if (is_array($Value) && isset($Value['type']))
|
2013-05-20 23:09:51 +02:00
|
|
|
{
|
2016-06-13 10:18:47 +02:00
|
|
|
$Text .= ';TYPE='.self::PrepareTypeStrForOutput($Value['type']);
|
2013-05-20 23:09:51 +02:00
|
|
|
}
|
|
|
|
$Text .= ':';
|
|
|
|
|
|
|
|
if (isset(self::$Spec_StructuredElements[$Key]))
|
|
|
|
{
|
|
|
|
$PartArray = array();
|
|
|
|
foreach (self::$Spec_StructuredElements[$Key] as $Part)
|
|
|
|
{
|
|
|
|
$PartArray[] = isset($Value[$Part]) ? $Value[$Part] : '';
|
|
|
|
}
|
|
|
|
$Text .= implode(';', $PartArray);
|
|
|
|
}
|
|
|
|
elseif (is_array($Value) && isset(self::$Spec_ElementTypes[$Key]))
|
|
|
|
{
|
2016-06-13 10:18:47 +02:00
|
|
|
$Text .= $Value['value'];
|
2013-05-20 23:09:51 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
$Text .= $Value;
|
|
|
|
}
|
|
|
|
|
|
|
|
$Text .= self::endl;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$Text .= 'END:VCARD'.self::endl;
|
|
|
|
return $Text;
|
|
|
|
}
|
|
|
|
|
|
|
|
// !Helper methods
|
|
|
|
|
|
|
|
private static function PrepareTypeStrForOutput($Type)
|
|
|
|
{
|
|
|
|
return implode(',', array_map('strtoupper', $Type));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes the escaping slashes from the text.
|
|
|
|
*
|
|
|
|
* @access private
|
|
|
|
*
|
|
|
|
* @param string Text to prepare.
|
|
|
|
*
|
|
|
|
* @return string Resulting text.
|
|
|
|
*/
|
|
|
|
private static function Unescape($Text)
|
|
|
|
{
|
|
|
|
return str_replace(array('\:', '\;', '\,', "\n"), array(':', ';', ',', ''), $Text);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Separates the various parts of a structured value according to the spec.
|
|
|
|
*
|
|
|
|
* @access private
|
|
|
|
*
|
|
|
|
* @param string Raw text string
|
|
|
|
* @param string Key (e.g., N, ADR, ORG, etc.)
|
|
|
|
*
|
|
|
|
* @return array Parts in an associative array.
|
|
|
|
*/
|
|
|
|
private static function ParseStructuredValue($Text, $Key)
|
|
|
|
{
|
|
|
|
$Text = array_map('trim', explode(';', $Text));
|
|
|
|
|
|
|
|
$Result = array();
|
|
|
|
$Ctr = 0;
|
|
|
|
|
|
|
|
foreach (self::$Spec_StructuredElements[$Key] as $Index => $StructurePart)
|
|
|
|
{
|
|
|
|
$Result[$StructurePart] = isset($Text[$Index]) ? $Text[$Index] : null;
|
|
|
|
}
|
|
|
|
return $Result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @access private
|
|
|
|
*/
|
|
|
|
private static function ParseMultipleTextValue($Text)
|
|
|
|
{
|
|
|
|
return explode(',', $Text);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @access private
|
|
|
|
*/
|
|
|
|
private static function ParseParameters($Key, array $RawParams = null)
|
|
|
|
{
|
|
|
|
if (!$RawParams)
|
|
|
|
{
|
|
|
|
return array();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parameters are split into (key, value) pairs
|
|
|
|
$Parameters = array();
|
|
|
|
foreach ($RawParams as $Item)
|
|
|
|
{
|
2016-06-13 10:18:47 +02:00
|
|
|
// try to correct issue https://github.com/nuovo/vCard-parser/issues/20
|
|
|
|
$Parameters[] = explode('=', strtolower($Item),2);
|
2013-05-20 23:09:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$Type = array();
|
|
|
|
$Result = array();
|
|
|
|
|
|
|
|
// And each parameter is checked whether anything can/should be done because of it
|
|
|
|
foreach ($Parameters as $Index => $Parameter)
|
|
|
|
{
|
|
|
|
// Skipping empty elements
|
|
|
|
if (!$Parameter)
|
|
|
|
{
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handling type parameters without the explicit TYPE parameter name (2.1 valid)
|
|
|
|
if (count($Parameter) == 1)
|
|
|
|
{
|
|
|
|
// Checks if the type value is allowed for the specific element
|
|
|
|
// The second part of the "if" statement means that email elements can have non-standard types (see the spec)
|
|
|
|
if (
|
|
|
|
(isset(self::$Spec_ElementTypes[$Key]) && in_array($Parameter[0], self::$Spec_ElementTypes[$Key])) ||
|
|
|
|
($Key == 'email' && is_scalar($Parameter[0]))
|
|
|
|
)
|
|
|
|
{
|
|
|
|
$Type[] = $Parameter[0];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elseif (count($Parameter) > 2)
|
|
|
|
{
|
2016-06-13 10:18:47 +02:00
|
|
|
if(count(explode(',', $RawParams[$Index], -1)) > 0)
|
2013-05-20 23:09:51 +02:00
|
|
|
{
|
2016-06-13 10:18:47 +02:00
|
|
|
$TempTypeParams = self::ParseParameters($Key, explode(',', $RawParams[$Index]));
|
|
|
|
if ($TempTypeParams['type'])
|
|
|
|
{
|
|
|
|
$Type = array_merge($Type, $TempTypeParams['type']);
|
|
|
|
}
|
2013-05-20 23:09:51 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
switch ($Parameter[0])
|
|
|
|
{
|
|
|
|
case 'encoding':
|
|
|
|
if (in_array($Parameter[1], array('quoted-printable', 'b', 'base64')))
|
|
|
|
{
|
|
|
|
$Result['encoding'] = $Parameter[1] == 'base64' ? 'b' : $Parameter[1];
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case 'charset':
|
|
|
|
$Result['charset'] = $Parameter[1];
|
|
|
|
break;
|
|
|
|
case 'type':
|
|
|
|
$Type = array_merge($Type, explode(',', $Parameter[1]));
|
|
|
|
break;
|
|
|
|
case 'value':
|
|
|
|
if (strtolower($Parameter[1]) == 'url')
|
|
|
|
{
|
|
|
|
$Result['encoding'] = 'uri';
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$Result['type'] = $Type;
|
|
|
|
|
|
|
|
return $Result;
|
|
|
|
}
|
|
|
|
|
|
|
|
// !Interface methods
|
|
|
|
|
|
|
|
// Countable interface
|
|
|
|
public function count()
|
|
|
|
{
|
|
|
|
switch ($this -> Mode)
|
|
|
|
{
|
|
|
|
case self::MODE_ERROR:
|
|
|
|
return 0;
|
|
|
|
break;
|
|
|
|
case self::MODE_SINGLE:
|
|
|
|
return 1;
|
|
|
|
break;
|
|
|
|
case self::MODE_MULTIPLE:
|
|
|
|
return count($this -> Data);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Iterator interface
|
|
|
|
public function rewind()
|
|
|
|
{
|
|
|
|
reset($this -> Data);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function current()
|
|
|
|
{
|
|
|
|
return current($this -> Data);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function next()
|
|
|
|
{
|
|
|
|
return next($this -> Data);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function valid()
|
|
|
|
{
|
|
|
|
return ($this -> current() !== false);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function key()
|
|
|
|
{
|
|
|
|
return key($this -> Data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
?>
|