using new vcard parser

This commit is contained in:
Christoph Haas 2016-06-13 13:41:52 +02:00
parent 2118320a5d
commit 7fd75b3efd
9 changed files with 191 additions and 885 deletions

18
.idea/contactimporter.iml Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module-library">
<library name="PHARS">
<CLASSES>
<root url="phar://$MODULE_DIR$/php/composer.phar" />
</CLASSES>
<SOURCES>
<root url="phar://$MODULE_DIR$/php/composer.phar" />
</SOURCES>
</library>
</orderEntry>
</component>
</module>

View File

@ -135,6 +135,7 @@
<foreach target="syntax-check" param="file">
<path>
<fileset dir=".">
<exclude name="php/vendor/**" />
<include name="**/*.php"/>
</fileset>
</path>

View File

@ -94,15 +94,6 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, {
this.parseContacts(this.vcffile);
}
},
close: function (cmp) {
Ext.getCmp("importcontactsbutton").enable();
},
hide: function (cmp) {
Ext.getCmp("importcontactsbutton").enable();
},
destroy: function (cmp) {
Ext.getCmp("importcontactsbutton").enable();
},
scope: this
}
});
@ -486,7 +477,6 @@ Zarafa.plugins.contactimporter.dialogs.ImportPanel = Ext.extend(Ext.Panel, {
},
importContactsDone : function (response) {
console.log(response);
this.loadMask.hide();
this.dialog.close();
if(response.status == true) {

View File

@ -83,16 +83,16 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, {
createAttachmentImportButton : function(include, btn) {
return {
text : _('Import Contacts'),
handler : this.getAttachmentFileName.createDelegate(this, [btn, this.gotAttachmentFileName]),
handler : this.getAttachmentFileName.createDelegate(this, [btn]),
scope : this,
iconCls : 'icon_contactimporter_button',
beforeShow : function(item, record) {
var extension = record.data.name.split('.').pop().toLowerCase();
if(record.data.filetype == "text/vcard" || extension == "vcf" || extension == "vcard") {
item.setVisible(false);
} else {
if(record.data.filetype == "text/vcard" || record.data.filetype == "text/x-vcard" || extension == "vcf" || extension == "vcard") {
item.setVisible(true);
} else {
item.setVisible(false);
}
}
};
@ -117,7 +117,7 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, {
/**
* Clickhandler for the button
*/
getAttachmentFileName: function (btn, callback) {
getAttachmentFileName: function (btn) {
Zarafa.common.dialogs.MessageBox.show({
title: 'Please wait',
msg: 'Loading attachment...',
@ -157,7 +157,8 @@ Zarafa.plugins.contactimporter.ImportPlugin = Ext.extend(Zarafa.core.Plugin, {
var filename = attachmentRecord.data.name;
var responseHandler = new Zarafa.plugins.contactimporter.data.ResponseHandler({
successCallback: callback
successCallback: this.gotAttachmentFileName.createDelegate(this),
scope: this
});
// request attachment preperation

View File

@ -20,7 +20,7 @@
<serverfile type="module" module="contactmodule">php/module.contact.php</serverfile>
</server>
<client>
<clientfile load="release">js/contactimporter.js</clientfile>
<clientfile load="release">js/contactimporter-debug.js</clientfile>
<clientfile load="debug">js/contactimporter-debug.js</clientfile>
<clientfile load="source">js/plugin.contactimporter.js</clientfile>
@ -30,7 +30,7 @@
<clientfile load="source">js/dialogs/ImportPanel.js</clientfile>
</client>
<resources>
<resourcefile load="release">resources/css/contactimporter-min.css</resourcefile>
<resourcefile load="release">resources/css/contactimporter.css</resourcefile>
<resourcefile load="debug">resources/css/contactimporter.css</resourcefile>
<resourcefile load="source">resources/css/contactimporter-main.css</resourcefile>
</resources>

5
php/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
composer.phar
.composer.lock
composer.lock
vendor
vendor/*

5
php/composer.json Normal file
View File

@ -0,0 +1,5 @@
{
"require": {
"jeroendesloovere/vcard": "1.2.*"
}
}

View File

@ -21,8 +21,10 @@
*
*/
include_once('vcf/class.vCard.php');
require_once('mapi/mapitags.php' );
include_once('vendor/autoload.php');
use JeroenDesloovere\VCard\VCard;
use JeroenDesloovere\VCard\VCardParser;
class ContactModule extends Module {
@ -142,8 +144,9 @@ class ContactModule extends Module {
$error_msg = "";
// parse the vcf file a last time...
$parser = null;
try {
$vcard = new vCard($vcffile, false, array('Collapse' => false)); // Parse it!
$parser = VCardParser::parseFromFile($vcffile);
} catch (Exception $e) {
$error = true;
$error_msg = $e->getMessage();
@ -151,13 +154,8 @@ class ContactModule extends Module {
$contacts = array();
if(!$error && count($vcard) > 0) {
$vCard = $vcard;
if (count($vCard) == 1) {
$vCard = array($vcard);
}
$contacts = $this->parseContactsToArray($vCard);
if(!$error && iterator_count($parser) > 0) {
$contacts = $this->parseContactsToArray($parser);
$store = $GLOBALS["mapisession"]->openMessageStore(hex2bin($storeid));
$folder = mapi_msgstore_openentry($store, hex2bin($folderid));
@ -450,8 +448,10 @@ class ContactModule extends Module {
$error_msg = "";
if(is_readable ($actionData["vcf_filepath"])) {
$parser = null;
try {
$vcard = new vCard($actionData["vcf_filepath"], false, array('Collapse' => false)); // Parse it!
$parser = VCardParser::parseFromFile($actionData["vcf_filepath"]);
} catch (Exception $e) {
$error = true;
$error_msg = $e->getMessage();
@ -460,19 +460,14 @@ class ContactModule extends Module {
$response['status'] = false;
$response['message']= $error_msg;
} else {
if(count($vcard) == 0) {
if(iterator_count($parser) == 0) {
$response['status'] = false;
$response['message']= "No contacts in vcf file";
} else {
$vCard = $vcard;
if (count($vCard) == 1) {
$vCard = array($vcard);
}
$response['status'] = true;
$response['parsed_file']= $actionData["vcf_filepath"];
$response['parsed'] = array (
'contacts' => $this->parseContactsToArray($vCard)
'contacts' => $this->parseContactsToArray($parser)
);
}
}
@ -503,141 +498,156 @@ class ContactModule extends Module {
if(!$csv) {
foreach ($contacts as $Index => $vCard) {
$properties = array();
$properties["display_name"] = $vCard -> FN[0];
$properties["fileas"] = $vCard -> FN[0];
if (isset($vCard->fullname)) {
$properties["display_name"] = $vCard->fullname;
$properties["fileas"] = $vCard->fullname;
} elseif(!isset($vCard->organization)) {
error_log("Skipping entry! No fullname/organization given.");
continue;
}
//uid - used for front/backend communication
$properties["internal_fields"] = array();
$properties["internal_fields"]["contact_uid"] = base64_encode($Index . $properties["fileas"]);
foreach ($vCard -> N as $Name) {
$properties["given_name"] = $Name['FirstName'];
$properties["middle_name"] = $Name['AdditionalNames'];
$properties["surname"] = $Name['LastName'];
$properties["display_name_prefix"] = $Name['Prefixes'];
}
if ($vCard -> TEL) {
foreach ($vCard -> TEL as $Tel) {
if(!is_scalar($Tel)) {
if(in_array("home", $Tel['Type'])) {
$properties["home_telephone_number"] = $Tel['Value'];
} else if(in_array("cell", $Tel['Type'])) {
$properties["cellular_telephone_number"] = $Tel['Value'];
} else if(in_array("work", $Tel['Type'])) {
$properties["business_telephone_number"] = $Tel['Value'];
} else if(in_array("fax", $Tel['Type'])) {
$properties["business_fax_number"] = $Tel['Value'];
} else if(in_array("pager", $Tel['Type'])) {
$properties["pager_telephone_number"] = $Tel['Value'];
} else if(in_array("isdn", $Tel['Type'])) {
$properties["isdn_number"] = $Tel['Value'];
} else if(in_array("car", $Tel['Type'])) {
$properties["car_telephone_number"] = $Tel['Value'];
} else if(in_array("modem", $Tel['Type'])) {
$properties["ttytdd_telephone_number"] = $Tel['Value'];
}
$properties["given_name"] = $vCard->firstname;
$properties["middle_name"] = $vCard->additional;
$properties["surname"] = $vCard->lastname;
$properties["display_name_prefix"] = $vCard->prefix;
if (isset($vCard->phone) && count($vCard->phone) > 0) {
foreach ($vCard->phone as $type => $number) {
$number = $number[0]; // we only can store one number
if($this->startswith(strtolower($type), "home") || strtolower($type) === "default") {
$properties["home_telephone_number"] = $number;
} else if($this->startswith(strtolower($type), "cell")) {
$properties["cellular_telephone_number"] = $number;
} else if($this->startswith(strtolower($type), "work")) {
$properties["business_telephone_number"] = $number;
} else if($this->startswith(strtolower($type), "fax")) {
$properties["business_fax_number"] = $number;
} else if($this->startswith(strtolower($type), "pager")) {
$properties["pager_telephone_number"] = $number;
} else if($this->startswith(strtolower($type), "isdn")) {
$properties["isdn_number"] = $number;
} else if($this->startswith(strtolower($type), "car")) {
$properties["car_telephone_number"] = $number;
} else if($this->startswith(strtolower($type), "modem")) {
$properties["ttytdd_telephone_number"] = $number;
}
}
}
if ($vCard -> EMAIL) {
$e=0;
foreach ($vCard -> EMAIL as $Email) {
$fileas = $Email['Value'];
if (isset($vCard->email) && count($vCard->email) > 0) {
$emailcount = 0;
foreach ($vCard->email as $type => $email) {
$email = $email[0]; // we only can store one mail address
$fileas = $email;
if(isset($properties["fileas"]) && !empty($properties["fileas"])) {
$fileas = $properties["fileas"];
$fileas = $properties["fileas"]; // set to real name
}
if(!is_scalar($Email)) {
switch($e) {
case 0:
$properties["email_address_1"] = $Email['Value'];
$properties["email_address_display_name_1"] = $fileas . " (" . $Email['Value'] . ")";
break;
case 1:
$properties["email_address_2"] = $Email['Value'];
$properties["email_address_display_name_2"] = $fileas . " (" . $Email['Value'] . ")";
break;
case 2:
$properties["email_address_3"] = $Email['Value'];
$properties["email_address_display_name_3"] = $fileas . " (" . $Email['Value'] . ")";
break;
default: break;
// we only have storage for 3 mail addresses!
switch($emailcount) {
case 0:
$properties["email_address_1"] = $email;
$properties["email_address_display_name_1"] = $fileas . " (" . $email . ")";
break;
case 1:
$properties["email_address_2"] = $email;
$properties["email_address_display_name_2"] = $fileas . " (" . $email . ")";
break;
case 2:
$properties["email_address_3"] = $email;
$properties["email_address_display_name_3"] = $fileas . " (" . $email . ")";
break;
default: break;
}
$emailcount++;
}
}
if (isset($vCard->organization)) {
$properties["company_name"] = $vCard->organization;
if(empty($properties["display_name"])) {
$properties["display_name"] = $vCard->organization; // if we have no displayname - use the company name as displayname
$properties["fileas"] = $vCard->organization;
}
}
if (isset($vCard->title)) {
$properties["title"] = $vCard->title;
}
if (isset($vCard->url) && count($vCard->url) > 0) {
foreach ($vCard->url as $type => $url) {
$url = $url[0]; // only 1 webaddress per type
$properties["webpage"] = $url;
break; // we can only store on url
}
}
if (isset($vCard->address) && count($vCard->address) > 0) {
foreach ($vCard->address as $type => $address) {
$address = $address[0]; // we only can store one address per type
if($this->startswith(strtolower($type), "work")) {
$properties["business_address_street"] = $address->street;
if(!empty($address->extended)) {
$properties["business_address_street"] .= "\n" . $address->extended;
}
$e++;
$properties["business_address_city"] = $address->city;
$properties["business_address_state"] = $address->region;
$properties["business_address_postal_code"] = $address->zip;
$properties["business_address_country"] = $address->country;
$properties["business_address"] = $this->buildAddressString($properties["business_address_street"], $address->zip, $address->city, $address->region, $address->country);
} else if($this->startswith(strtolower($type), "home")) {
$properties["home_address_street"] = $address->street;
if(!empty($address->extended)) {
$properties["home_address_street"] .= "\n" . $address->extended;
}
$properties["home_address_city"] = $address->city;
$properties["home_address_state"] = $address->region;
$properties["home_address_postal_code"] = $address->zip;
$properties["home_address_country"] = $address->country;
$properties["home_address"] = $this->buildAddressString($properties["home_address_street"], $address->zip, $address->city, $address->region, $address->country);
} else {
$properties["other_address_street"] = $address->street;
if(!empty($address->extended)) {
$properties["other_address_street"] .= "\n" . $address->extended;
}
$properties["other_address_city"] = $address->city;
$properties["other_address_state"] = $address->region;
$properties["other_address_postal_code"] = $address->zip;
$properties["other_address_country"] = $address->country;
$properties["other_address"] = $this->buildAddressString($properties["other_address_street"], $address->zip, $address->city, $address->region, $address->country);
}
}
}
if ($vCard -> ORG) {
foreach ($vCard -> ORG as $Organization) {
$properties["company_name"] = $Organization['Name'];
if(empty($properties["display_name"])) {
$properties["display_name"] = $Organization['Name']; // if we have no displayname - use the company name as displayname
$properties["fileas"] = $Organization['Name'];
}
}
if (isset($vCard->birthday)) {
$properties["birthday"] = $vCard->birthday->getTimestamp();
}
if ($vCard -> TITLE) {
$title = $vCard -> TITLE[0];
$properties["title"] = is_array($title) ? $title["Value"] : $title;
if (isset($vCard->note)) {
$properties["notes"] = $vCard->note;
}
if ($vCard -> URL) {
$url = $vCard -> URL[0]; // only 1 webaddress
$properties["webpage"] = is_array($url) ? $url["Value"] : $url;
}
if ($vCard -> IMPP) {
foreach ($vCard -> IMPP as $IMPP) {
if (!is_scalar($IMPP)) {
$properties["im"] = $IMPP['Value'];
}
}
}
if ($vCard -> ADR) {
foreach ($vCard -> ADR as $Address) {
if(in_array("work", $Address['Type'])) {
$properties["business_address_street"] = $Address['StreetAddress'];
$properties["business_address_city"] = $Address['Locality'];
$properties["business_address_state"] = $Address['Region'];
$properties["business_address_postal_code"] = $Address['PostalCode'];
$properties["business_address_country"] = $Address['Country'];
$properties["business_address"] = $this->buildAddressString($Address['StreetAddress'], $Address['PostalCode'], $Address['Locality'], $Address['Region'], $Address['Country']);
} else if(in_array("home", $Address['Type'])) {
$properties["home_address_street"] = $Address['StreetAddress'];
$properties["home_address_city"] = $Address['Locality'];
$properties["home_address_state"] = $Address['Region'];
$properties["home_address_postal_code"] = $Address['PostalCode'];
$properties["home_address_country"] = $Address['Country'];
$properties["home_address"] = $this->buildAddressString($Address['StreetAddress'], $Address['PostalCode'], $Address['Locality'], $Address['Region'], $Address['Country']);
} else if(in_array("postal", $Address['Type'])||in_array("parcel", $Address['Type'])||in_array("intl", $Address['Type'])||in_array("dom", $Address['Type'])) {
$properties["other_address_street"] = $Address['StreetAddress'];
$properties["other_address_city"] = $Address['Locality'];
$properties["other_address_state"] = $Address['Region'];
$properties["other_address_postal_code"] = $Address['PostalCode'];
$properties["other_address_country"] = $Address['Country'];
$properties["other_address"] = $this->buildAddressString($Address['StreetAddress'], $Address['PostalCode'], $Address['Locality'], $Address['Region'], $Address['Country']);
}
}
}
if ($vCard -> BDAY) {
$properties["birthday"] = strtotime($vCard -> BDAY[0]);
}
if ($vCard -> NOTE) {
$properties["notes"] = $vCard -> NOTE[0];
}
if ($vCard -> PHOTO) {
if (isset($vCard->rawPhoto) || isset($vCard->photo)) {
if(!is_writable(TMP_PATH . "/")) {
error_log("could not write to export tmp directory!: " . $E);
error_log("Can not write to export tmp directory!");
} else {
$tmppath = TMP_PATH . "/" . $this->randomstring(15);
try {
if($vCard -> SaveFile('photo', 0, $tmppath)) {
if(isset($vCard->rawPhoto)) {
if(file_put_contents($tmppath, $vCard->rawPhoto)) {
$properties["internal_fields"]["x_photo_path"] = $tmppath;
} else {
if($this->DEBUG) {
error_log("remote imagefetching not implemented");
}
}
} catch (Exception $E) {
error_log("Image exception: " . $E);
} elseif(isset($vCard->photo)) {
if($this->startswith(strtolower($vCard->photo), "http://") || $this->startswith(strtolower($vCard->photo), "https://")) { // check if it starts with http
$ctx = stream_context_create(array('http'=>
array(
'timeout' => 3, //3 Seconds timout
)
));
if(file_put_contents($tmppath, file_get_contents($vCard->photo, false, $ctx))) {
$properties["internal_fields"]["x_photo_path"] = $tmppath;
}
} else {
error_log("Invalid photo url: " . $vCard->photo);
}
}
}
}
@ -815,6 +825,11 @@ class ContactModule extends Module {
$GLOBALS["bus"]->addData($this->getResponseData());
}
}
private function startswith($haystack, $needle) {
$haystack = str_replace("type=", "", $haystack); // remove type from string
return substr($haystack, 0, strlen($needle)) === $needle;
}
};
?>

View File

@ -1,729 +0,0 @@
<?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
* @version 0.4.9
*/
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(
'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')
);
private static $Spec_MultipleValueElements = array('nickname', 'categories');
private static $Spec_ElementTypes = array(
'email' => array('internet', 'x400', 'pref', 'home', 'work'),
'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
// 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);
// 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)
{
//Cannot use "explode", because we need to ignore, for example, 'AGENT:BEGIN:VCARD'
$this -> RawData = preg_split('{^BEGIN\:VCARD}miS', $this -> RawData);
$this -> RawData = array_filter($this -> RawData);
foreach ($this -> RawData as $SinglevCardRawData)
{
// mca: remove \n and \r at start
//$SinglevCardRawData=ltrim($SinglevCardRawData);
// 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.
$SinglevCardRawData = 'BEGIN:VCARD'.$SinglevCardRawData;
$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)
$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);
// Joining multiple lines that are split with a soft wrap (space or tab on the beginning of the next line
$this -> RawData = str_replace(array("\r\n ", "\r\n\t"), '-wrap-', $this -> RawData);
// Restoring the BASE64 final equals sign (see a few lines above)
$this -> RawData = str_replace("-base64=-\r\n", "=\r\n", $this -> RawData);
$Lines = explode("\r\n", $this -> RawData);
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]);
}
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)
{
$Value['type'] = $Type;
}
}
else
{
if (in_array($Key, self::$Spec_MultipleValueElements))
{
$Value = self::ParseMultipleTextValue($Value, $Key);
}
if ($Type)
{
$Value = array(
'value' => $Value,
'type' => $Type
);
}
}
if (is_array($Value) && $Encoding)
{
$Value['encoding'] = $Encoding;
}
if (!isset($this -> Data[$Key]))
{
$this -> Data[$Key] = array();
}
$this -> Data[$Key][] = $Value;
}
}
}
/**
* 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;
}
/**
* 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)
{
if (isset($V['Value']) && stripos($V['Value'], 'uri:') === 0)
{
$Value[$K]['value'] = substr($V, 4);
$Value[$K]['encoding'] = 'uri';
}
}
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();
}
/**
* 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);
}
/**
* 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
if (stripos($this -> Data[$Key][$Index]['value'], 'uri:') === 0)
{
return false;
}
if (is_writable($TargetPath) || (!file_exists($TargetPath) && is_writable(dirname($TargetPath))))
{
$RawContent = $this -> Data[$Key][$Index]['value'];
if (isset($this -> Data[$Key][$Index]['encoding']) && $this -> Data[$Key][$Index]['encoding'] == 'b')
{
$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)
{
$Types = array_map('strtolower', array_values(array_slice($Arguments, 1)));
if (isset(self::$Spec_StructuredElements[$Key]) &&
in_array(strtolower($Arguments[1]), self::$Spec_StructuredElements[$Key])
)
{
$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(
'value' => $Value,
'type' => $Types
);
}
}
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;
if (is_array($Value) && isset($Value['type']))
{
$Text .= ';TYPE='.self::PrepareTypeStrForOutput($Value['type']);
}
$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]))
{
$Text .= $Value['value'];
}
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)
{
// try to correct issue https://github.com/nuovo/vCard-parser/issues/20
$Parameters[] = explode('=', strtolower($Item),2);
}
$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)
{
if(count(explode(',', $RawParams[$Index], -1)) > 0)
{
$TempTypeParams = self::ParseParameters($Key, explode(',', $RawParams[$Index]));
if ($TempTypeParams['type'])
{
$Type = array_merge($Type, $TempTypeParams['type']);
}
}
}
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);
}
}
?>