diff --git a/.idea/contactimporter.iml b/.idea/contactimporter.iml new file mode 100644 index 0000000..0c76797 --- /dev/null +++ b/.idea/contactimporter.iml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.xml b/build.xml index c47e16b..dba3f1c 100644 --- a/build.xml +++ b/build.xml @@ -135,6 +135,7 @@ + diff --git a/js/dialogs/ImportPanel.js b/js/dialogs/ImportPanel.js index 0840159..44db926 100644 --- a/js/dialogs/ImportPanel.js +++ b/js/dialogs/ImportPanel.js @@ -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) { diff --git a/js/plugin.contactimporter.js b/js/plugin.contactimporter.js index c36a696..f4588d5 100644 --- a/js/plugin.contactimporter.js +++ b/js/plugin.contactimporter.js @@ -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 diff --git a/manifest.xml b/manifest.xml index acafc6f..e01248d 100644 --- a/manifest.xml +++ b/manifest.xml @@ -20,7 +20,7 @@ php/module.contact.php - js/contactimporter.js + js/contactimporter-debug.js js/contactimporter-debug.js js/plugin.contactimporter.js @@ -30,7 +30,7 @@ js/dialogs/ImportPanel.js - resources/css/contactimporter-min.css + resources/css/contactimporter.css resources/css/contactimporter.css resources/css/contactimporter-main.css diff --git a/php/.gitignore b/php/.gitignore new file mode 100644 index 0000000..3d60449 --- /dev/null +++ b/php/.gitignore @@ -0,0 +1,5 @@ +composer.phar +.composer.lock +composer.lock +vendor +vendor/* diff --git a/php/composer.json b/php/composer.json new file mode 100644 index 0000000..303bfe5 --- /dev/null +++ b/php/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "jeroendesloovere/vcard": "1.2.*" + } +} diff --git a/php/module.contact.php b/php/module.contact.php index d7eefd3..fd7e5d0 100644 --- a/php/module.contact.php +++ b/php/module.contact.php @@ -21,9 +21,11 @@ * */ -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 { private $DEBUG = false; // enable error_log debugging @@ -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)); @@ -172,7 +170,7 @@ class ContactModule extends Module { $count = 0; // iterate through all contacts and import them :) - foreach($contacts as $contact) { + foreach($contacts as $contact) { if (isset($contact["display_name"]) && ($importall || in_array($contact["internal_fields"]["contact_uid"], $uids))) { // parse the arraykeys // TODO: this is very slow... @@ -194,7 +192,7 @@ class ContactModule extends Module { $contactPicture = file_get_contents($contact["internal_fields"]["x_photo_path"]); $attach = mapi_message_createattach($message); - // Set properties of the attachment + // Set properties of the attachment $propValuesIMG = array( PR_ATTACH_SIZE => strlen($contactPicture), PR_ATTACH_LONG_FILENAME => 'ContactPicture.jpg', @@ -224,7 +222,7 @@ class ContactModule extends Module { mapi_savechanges($message); if($this->DEBUG) { error_log("New contact added: \"" . $propValuesMAPI[$properties["display_name"]] . "\".\n"); - } + } $count++; } } @@ -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)) { - $properties["internal_fields"]["x_photo_path"] = $tmppath; - } else { - if($this->DEBUG) { - error_log("remote imagefetching not implemented"); - } + if(isset($vCard->rawPhoto)) { + if(file_put_contents($tmppath, $vCard->rawPhoto)) { + $properties["internal_fields"]["x_photo_path"] = $tmppath; + } + } 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); } - } catch (Exception $E) { - error_log("Image exception: " . $E); } } } @@ -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; + } }; ?> diff --git a/php/vcf/class.vCard.php b/php/vcf/class.vCard.php deleted file mode 100644 index bed6891..0000000 --- a/php/vcf/class.vCard.php +++ /dev/null @@ -1,729 +0,0 @@ - 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); - } -} -?> \ No newline at end of file