From d333605510fcf8151ec4598e5dbceb302f5e44d9 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Wed, 15 Jul 2009 19:44:09 +0000 Subject: [PATCH] - merged SyncML-1.2 branch with trunk: svn merge ^/trunk/addressbook@27378 ^/branches/SyncML-1.2/addressbook . - re-added to trunk commits, which were somehow not in SyncML-1.2 branch: svn merge -c 26581 ^/trunk/addressbook svn merge -c 26582 ^/trunk/addressbook --- addressbook/inc/class.addressbook_bo.inc.php | 275 +++++- .../inc/class.addressbook_groupdav.inc.php | 4 +- addressbook/inc/class.addressbook_sif.inc.php | 73 +- addressbook/inc/class.addressbook_sql.inc.php | 24 +- .../inc/class.addressbook_vcal.inc.php | 912 +++++++++++++----- 5 files changed, 987 insertions(+), 301 deletions(-) diff --git a/addressbook/inc/class.addressbook_bo.inc.php b/addressbook/inc/class.addressbook_bo.inc.php index ed72bbc640..33f067f65d 100755 --- a/addressbook/inc/class.addressbook_bo.inc.php +++ b/addressbook/inc/class.addressbook_bo.inc.php @@ -5,6 +5,7 @@ * @link http://www.egroupware.org * @author Cornelius Weiss * @author Ralf Becker + * @author Joerg Lehrke * @package addressbook * @copyright (c) 2005-8 by Ralf Becker * @copyright (c) 2005/6 by Cornelius Weiss @@ -441,8 +442,13 @@ class addressbook_bo extends addressbook_so */ function fullname($contact) { + if (empty($contact['n_family']) && empty($contact['n_given'])) { + $cpart = array('org_name'); + } else { + $cpart = array('n_prefix','n_given','n_middle','n_family','n_suffix'); + } $parts = array(); - foreach(array('n_prefix','n_given','n_middle','n_family','n_suffix') as $n) + foreach($cpart as $n) { if ($contact[$n]) $parts[] = $contact[$n]; } @@ -611,8 +617,7 @@ class addressbook_bo extends addressbook_so $contact['modifier'] = $this->user; $contact['modified'] = $this->now_su; // set full name and fileas from the content - if (isset($contact['n_family']) && isset($contact['n_given'])) - { + if (!isset($contact['n_fn'])) { $contact['n_fn'] = $this->fullname($contact); if (isset($contact['org_name'])) $contact['n_fileas'] = $this->fileas($contact); } @@ -1432,7 +1437,7 @@ class addressbook_bo extends addressbook_so { $cat_name = substr($cat_name, 2); } - $cat_id = $this->categories->add(array('name' => $cat_name,'descr' => $cat_name)); + $cat_id = $this->categories->add(array('name' => $cat_name, 'descr' => $cat_name, 'access' => 'private')); } if ($cat_id) @@ -1474,15 +1479,273 @@ class addressbook_bo extends addressbook_so function fixup_contact(&$contact) { - if (!isset($contact['n_fn']) || empty($contact['n_fn'])) + if (empty($contact['n_fn'])) { $contact['n_fn'] = $this->fullname($contact); } - if (!isset($contact['n_fileas']) || empty($contact['n_fileas'])) + if (empty($contact['n_fileas'])) { $contact['n_fileas'] = $this->fileas($contact); } } + function all_empty(&$_contact, &$fields) + { + $retval = true; + foreach ($fields as $field) { + if (isset($_contact[$field]) && !empty($_contact[$field])) { + $retval = false; + break; + } + } + return $retval; + } + + + /** + * Try to find a matching db entry + * + * @param array $contact the contact data we try to find + * @param boolean $relax=false if asked to relax, we only match against some key fields + * @return the contact_id of the matching entry or false (if none matches) + */ + function find_contact($contact, $relax=false) + { + if ($contact['id'] && ($found = $this->read($contact['id']))) + { + // We only do a simple consistency check + if ((empty($found['n_family']) || $found['n_family'] == $contact['n_family']) + && (empty($found['n_given']) || $found['n_given'] == $contact['n_given']) + && (empty($found['org_name']) || $found['org_name'] == $contact['org_name'])) + { + return $found['id']; + } + } + unset($contact['id']); + + $columns_to_search = array('contact_id', + 'n_family', 'n_given', 'n_middle', 'n_prefix', 'n_suffix', + 'bday', 'org_name', 'org_unit', 'title', 'role', + 'email', 'email_home', 'url', 'url_home'); + $tolerance_fields = array('contact_id', + 'n_middle', 'n_prefix', 'n_suffix', + 'org_unit', 'role', + 'bday', + 'email', 'email_home', + 'url', 'url_home'); + $addr_one_fields = array('adr_one_street', 'adr_one_street2', + 'adr_one_locality', 'adr_one_region', + 'adr_one_postalcode', 'adr_one_countryname'); + $addr_two_fields = array('adr_two_street', 'adr_two_street2', + 'adr_two_locality', 'adr_two_region', + 'adr_two_postalcode', 'adr_two_countryname'); + $no_addr_one = array(); + $no_addr_two = array(); + + $backend =& $this->get_backend(); + + // define filter for empty address one + foreach ($addr_one_fields as $field) + { + if (!($db_col = array_search($field, $backend->db_cols))) + { + $db_col = $field; + } + $no_addr_one[] = "(" . $db_col . " IS NULL OR " . $db_col . " = '')"; + } + + // define filter for empty address two + foreach ($addr_two_fields as $field) + { + if (!($db_col = array_search($field, $backend->db_cols))) + { + $db_col = $field; + } + $no_addr_two[] = "(" . $db_col . " IS NULL OR " . $db_col . " = '')"; + } + + $result = false; + + $criteria = array(); + $empty_columns = array(); + foreach ($columns_to_search as $field) + { + if (!isset($contact[$field]) || empty($contact[$field])) { + // Not every device supports all fields + if (!in_array($field, $tolerance_fields)) + { + if (!($db_col = array_search($field, $backend->db_cols))) + { + $db_col = $field; + } + $empty_columns[] = "(" . $db_col . " IS NULL OR " . $db_col . " = '')"; + } + } + else + { + if (!$relax || !in_array($field, $tolerance_fields)) + { + $criteria[$field] = $contact[$field]; + } + } + } + + $filter = $empty_columns; + + if (!$relax) + { + // We use addresses only for strong matching + + if ($this->all_empty($contact, $addr_one_fields)) + { + $filter = $filter + $no_addr_one; + } + else + { + foreach ($addr_one_fields as $field) + { + if (!isset($contact[$field]) || empty($contact[$field])) + { + if (!($db_col = array_search($field, $backend->db_cols))) + { + $db_col = $field; + } + $filter[] = "(" . $db_col . " IS NULL OR " . $db_col . " = '')"; + } + else + { + $criteria[$field] = $contact[$field]; + } + } + } + + if ($this->all_empty($contact, $addr_two_fields)) + { + $filter = $filter + $no_addr_two; + } + else + { + foreach ($addr_two_fields as $field) + { + if (!isset($contact[$field]) || empty($contact[$field])) + { + if (!($db_col = array_search($field, $backend->db_cols))) + { + $db_col = $field; + } + $filter[] = "(" . $db_col . " IS NULL OR " . $db_col . " = '')"; + } + else + { + $criteria[$field] = $contact[$field]; + } + } + } + } + + Horde::logMessage("Addressbook find step 1:\n" . print_r($criteria, true) . + "filter:\n" . print_r($filter, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + + // first try full match + if (($foundContacts = parent::search($criteria, true, '', '', '', False, 'AND', false, $filter))) + { + $result = $foundContacts[0]['id']; + } + + // No need for more searches for relaxed matching + if (!$relax && !$result && !$this->all_empty($contact, $addr_one_fields) + && $this->all_empty($contact, $addr_two_fields)) + { + // try given address and ignore the second one in EGW + $filter = array_diff($filter, $no_addr_two); + + Horde::logMessage("Addressbook find step 2:\n" . print_r($criteria, true) . + "filter:\n" . print_r($filter, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + if (($foundContacts = parent::search($criteria, true, '', '', '', False, 'AND', false, $filter))) + { + $result = $foundContacts[0]['id']; + } + else + { + // try address as home address -- some devices don't qualify addresses + $filter = $empty_columns; + foreach ($addr_two_fields as $key => $field) + { + if (isset($criteria[$addr_one_fields[$key]])) + { + $criteria[$field] = $criteria[$addr_one_fields[$key]]; + unset($criteria[$addr_one_fields[$key]]); + } + else + { + if (!($db_col = array_search($field,$backend->db_cols))) + { + $db_col = $field; + } + $filter[] = "(" . $db_col . " IS NULL OR " . $db_col . " = '')"; + } + } + + $filter = $filter + $no_addr_one; + + Horde::logMessage("Addressbook find step 3:\n" . print_r($criteria, true) . + "filter:\n" . print_r($filter, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + if (($foundContacts = parent::search($criteria, true, '', '', '', False, 'AND', false, $filter))) + { + $result = $foundContacts[0]['id']; + } + } + } + elseif (!$relax && !$result) + { // No more searches for relaxed matching, try again after address swap + + $filter = $empty_columns; + + foreach ($addr_one_fields as $key => $field) + { + $_temp_set = false; + if (isset($criteria[$field])) + { + $_temp = $criteria[$field]; + $_temp_set = true; + unset($criteria[$field]); + } + if (isset($criteria[$addr_two_fields[$key]])) + { + $criteria[$field] = $criteria[$addr_two_fields[$key]]; + unset($criteria[$addr_two_fields[$key]]); + } + else + { + if (!($db_col = array_search($field,$backend->db_cols))) + { + $db_col = $field; + } + $filter[] = "(" . $db_col . " IS NULL OR " . $db_col . " = '')"; + } + if ($_temp_set) + { + $criteria[$addr_two_fields[$key]] = $_temp; + } + else + { + if (!($db_col = array_search($addr_two_fields[$key],$backend->db_cols))) + { + $db_col = $field; + } + $filter[] = "(" . $db_col . " IS NULL OR " . $db_col . " = '')"; + } + } + + Horde::logMessage("Addressbook find step 4:\n" . print_r($criteria, true) . + "filter:\n" . print_r($filter, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + if(($foundContacts = parent::search($criteria, true, '', '', '', False, 'AND', false, $filter))) + { + $result = $foundContacts[0]['id']; + } + } + return $result; + } + } diff --git a/addressbook/inc/class.addressbook_groupdav.inc.php b/addressbook/inc/class.addressbook_groupdav.inc.php index 9c9275bafc..1a82d68193 100644 --- a/addressbook/inc/class.addressbook_groupdav.inc.php +++ b/addressbook/inc/class.addressbook_groupdav.inc.php @@ -310,7 +310,7 @@ class addressbook_groupdav extends groupdav_handler */ private function _get_handler() { - $handler = new addressbook_vcal(); + $handler = new addressbook_vcal('addressbook','text/vcard'); $handler->setSupportedFields('GroupDAV',$this->agent); return $handler; @@ -358,4 +358,4 @@ class addressbook_groupdav extends groupdav_handler { return $this->bo->check_perms($acl,$contact); } -} \ No newline at end of file +} diff --git a/addressbook/inc/class.addressbook_sif.inc.php b/addressbook/inc/class.addressbook_sif.inc.php index e527a242cc..158303342f 100644 --- a/addressbook/inc/class.addressbook_sif.inc.php +++ b/addressbook/inc/class.addressbook_sif.inc.php @@ -5,6 +5,7 @@ * @link http://www.egroupware.org * @author Lars Kneschke * @author Ralf Becker + * @author Joerg Lehrke * @package addressbook * @subpackage export * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License @@ -94,6 +95,10 @@ class addressbook_sif extends addressbook_bo 'Folder' => '', ); + // standard headers + const xml_decl = ''; + const SIF_decl = '1.1'; + function startElement($_parser, $_tag, $_attributes) { } @@ -108,13 +113,12 @@ class addressbook_sif extends addressbook_bo $this->sifData .= $_data; } - function siftoegw($_sifdata) { - $sifData = base64_decode($_sifdata); + function siftoegw($sifData) { #$tmpfname = tempnam('/tmp/sync/contents','sifc_'); #$handle = fopen($tmpfname, "w"); - #fwrite($handle, $sifdata); + #fwrite($handle, $sifData); #fclose($handle); $this->xml_parser = xml_parser_create('UTF-8'); @@ -131,6 +135,7 @@ class addressbook_sif extends addressbook_bo } foreach($this->contact as $key => $value) { + $value = preg_replace('/<\!\[CDATA\[(.+)\]\]>/Usim', '$1', $value); $value = $GLOBALS['egw']->translation->convert($value, 'utf-8'); switch($key) { case 'cat_id': @@ -164,25 +169,17 @@ class addressbook_sif extends addressbook_bo * @param string $_sifdata * @return boolean/int/string contact-id or false, if not found */ - function search($_sifdata,$contentID=null) + function search($_sifdata, $contentID=null, $relax=false) { - if(!$contact = $this->siftoegw($_sifdata)) - { - return false; - } - if ($contentID) { - $contact['contact_id'] = $contentID; - } - // patch from Di Guest says: we need to ignore the n_fileas - unset($contact['n_fileas']); - // we probably need to ignore even more as we do in vcaladdressbook + $result = false; - if(($foundContacts = addressbook_bo::search($contact))) - { - error_log(print_r($foundContacts,true)); - return $foundContacts[0]['id']; + if($contact = $this->siftoegw($_sifdata)) { + if ($contentID) { + $contact['contact_id'] = $contentID; + } + $result = $this->find_contact($contact, $relax); } - return false; + return $result; } /** @@ -191,8 +188,9 @@ class addressbook_sif extends addressbook_bo * @return int contact id * @param string $_vcard the vcard * @param int/string $_abID=null the internal addressbook id or !$_abID for a new enty + * @param boolean $merge=false merge data with existing entry */ - function addSIF($_sifdata, $_abID=null) + function addSIF($_sifdata, $_abID=null, $merge=false) { #error_log('ABID: '.$_abID); #error_log(base64_decode($_sifdata)); @@ -209,24 +207,25 @@ class addressbook_sif extends addressbook_bo } /** - * return a vcard + * return a sifc * * @param int $_id the id of the contact - * @param int $_vcardProfile profile id for mapping from vcard values to egw addressbook * @return string containing the vcard */ function getSIF($_id) { + $sysCharSet = $GLOBALS['egw']->translation->charset(); + $fields = array_unique(array_values($this->sifMapping)); sort($fields); - if(!($entry = $this->read($_id))) - { + if(!($entry = $this->read($_id))) { return false; } - $sifContact = ''; + + $sifContact = self::xml_decl . "\n" . self::SIF_decl; + #error_log(print_r($entry,true)); - $sysCharSet = $GLOBALS['egw']->translation->charset(); // fillup some defaults such as n_fn and n_fileas is needed $this->fixup_contact($entry); @@ -242,15 +241,6 @@ class addressbook_sif extends addressbook_bo switch($sifField) { - // TODO handle multiple categories - case 'Categories': - if(!empty($value)) { - $value = implode("; ", $this->get_categories($value)); - $value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8'); - } - $sifContact .= "<$sifField>$value"; - break; - case 'Sensitivity': $value = 2 * $value; // eGW private is 0 (public) or 1 (private) $sifContact .= "<$sifField>$value"; @@ -261,13 +251,22 @@ class addressbook_sif extends addressbook_bo #$sifContact .= "<$sifField>/"; break; + case 'Categories': + if(!empty($value)) { + $value = implode("; ", $this->get_categories($value)); + $value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8'); + } else { + break; + } + default: - $sifContact .= "<$sifField>".trim($value).""; + $value = @htmlspecialchars(trim($value), ENT_QUOTES, 'utf-8'); + $sifContact .= "<$sifField>$value"; break; } } $sifContact .= ""; - return base64_encode($sifContact); + return $sifContact; } } diff --git a/addressbook/inc/class.addressbook_sql.inc.php b/addressbook/inc/class.addressbook_sql.inc.php index 57c0d43831..f38c5ad421 100644 --- a/addressbook/inc/class.addressbook_sql.inc.php +++ b/addressbook/inc/class.addressbook_sql.inc.php @@ -607,11 +607,23 @@ class addressbook_sql extends so_sql */ function read($keys,$extra_cols='',$join='') { + if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) { + $minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']; + } else { + $minimum_uid_length = 8; + } + if (!is_array($keys) && !is_numeric($keys)) { $keys = array('contact_uid' => $keys); } - return parent::read($keys,$extra_cols,$join); + $contact = parent::read($keys,$extra_cols,$join); + // enforce a minium uid strength + if (is_array($contact) && (!isset($contact['uid']) + || strlen($contact['uid']) < $minimum_uid_length)) { + parent::update(array('uid' => common::generate_uid('addressbook',$contact['id']))); + } + return $contact; } /** @@ -623,6 +635,12 @@ class addressbook_sql extends so_sql */ function save($keys=null) { + if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) { + $minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']; + } else { + $minimum_uid_length = 8; + } + if (is_array($keys) && count($keys)) $this->data_merge($keys); $new_entry = !$this->data['id']; @@ -649,8 +667,8 @@ class addressbook_sql extends so_sql } } // enforce a minium uid strength - if (!$err && ($new_entry || isset($this->data['uid'])) && (strlen($this->data['uid']) < 20 || is_numeric($this->data['uid']))) - { + if (!$err && (!isset($this->data['uid']) + || strlen($this->data['uid']) < $minimum_uid_length)) { parent::update(array('uid' => common::generate_uid('addressbook',$this->data['id']))); //echo "

set uid={$this->data['uid']}, etag={$this->data['etag']}

"; } diff --git a/addressbook/inc/class.addressbook_vcal.inc.php b/addressbook/inc/class.addressbook_vcal.inc.php index 8712a1b7e9..113af384cb 100644 --- a/addressbook/inc/class.addressbook_vcal.inc.php +++ b/addressbook/inc/class.addressbook_vcal.inc.php @@ -5,13 +5,15 @@ * @link http://www.egroupware.org * @author Lars Kneschke * @author Ralf Becker + * @author Joerg Lehrke * @package addressbook * @subpackage export * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ -require_once EGW_SERVER_ROOT.'/phpgwapi/inc/horde/Horde/iCalendar.php'; +require_once EGW_SERVER_ROOT.'/phpgwapi/inc/horde/lib/core.php'; +require_once(EGW_SERVER_ROOT.'/phpgwapi/inc/horde/Horde/SyncML/State.php'); /** * Addressbook - vCard parser @@ -24,28 +26,80 @@ class addressbook_vcal extends addressbook_bo * * @var string */ - var $productManufacturer='file'; + var $productManufacturer = 'file'; /** * product name from setSupportedFields (lowercase!) * * @var string */ var $productName; + /** + * VCard version + * + * @var string + */ + var $version; + /** + * Client CTCap Properties + * + * @var array + */ + var $clientProperties; + + /** + * Constructor + * + * @param string $contact_app the current application + * @param string $_contentType the content type (version) + * @param array $_clientProperties client properties + */ + function __construct($contact_app='addressbook', $_contentType='text/x-vcard', &$_clientProperties = array()) + { + parent::__construct($contact_app); + #Horde::logMessage("vCalAddressbook Constructor for $_contentType", __FILE__, __LINE__, PEAR_LOG_DEBUG); + switch($_contentType) + { + case 'text/vcard': + $this->version = '3.0'; + break; + default: + $this->version = '2.1'; + break; + } + $this->clientProperties = $_clientProperties; + } /** * import a vard into addressbook * * @param string $_vcard the vcard * @param int/string $_abID=null the internal addressbook id or !$_abID for a new enty + * @param boolean $merge=false merge data with existing entry * @return int contact id */ - function addVCard($_vcard, $_abID) + function addVCard($_vcard, $_abID=null, $merge=false) { - if(!$contact = $this->vcardtoegw($_vcard)) { + if(!$contact = $this->vcardtoegw($_vcard)) + { return false; } - if($_abID) { + if($_abID) + { + if ($merge) + { + $old_contact = $this->read($_abID); + if ($old_contact) + { + foreach ($contact as $key => $value) + { + if (!empty($old_contact[$key])) + { + $contact[$key] = $old_contact[$key]; + } + } + } + } // update entry $contact['id'] = $_abID; } @@ -56,35 +110,73 @@ class addressbook_vcal extends addressbook_bo * return a vcard * * @param int/string $_id the id of the contact - * @param string $_charset='utf-8' encoding of the vcard, default utf-8 + * @param string $_charset='UTF-8' encoding of the vcard, default UTF-8 * @param boolean $extra_charset_attribute=true GroupDAV/CalDAV dont need the charset attribute and some clients have problems with it * @return string containing the vcard */ - function getVCard($_id,$_charset='utf-8',$extra_charset_attribute=true) + function getVCard($_id,$_charset='UTF-8',$extra_charset_attribute=true) { require_once(EGW_SERVER_ROOT.'/phpgwapi/inc/horde/Horde/iCalendar/vcard.php'); - $vCard = new Horde_iCalendar_vcard; + #Horde::logMessage("vCalAddressbook clientProperties:\n" . print_r($this->clientProperties, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); - if(!is_array($this->supportedFields)) { + $vCard = new Horde_iCalendar_vcard($this->version); + + if (!is_array($this->supportedFields)) + { $this->setSupportedFields(); } $sysCharSet = $GLOBALS['egw']->translation->charset(); - $called = microtime(true); - if(!($entry = $this->read($_id))) { + + if (!($entry = $this->read($_id))) + { return false; } - //error_log("entry before fixed:\n".print_r($entry,true)); $this->fixup_contact($entry); - - foreach($this->supportedFields as $vcardField => $databaseFields) + foreach ($this->supportedFields as $vcardField => $databaseFields) { $values = array(); $options = array(); $hasdata = 0; - foreach($databaseFields as $databaseField) + // seperate fields from their options/attributes + $vcardFields = explode(';', $vcardField); + $vcardField = $vcardFields[0]; + $i = 1; + while (isset($vcardFields[$i])) + { + list($oname, $oval) = explode('=', $vcardFields[$i]); + if (!$oval && ($this->version == '3.0')) + { + // declare OPTION as TYPE=OPTION + $options['TYPE'][] = $oname ; + } + else + { + $options[$oname] = $oval; + } + $i++; + } + if (is_array($options['TYPE'])) + { + $oval = implode(",", $options['TYPE']); + unset($options['TYPE']); + $options['TYPE'] = $oval; + } + if (isset($this->clientProperties[$vcardField]['Size'])) + { + $size = $this->clientProperties[$vcardField]['Size']; + $noTruncate = $this->clientProperties[$vcardField]['NoTruncate']; + //Horde::logMessage("vCalAddressbook $vcardField Size: $size, NoTruncate: " . + // ($noTruncate ? 'TRUE' : 'FALSE'), __FILE__, __LINE__, PEAR_LOG_DEBUG); + } + else + { + $size = -1; + $noTruncate = false; + } + foreach ($databaseFields as $databaseField) { $value = ""; @@ -103,34 +195,92 @@ class addressbook_vcal extends addressbook_bo case 'bday': if (!empty($value)) { - $value = str_replace('-','',$value).'T000000Z'; + if ($size == 8) + { + $value = str_replace('-','',$value); + } + elseif (isset($options['TYPE']) && ( + $options['TYPE'] == 'BASIC')) + { + unset($options['TYPE']); + // used by old SyncML implementations + $value = str_replace('-','',$value).'T000000Z'; + } $hasdata++; } break; case 'jpegphoto': - if(!empty($value)) + if (!empty($value) && + (($size < 0) || (strlen($value) < $size))) { - error_log(__FILE__ ."\nThis vcard contains a JPEG\n '".$value."'"); - $options['ENCODING'] = 'BASE64'; - $options['TYPE'] = 'JPEG'; - $value = base64_encode($value); - $hasdata++; + if (!isset($options['TYPE'])) + { + $options['TYPE'] = 'JPEG'; + } + if (!isset($options['ENCODING'])) + { + $options['ENCODING'] = 'BASE64'; + } + $hasdata++; + } + else + { + $value = ''; } break; case 'cat_id': if (!empty($value)) { - $value = implode(",", $this->get_categories($value)); + $values = &$this->get_categories($value); + $values = (array) $GLOBALS['egw']->translation->convert($values, $sysCharSet, $_charset); + $value = implode(',', $values); // just for the CHARSET recognition + if(($extra_charset_attribute || $this->productName == 'kde') + && preg_match('/([\177-\377])/',$value)) + { + $options['CHARSET'] = $_charset; + // KAddressbook requires non-ascii chars to be qprint encoded, other clients eg. nokia phones have trouble with that + if ($this->productName == 'kde') + { + $options['ENCODING'] = 'QUOTED-PRINTABLE'; + } + } + $hasdata++; } - // fall-through to the normal processing of string values - default: - if(!empty($value) || in_array($vcardField,array('FN','ORG','N'))) - { - $value = $GLOBALS['egw']->translation->convert(trim($value), $sysCharSet,$_charset); + break; - if(($extra_charset_attribute || $this->productName == 'kde') && preg_match('/([\177-\377])/',$value)) + default: + if (($size > 0) && strlen(implode(',', $values) . $value) > $size) + { + if ($noTruncate) + { + Horde::logMessage("vCalAddressbook $vcardField omitted due to maximum size $size", + __FILE__, __LINE__, PEAR_LOG_WARNING); + continue; + } + // truncate the value to size + $cursize = strlen(implode('', $values)); + $left = $size - $cursize - count($databaseFields) + 1; + if ($left > 0) + { + $value = substr($value, 0, $left); + } + else + { + $value = ''; + } + Horde::logMessage("vCalAddressbook $vcardField truncated to maximum size $size", + __FILE__, __LINE__, PEAR_LOG_INFO); + } + if (!empty($value) // required field + || in_array($vcardField,array('FN','ORG','N')) + || ($size >= 0 && !$noTruncate)) + { + $value = $GLOBALS['egw']->translation->convert(trim($value), $sysCharSet, $_charset); + $values[] = $value; + if(($extra_charset_attribute || $this->productName == 'kde') + && preg_match('/([\177-\377])/',$value)) { $options['CHARSET'] = $_charset; // KAddressbook requires non-ascii chars to be qprint encoded, other clients eg. nokia phones have trouble with that @@ -140,22 +290,27 @@ class addressbook_vcal extends addressbook_bo } } // protect the CardDAV - elseif(($extra_charset_attribute && preg_match('/([\000-\012\015\016\020-\037\075])/',$value))) + elseif (($extra_charset_attribute && preg_match('/([\000-\012\015\016\020-\037\075])/',$value))) { $options['ENCODING'] = 'QUOTED-PRINTABLE'; } + if ($vcardField == 'TEL' && $entry['tel_prefer'] && + ($databaseField == $entry['tel_prefer'])) + { + if ($options['TYPE']) + { + $options['TYPE'] .= ','; + } + $options['TYPE'] .= 'PREF'; + } $hasdata++; } + else + { + $values[] = ''; + } break; } - - if (empty($value)) - { - $value = ""; - } - //if(preg_match('/([\000-\010])/',$value)) error_log(__FILE__."#".__METHOD__."\nThis value has 000-010: $value"); - - $values[] = $value; } if ($hasdata <= 0) @@ -165,57 +320,64 @@ class addressbook_vcal extends addressbook_bo continue; } - $vCard->setAttribute($vcardField, implode(';', $values)); - $vCard->setParameter($vcardField, $options); + $vCard->setAttribute($vcardField, $value, $options, true, $values); + //$vCard->setParameter($vcardField, $options); } + $result = $vCard->exportvCalendar(); - //error_log(__FILE__ . __METHOD__ ."\nvcard:".print_r($result,true)); + + Horde::logMessage("vCalAddressbook getVCard:\n" . print_r($result, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + return $result; } - function search($_vcard, $contentID=null) + function search($_vcard, $contentID=null, $relax=false) { - if(!($contact = $this->vcardtoegw($_vcard))) { - return false; + $result = false; + + if (($contact = $this->vcardtoegw($_vcard))) + { + if ($contentID) + { + $contact['contact_id'] = $contentID; + } + $result = $this->find_contact($contact, $relax); + } + return $result; + } + + function setSupportedFields($_productManufacturer='', $_productName='') + { + $state = &$_SESSION['SyncML.state']; + if (isset($state)) + { + $deviceInfo = $state->getClientDeviceInfo(); } - if ($contentID) { - $contact['contact_id'] = $contentID; + // store product manufacturer and name, to be able to use it elsewhere + if ($_productManufacturer) + { + $this->productManufacturer = strtolower($_productManufacturer); + $this->productName = strtolower($_productName); } - unset($contact['private']); - unset($contact['note']); - unset($contact['n_fn']); - unset($contact['email']); - unset($contact['email_home']); - unset($contact['url']); - unset($contact['url_home']); - unset($contact['n_fileas']); - - // some clients cut the values, because they do not support the same length of data like eGW - // at least the first 10 characters must match - $maybeCuttedFields = array('org_unit', 'org_name','title'); - foreach($maybeCuttedFields as $fieldName) { - if(!empty($contact[$fieldName]) && strlen($contact[$fieldName]) > 10) { - $contact[$fieldName] .= '*'; + if(isset($deviceInfo) && is_array($deviceInfo)) + { + if(!isset($this->productManufacturer) || + $this->productManufacturer == '' || + $this->productManufacturer == 'file') + { + $this->productManufacturer = strtolower($deviceInfo['manufacturer']); + } + if(!isset($this->productName) || $this->productName == '') + { + $this->productName = strtolower($deviceInfo['model']); } } - //error_log(print_r($contact, true)); + Horde::logMessage('setSupportedFields(' . $this->productManufacturer . ', ' . $this->productName .')', + __FILE__, __LINE__, PEAR_LOG_DEBUG); - #if($foundContacts = parent::search($contact, true, '', '', '%')) { - if($foundContacts = parent::search($contact)) { - return $foundContacts[0]['id']; - } - return false; - } - - function setSupportedFields($_productManufacturer='file', $_productName='') - { - // store product manufacturer and name, to be able to use it elsewhere - $this->productManufacturer = strtolower($_productManufacturer); - $this->productName = strtolower($_productName); - //error_log("prodM- prodN".print_r($_productManufacturer,true).print_r($_productName,true)); /** * ToDo Lars: * + changes / renamed fields in 1.3+: @@ -256,6 +418,7 @@ class addressbook_vcal extends addressbook_bo 'TEL;HOME' => array('tel_home'), 'TEL;WORK' => array('tel_work'), 'TITLE' => array('title'), + 'UID' => array('uid'), ); $defaultFields[1] = array( // all entries, nexthaus corporation, groupdav, ... @@ -265,9 +428,8 @@ class addressbook_vcal extends addressbook_bo 'adr_two_postalcode','adr_two_countryname'), 'BDAY' => array('bday'), 'CATEGORIES' => array('cat_id'), - 'EMAIL;INTERNET' => array('email'),//tp4 tp43 - 'EMAIL;INTERNET;HOME' => array('email_home'),//tp42 tp42 - #'EMAIL;INTERNET;WORK' => array('#email_work'),//tp43 lost + 'EMAIL;INTERNET;WORK' => array('email'), + 'EMAIL;INTERNET;HOME' => array('email_home'), 'N' => array('n_family','n_given','n_middle','n_prefix','n_suffix'), 'FN' => array('n_fn'), 'NOTE' => array('note'), @@ -305,6 +467,7 @@ class addressbook_vcal extends addressbook_bo 'TEL;WORK' => array('tel_work'), 'TITLE' => array('title'), 'URL;WORK' => array('url'), + 'UID' => array('uid'), ); $defaultFields[3] = array( // siemens @@ -318,7 +481,7 @@ class addressbook_vcal extends addressbook_bo 'N' => array('n_family','n_given','','',''), 'FN' => array('n_fn'), 'NOTE' => array('note'), - 'ORG' => array('org_name','org_unit'), + 'ORG' => array('org_name'), // only one company field is supported 'TEL;CELL;WORK' => array('tel_cell'), 'TEL;FAX;WORK' => array('tel_fax'), 'TEL;HOME' => array('tel_home'), @@ -326,6 +489,7 @@ class addressbook_vcal extends addressbook_bo 'TEL;WORK' => array('tel_work'), 'TITLE' => array('title'), 'URL;WORK' => array('url'), + 'UID' => array('uid'), ); $defaultFields[4] = array( // nokia 6600 @@ -333,7 +497,7 @@ class addressbook_vcal extends addressbook_bo 'adr_one_postalcode','adr_one_countryname'), 'ADR;HOME' => array('','','adr_two_street','adr_two_locality','adr_two_region', 'adr_two_postalcode','adr_two_countryname'), - 'BDAY' => array('bday'), + 'BDAY;TYPE=BASIC' => array('bday'), 'EMAIL;INTERNET;WORK' => array('email'), 'EMAIL;INTERNET;HOME' => array('email_home'), 'N' => array('n_family','n_given','','',''), @@ -350,6 +514,7 @@ class addressbook_vcal extends addressbook_bo 'TITLE' => array('title'), 'URL;WORK' => array('url'), 'URL;HOME' => array('url_home'), + 'UID' => array('uid'), ); $defaultFields[5] = array( // nokia e61 @@ -357,7 +522,7 @@ class addressbook_vcal extends addressbook_bo 'adr_one_postalcode','adr_one_countryname'), 'ADR;HOME' => array('','','adr_two_street','adr_two_locality','adr_two_region', 'adr_two_postalcode','adr_two_countryname'), - 'BDAY' => array('bday'), + 'BDAY;TYPE=BASIC' => array('bday'), 'EMAIL;INTERNET;WORK' => array('email'), 'EMAIL;INTERNET;HOME' => array('email_home'), 'N' => array('n_family','n_given','','n_prefix','n_suffix'), @@ -374,6 +539,7 @@ class addressbook_vcal extends addressbook_bo 'TITLE' => array('title'), 'URL;WORK' => array('url'), 'URL;HOME' => array('url_home'), + 'UID' => array('uid'), ); $defaultFields[6] = array( // funambol: fmz-thunderbird-plugin @@ -384,7 +550,7 @@ class addressbook_vcal extends addressbook_bo 'EMAIL' => array('email'), 'EMAIL;HOME' => array('email_home'), 'N' => array('n_family','n_given','','',''), - 'FN' => array('n_fn'), + 'FN' => array('n_fn'), 'NOTE' => array('note'), 'ORG' => array('org_name','org_unit'), 'TEL;CELL' => array('tel_cell'), @@ -420,12 +586,12 @@ class addressbook_vcal extends addressbook_bo 'NOTE' => array('note'), 'X-EVOLUTION-ASSISTANT' => array('assistent'), 'PHOTO' => array('jpegphoto'), + 'UID' => array('uid'), ); $defaultFields[8] = array_merge($defaultFields[1],array( // KDE Addressbook, only changes from all=1 'ORG' => array('org_name'), 'X-KADDRESSBOOK-X-Department' => array('org_unit'), - 'PHOTO' => array('jpegphoto'), )); $defaultFields[9] = array( // nokia e90 @@ -433,7 +599,7 @@ class addressbook_vcal extends addressbook_bo 'adr_one_postalcode','adr_one_countryname'), 'ADR;HOME' => array('','adr_two_street2','adr_two_street','adr_two_locality','adr_two_region', 'adr_two_postalcode','adr_two_countryname'), - 'BDAY' => array('bday'), + 'BDAY;TYPE=BASIC' => array('bday'), 'X-CLASS' => array('private'), 'EMAIL;INTERNET;WORK' => array('email'), 'EMAIL;INTERNET;HOME' => array('email_home'), @@ -455,6 +621,7 @@ class addressbook_vcal extends addressbook_bo 'X-ASSISTANT' => array('assistent'), 'X-ASSISTANT-TEL' => array('tel_assistent'), 'PHOTO' => array('jpegphoto'), + 'UID' => array('uid'), ); $defaultFields[10] = array( // nokia 9300 @@ -476,50 +643,106 @@ class addressbook_vcal extends addressbook_bo 'TEL;HOME;VOICE' => array('tel_home'), 'TITLE' => array('contact_role'), 'URL' => array('url'), + 'UID' => array('uid'), ); - - $defaultFields[11] = array( // funambol: iphone + $defaultFields[11] = array( // funambol: iphone, blackberry, wm pocket pc 'ADR;WORK' => array('','','adr_one_street','adr_one_locality','adr_one_region', 'adr_one_postalcode','adr_one_countryname'), 'ADR;HOME' => array('','','adr_two_street','adr_two_locality','adr_two_region', 'adr_two_postalcode','adr_two_countryname'), + 'BDAY' => array('bday'), + 'CATEGORIES' => array('cat_id'), 'EMAIL;INTERNET;WORK' => array('email'), 'EMAIL;INTERNET;HOME' => array('email_home'), 'N' => array('n_family','n_given','n_middle','n_prefix','n_suffix'), - 'FN' => array('n_fn'), + 'FN' => array('n_fn'), 'NOTE' => array('note'), 'ORG' => array('org_name','org_unit'), 'TEL;CELL' => array('tel_cell'), - 'TEL;HOME;FAX' => array('tel_fax_home'), - 'TEL;WORK;FAX' => array('tel_fax'), + 'TEL;FAX;HOME' => array('tel_fax_home'), + 'TEL;FAX;WORK' => array('tel_fax'), 'TEL;VOICE;HOME' => array('tel_home'), 'TEL;VOICE;WORK' => array('tel_work'), 'TEL;PAGER' => array('tel_pager'), - 'TEL;CAR' => array('tel_car'), + 'TEL;CAR' => array('tel_car'), 'TITLE' => array('title'), 'URL;WORK' => array('url'), - 'URL;HOME' => array('url_home'), + 'URL;HOME' => array('url_home'), 'PHOTO' => array('jpegphoto'), + 'UID' => array('uid'), + ); + + $defaultFields[12] = array( // Synthesis 4 iPhone + 'ADR;WORK' => array('','','adr_one_street','adr_one_locality','adr_one_region', + 'adr_one_postalcode','adr_one_countryname'), + 'ADR;HOME' => array('','','adr_two_street','adr_two_locality','adr_two_region', + 'adr_two_postalcode','adr_two_countryname'), + 'BDAY' => array('bday'), + 'CATEGORIES' => array('cat_id'), + 'EMAIL;WORK;INTERNET' => array('email'), + 'EMAIL;HOME;INTERNET' => array('email_home'), + 'N' => array('n_family','n_given','n_middle','n_prefix','n_suffix'), + 'FN' => array('n_fn'), + 'NOTE' => array('note'), + 'ORG' => array('org_name','org_unit'), + 'TEL;VOICE;CELL' => array('tel_cell'), + 'TEL;WORK;FAX' => array('tel_fax'), + 'TEL;HOME;FAX' => array('tel_fax_home'), + 'TEL;WORK;VOICE' => array('tel_work'), + 'TEL;HOME;VOICE' => array('tel_home'), + 'TEL;PAGER' => array('tel_pager'), + 'TEL;X-CustomLabel-car' => array('tel_car'), + 'TITLE' => array('title'), + 'URL;WORK' => array('url'), + 'ROLE' => array('role'), + 'URL;HOME' => array('url_home'), + 'PHOTO' => array('jpegphoto'), + 'UID' => array('uid'), + ); + + $defaultFields[13] = array( // sonyericsson + 'ADR;WORK' => array('','','adr_one_street','adr_one_locality','adr_one_region', + 'adr_one_postalcode','adr_one_countryname'), + 'ADR;HOME' => array('','','adr_two_street','adr_two_locality','adr_two_region', + 'adr_two_postalcode','adr_two_countryname'), + 'BDAY' => array('bday'), + 'EMAIL;WORK' => array('email'), + 'EMAIL;HOME' => array('email_home'), + 'N' => array('n_family','n_given','n_middle','n_prefix','n_suffix'), + 'NOTE' => array('note'), + 'ORG' => array('org_name',''), + 'TEL;CELL;WORK' => array('tel_cell'), + 'TEL;CELL;HOME' => array('tel_cell_private'), + 'TEL;FAX' => array('tel_fax'), + 'TEL;HOME' => array('tel_home'), + 'TEL;WORK' => array('tel_work'), + 'TITLE' => array('title'), + 'URL' => array('url'), + 'UID' => array('uid'), + //'PHOTO' => array('jpegphoto'), ); //error_log("Client: $_productManufacturer $_productName"); - switch($this->productManufacturer) + switch ($this->productManufacturer) { case 'funambol': case 'funambol inc.': switch ($this->productName) { case 'thunderbird': + case 'mozilla plugin': $this->supportedFields = $defaultFields[6]; break; + case 'pocket pc plug-in': + case 'blackberry plug-in': case 'iphone': $this->supportedFields = $defaultFields[11]; break; default: - //error_log("Funambol product '$_productName', assuming same as thunderbird"); + error_log("Funambol product '$this->productName', assuming same as thunderbird"); $this->supportedFields = $defaultFields[6]; break; } @@ -527,20 +750,20 @@ class addressbook_vcal extends addressbook_bo case 'nexthaus corporation': case 'nexthaus corp': - switch($this->productName) + switch ($this->productName) { case 'syncje outlook edition': $this->supportedFields = $defaultFields[1]; break; default: - error_log("Nethaus product '$_productName', assuming same as 'syncje outlook'"); + error_log("Nexthaus product '$this->productName', assuming same as 'syncje outlook'"); $this->supportedFields = $defaultFields[1]; break; } break; case 'nokia': - switch($this->productName) + switch ($this->productName) { case 'e61': $this->supportedFields = $defaultFields[5]; @@ -556,8 +779,11 @@ class addressbook_vcal extends addressbook_bo case '6600': $this->supportedFields = $defaultFields[4]; break; + case 'nokia 6131': + $this->supportedFields = $defaultFields[4]; + break; default: - error_log("Unknown Nokia phone '$_productName', assuming same as '6600'"); + error_log("Unknown Nokia phone 'this->$productName', assuming same as '6600'"); $this->supportedFields = $defaultFields[4]; break; } @@ -567,7 +793,7 @@ class addressbook_vcal extends addressbook_bo // multisync does not provide anymore information then the manufacturer // we suppose multisync with evolution case 'the multisync project': - switch($this->productName) + switch ($this->productName) { default: $this->supportedFields = $defaultFields[0]; @@ -576,13 +802,13 @@ class addressbook_vcal extends addressbook_bo break; case 'siemens': - switch($this->productName) + switch ($this->productName) { case 'sx1': $this->supportedFields = $defaultFields[3]; break; default: - error_log("Unknown Siemens phone '$_productName', assuming same as 'sx1'"); + error_log("Unknown Siemens phone '$this->productName', assuming same as 'sx1'"); $this->supportedFields = $defaultFields[3]; break; } @@ -590,29 +816,45 @@ class addressbook_vcal extends addressbook_bo case 'sonyericsson': case 'sony ericsson': - switch($this->productName) + switch ($this->productName) { + case 'p910i': case 'd750i': $this->supportedFields = $defaultFields[2]; break; - case 'p910i': + case 'w760i': + case 'w890i': + $this->supportedFields = $defaultFields[13]; + break; default: - error_log("unknown Sony Ericsson phone '$_productName', assuming same as 'd750i'"); - $this->supportedFields = $defaultFields[2]; + if ($this->productName[0] == 'w') + { + error_log("unknown Sony Ericsson phone '$this->productName', assuming same as 'W760i'"); + $this->supportedFields = $defaultFields[13]; + } + else + { + error_log("unknown Sony Ericsson phone '$this->productName', assuming same as 'D750i'"); + $this->supportedFields = $defaultFields[2]; + } break; } break; case 'synthesis ag': - switch($this->productName) + switch ($this->productName) { case 'sysync client pocketpc pro': case 'sysync client pocketpc std': $this->supportedFields = $defaultFields[1]; - #$this->supportedFields['PHOTO'] = array('jpegphoto'); + $this->supportedFields['TEL;CELL;CAR;VOICE'] = array('tel_car'); + break; + case 'sysync client iphone contacts': + case 'sysync client iphone contacts+todoz': + $this->supportedFields = $defaultFields[12]; break; default: - error_log("Synthesis connector '$_productName', using default fields"); + error_log("Synthesis connector '$this->productName', using default fields"); $this->supportedFields = $defaultFields[0]; break; } @@ -625,8 +867,9 @@ class addressbook_vcal extends addressbook_bo case 'file': // used outside of SyncML, eg. by the calendar itself ==> all possible fields $this->supportedFields = $defaultFields[1]; break; - case 'groupdav': // all GroupDAV access goes through here - switch($this->productName) + + case 'groupdav': // all GroupDAV access goes through here + switch ($this->productName) { case 'kde': // KDE Addressbook $this->supportedFields = $defaultFields[1]; @@ -636,9 +879,10 @@ class addressbook_vcal extends addressbook_bo $this->supportedFields = $defaultFields[1]; } break; + // the fallback for SyncML default: - error_log(__FILE__. __METHOD__ ."\nManufacturer-Product not found: '$_productManufacturer' '$_productName'"); + error_log(__FILE__ . __METHOD__ ."\nClient not found:'" . $this->productManufacturer . "' '" . $this->productName . "'"); $this->supportedFields = $defaultFields[0]; break; } @@ -647,207 +891,358 @@ class addressbook_vcal extends addressbook_bo function vcardtoegw($_vcard) { // the horde class does the charset conversion. DO NOT CONVERT HERE. + // be as flexible as possible - //error_log("vcardin vtoe".print_r($_vcard, true)); - if(!is_array($this->supportedFields)) - { - $this->setSupportedFields(); - } + + $databaseFields = array( + 'ADR;WORK' => array('','','adr_one_street','adr_one_locality','adr_one_region', + 'adr_one_postalcode','adr_one_countryname'), + 'ADR;HOME' => array('','','adr_two_street','adr_two_locality','adr_two_region', + 'adr_two_postalcode','adr_two_countryname'), + 'BDAY' => array('bday'), + 'X-CLASS' => array('private'), + 'CATEGORIES' => array('cat_id'), + 'EMAIL;WORK' => array('email'), + 'EMAIL;HOME' => array('email_home'), + 'N' => array('n_family','n_given','n_middle', + 'n_prefix','n_suffix'), + #'FN' => array('n_fn'), + 'NOTE' => array('note'), + 'ORG' => array('org_name','org_unit'), + 'TEL;CELL;WORK' => array('tel_cell'), + 'TEL;CELL;HOME' => array('tel_cell_private'), + 'TEL;CAR' => array('tel_car'), + 'TEL;OTHER' => array('tel_other'), + 'TEL;WORK' => array('tel_work'), + 'TEL;FAX;WORK' => array('tel_fax'), + 'TEL;HOME' => array('tel_home'), + 'TEL;FAX;HOME' => array('tel_fax_home'), + 'TEL;PAGER' => array('tel_pager'), + 'TITLE' => array('title'), + 'URL;WORK' => array('url'), + 'URL;HOME' => array('url_home'), + 'ROLE' => array('role'), + 'NICKNAME' => array('label'), + 'FBURL' => array('freebusy_uri'), + 'PHOTO' => array('jpegphoto'), + 'X-ASSISTANT' => array('assistent'), + 'X-ASSISTANT-TEL' => array('tel_assistent'), + 'UID' => array('uid'), + ); + + Horde::logMessage("vCalAddressbook vcardtoegw:\n$_vcard", __FILE__, __LINE__, PEAR_LOG_DEBUG); require_once(EGW_SERVER_ROOT.'/phpgwapi/inc/horde/Horde/iCalendar.php'); + $container = false; $vCard = Horde_iCalendar::newComponent('vcard', $container); - // Unfold any folded lines. - $vCardUnfolded = preg_replace ('/(\r|\n)+ /', ' ', $_vcard); - - if(!$vCard->parsevCalendar($vCardUnfolded, 'VCARD')) + if (!$vCard->parsevCalendar($_vcard, 'VCARD')) { return False; } $vcardValues = $vCard->getAllAttributes(); + if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) + { + $minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']; + } + else + { + $minimum_uid_length = 8; + } + #print "
$_vcard
"; #error_log(print_r($vcardValues, true)); + Horde::logMessage("vCalAddressbook vcardtoegw: " . print_r($vcardValues, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $email = 0; + $tel = 1; + $cell = 1; + $url = 0; + $pref_tel = false; foreach($vcardValues as $key => $vcardRow) { - $rowName = $vcardRow['name']; - - $vcardElementCount = count($vcardRow['params'],COUNT_RECURSIVE); - - if( $vcardElementCount > 0 ) + $rowName = strtoupper($vcardRow['name']); + switch ($rowName) { - foreach($vcardRow['params'] as $VKey => $vcardParam) + case 'EMAIL': + $email++; + break; + case 'URL': + $url++; + break; + } + if ($vcardRow['value'] == '' && implode('', $vcardRow['values']) == '') + { + unset($vcardRow); + continue; + } + $rowTypes = array(); + + $vcardRow['uparams'] = array(); + foreach ($vcardRow['params'] as $pname => $params) + { + $pname = strtoupper($pname); + $vcardRow['uparams'][$pname] = $params; + } + ksort($vcardRow['uparams']); + + foreach ($vcardRow['uparams'] as $pname => $params) + { + switch ($pname) { - if( ! strlen($vcardParam) ) - { - $rowName .= ';'.$VKey; - } - if( $VKey == 'TYPE' && in_array(strtoupper($vcardParam),array('CELL','FAX','PAGER','WORK','HOME','VOICE','CAR'))) - { - $rowName .= ';'.strtoupper($vcardParam); - } + case 'TYPE': + if (is_array($params)) + { + $rowTypes = array(); + foreach ($params as $param) + { + $rowTypes[] = strtoupper($param); + } + sort($rowTypes); + } + else + { + $rowTypes[] = strtoupper($params); + } + foreach ($rowTypes as $type) + { + switch ($type) + { + case 'PAGER': + case 'FAX': + case 'VOICE': + case 'OTHER': + case 'CELL': + case 'WORK': + case 'HOME': + $rowName .= ';' . $type; + break; + case 'PREF': + if ($vcardRow['name'] == 'TEL') + { + $pref_tel = $key; + } + break; + case 'CAR': + case 'X-CUSTOMLABEL-CAR': + $rowName = 'TEL;CAR'; + break; + default: + break; + } + } + break; + //case 'INTERNET': + case 'PREF': + if (strtoupper($vcardRow['name']) == 'TEL') + { + $pref_tel = $key; + } + break; + case 'FAX': + case 'PAGER': + case 'VOICE': + case 'OTHER': + case 'CELL': + case 'WORK': + case 'HOME': + $rowName .= ';' . $pname; + break; + case 'CAR': + case 'X-CUSTOMLABEL-CAR': + $rowName = 'TEL;CAR'; + break; + default: + break; } - } + } + + if($rowName == 'EMAIL') + { + $rowName .= ';X-egw-Ref' . $email; + } + + if(($rowName == 'TEL;CELL') || + ($rowName == 'TEL;CELL;VOICE')) + { + $rowName = 'TEL;CELL;X-egw-Ref' . $cell++; + } + + if(($rowName == 'TEL') || + ($rowName == 'TEL;VOICE')) + { + $rowName = 'TEL;X-egw-Ref' . $tel++; + } + + if($rowName == 'URL') + { + $rowName = 'URL;X-egw-Ref' . $url; + } + $rowNames[$rowName] = $key; } #error_log(print_r($rowNames, true)); - // now we have all rowNames the vcard provides - // we just need to map to the right addressbook fieldnames - // we need also to take care about ADR for example. we do not - // support this. We support only ADR;WORK or ADR;HOME + // All rowNames of the vCard are now concatenated with their qualifiers. + // If qualifiers are missing we apply a default strategy. + // E.g. ADR will be either ADR;WORK, if no ADR;WORK is given, + // or else ADR;HOME, if not available elsewhere. - // njv: As the order of tag occurence is undefined and tags 1... to n are mapped to one addressbook - // fieldname and tags 1... to n may have conflicting content eg EMAIL is set but EMAIL;INTERNET is empty - // and both are mapped to "email" who wins? + $finalRowNames = array(); - foreach($rowNames as $rowName => $vcardKey) + foreach ($rowNames as $rowName => $vcardKey) { - // error_log("eachrownane:".print_r($rowName,true)); - switch($rowName) { - case 'TEL;VOICE;HOME': - case 'TEL;VOICE;WORK': // check if we have a mapping without VOICE - $replace = str_replace('VOICE;','',$rowName); - if (!isset($rowNames[$replace]) && array_key_exists($replace, $this->supportedFields)) - { - $finalRowNames[$replace] = $vcardKey; // if yes use that - } - else - { - $finalRowNames[$rowName] = $vcardKey; // else use existing mapping - } - break; - case 'ADR': - case 'TEL': - case 'URL': + if (!isset($rowNames[$rowName . ';WORK'])) + { + $finalRowNames[$rowName . ';WORK'] = $vcardKey; + } + elseif (!isset($rowNames[$rowName . ';HOME'])) + { + $finalRowNames[$rowName . ';HOME'] = $vcardKey; + } + break; case 'TEL;FAX': - case 'TEL;CELL': - case 'TEL;PAGER': - case 'TEL;VOICE': - if(!isset($rowNames[$rowName. ';WORK']) && array_key_exists($rowName. ';WORK', $this->supportedFields) ) + if (!isset($rowNames['TEL;FAX;WORK']) + && !isset($finalRowNames['TEL;FAX;WORK'])) { - $finalRowNames[$rowName. ';WORK'] = $vcardKey; + $finalRowNames['TEL;FAX;WORK'] = $vcardKey; } - else + elseif (!isset($rowNames['TEL;FAX;HOME']) + && !isset($finalRowNames['TEL;FAX;HOME'])) + { + $finalRowNames['TEL;FAX;HOME'] = $vcardKey; + } + break; + case 'TEL;VOICE;WORK': + $finalRowNames['TEL;WORK'] = $vcardKey; + break; + case 'TEL;HOME;VOICE': + $finalRowNames['TEL;HOME'] = $vcardKey; + break; + case 'TEL;OTHER;VOICE': + $finalRowNames['TEL;OTHER'] = $vcardKey; + break; + case 'TEL;CAR;VOICE': + case 'TEL;CAR;CELL': + case 'TEL;CAR;CELL;VOICE': + $finalRowNames['TEL;CAR'] = $vcardKey; + break; + case 'TEL;X-egw-Ref1': + if (!isset($rowNames['TEL;VOICE;WORK']) + && !isset($rowNames['TEL;WORK']) + && !isset($finalRowNames['TEL;WORK'])) { - $InvolvedValues = explode(';',$rowName.';WORK'); - foreach($this->supportedFields as $suppFields => $suppFieldsValue ) - { - $InvolvedHits = 0; - foreach($InvolvedValues as $hlparr => $hlparrKey ) - { - if( ! stristr( $suppFields,$hlparrKey ) === FALSE ) - { - $InvolvedHits++; - } - } - if( count($InvolvedValues) == $InvolvedHits && !isset($finalRowNames[$suppFields]) ) - { - $finalRowNames[$suppFields] = $vcardKey; - break; // if a combination of all words in $InvolvedValues were found - } - } - if( count($InvolvedValues) != $InvolvedHits && ! isset($finalRowNames[$rowName]) && array_key_exists($rowName, $this->supportedFields) ) - { - $finalRowNames[$rowName] = $vcardKey; - } + $finalRowNames['TEL;WORK'] = $vcardKey; + break; + } + case 'TEL;X-egw-Ref2': + if (!isset($rowNames['TEL;HOME;VOICE']) + && !isset($rowNames['TEL;HOME']) + && !isset($finalRowNames['TEL;HOME'])) + { + $finalRowNames['TEL;HOME'] = $vcardKey; } break; - case 'EMAIL': - case 'EMAIL;INTERNET': - $ckey = false; - foreach($this->supportedFields as $tag => $value) - { - if(in_array ('email', $value)) - { - $ckey = $tag; - } - } - // error_log("key:$ckey:$vcardKey"); - - if( $ckey && !empty($vcardValues[$vcardKey]['value'])) - { - // error_log("$akey : ".print_r($vcardKey,true)); - $finalRowNames[$ckey] = $vcardKey; - } - break; - - case 'EMAIL;WORK': - if(!isset($rowNames['EMAIL;INTERNET;WORK'])) + case 'TEL;CELL;X-egw-Ref1': + if (!isset($rowNames['TEL;CELL;WORK']) + && !isset($finalRowNames['TEL;CELL;WORK'])) { - if(in_array ('#email_work', $value)) - { - $akey = $tag; - } + $finalRowNames['TEL;CELL;WORK'] = $vcardKey; + break; } - // error_log("key:$akey:$vcardKey"); - - if( $akey && !empty($vcardValues[$vcardKey]['value'])) + case 'TEL;CELL;X-egw-Ref2': + if (!isset($rowNames['TEL;CELL;HOME']) + && !isset($finalRowNames['TEL;CELL;HOME'])) { - //error_log("$akey : ".print_r($vcardKey,true)); - $finalRowNames[$akey] = $vcardKey; + $finalRowNames['TEL;CELL;HOME'] = $vcardKey; + break; + } + case 'TEL;CELL;X-egw-Ref3': + if (!isset($rowNames['TEL;CAR']) + && !isset($rowNames['TEL;CAR;VOICE']) + && !isset($rowNames['TEL;CAR;CELL']) + && !isset($rowNames['TEL;CAR;CELL;VOICE']) + && !isset($finalRowNames['TEL;CAR'])) + { + $finalRowNames['TEL;CAR'] = $vcardKey; } break; - case 'EMAIL;HOME': - if(!isset($rowNames['EMAIL;INTERNET;HOME'])) + case 'EMAIL;X-egw-Ref1': + if (!isset($rowNames['EMAIL;WORK']) && + !isset($finalRowNames['EMAIL;WORK'])) { - $finalRowNames['EMAIL;INTERNET;HOME'] = $vcardKey; + $finalRowNames['EMAIL;WORK'] = $vcardKey; + break; } - - // error_log("email_home-key: ".print_r($bkey, true)); - - if($bkey && !empty($vcardValues[$vcardKey]['value'])) + case 'EMAIL;X-egw-Ref2': + if (!isset($rowNames['EMAIL;HOME']) && + !isset($finalRowNames['EMAIL;HOME'])) { - // error_log("$bkey :".print_r($vcardKey,true)); - $finalRowNames[$bkey] = $vcardKey; + $finalRowNames['EMAIL;HOME'] = $vcardKey; + } + break; + case 'URL;X-egw-Ref1': + if (!isset($rowNames['URL;WORK']) && + !isset($finalRowNames['URL;WORK'])) + { + $finalRowNames['URL;WORK'] = $vcardKey; + break; + } + case 'URL;X-egw-Ref2': + if (!isset($rowNames['URL;HOME']) && + !isset($finalRowNames['URL;HOME'])) + { + $finalRowNames['URL;HOME'] = $vcardKey; } break; - case 'VERSION': break; - - default: - //error_log("default row map".print_r($vcardKey,true)); - $finalRowNames[$rowName] = $vcardKey; + case 'X-EVOLUTION-ASSISTANT': + $finalRowNames['X-ASSISTANT'] = $vcardKey; break; + default: + $finalRowNames[$rowName] = $vcardKey; + break; } } - //error_log("finalrownames".print_r($finalRowNames, true)); + + #error_log(print_r($finalRowNames, true)); + #Horde::logMessage("vCalAddressbook vcardtoegw finalRowNames: " . print_r($finalRowNames, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + $contact = array(); - foreach($finalRowNames as $key => $vcardKey) + foreach ($finalRowNames as $key => $vcardKey) { - if(isset($this->supportedFields[$key])) + if (isset($databaseFields[$key])) { - $fieldNames = $this->supportedFields[$key]; - foreach($fieldNames as $fieldKey => $fieldName) + $fieldNames = $databaseFields[$key]; + foreach ($fieldNames as $fieldKey => $fieldName) { - if(!empty($fieldName)) + if (!empty($fieldName)) { - if ($fieldName == 'jpegphoto' || $vcardValues[$vcardKey]['params']['ENCODING'] == 'b') + $value = trim($vcardValues[$vcardKey]['values'][$fieldKey]); + if ($pref_tel && (($vcardKey == $pref_tel) || + ($vcardValues[$vcardKey]['name'] == 'TEL') && + ($vcardValues[$vcardKey]['value'] == $vcardValues[$pref_tel]['value']))) { - $value = base64_decode($vcardValues[$vcardKey]['values'][$fieldKey]); + $contact['tel_prefer'] = $fieldName; } - else - { - $value = trim($vcardValues[$vcardKey]['values'][$fieldKey]); - } - switch($fieldName) { case 'bday': - if(!empty($value)) { - $contact[$fieldName] = date('Y-m-d', $value); - } + $contact[$fieldName] = $vcardValues[$vcardKey]['values']['year'] . + '-' . $vcardValues[$vcardKey]['values']['month'] . + '-' . $vcardValues[$vcardKey]['values']['mday']; break; case 'private': @@ -855,17 +1250,26 @@ class addressbook_vcal extends addressbook_bo break; case 'cat_id': - $contact[$fieldName] = implode(',',$this->find_or_add_categories(explode(',',$value),'private')); + $contact[$fieldName] = implode(',',$this->find_or_add_categories($vcardValues[$vcardKey]['values'])); + break; + + case 'jpegphoto': + $contact[$fieldName] = $vcardValues[$vcardKey]['value']; break; case 'note': // note may contain ','s but maybe this needs to be fixed in vcard parser... - //$contact[$fieldName] = trim($vcardValues[$vcardKey]['value']); - //break; + $contact[$fieldName] = $vcardValues[$vcardKey]['value']; + break; + case 'uid': + if (strlen($value) < $minimum_uid_length) { + // we don't use it + break; + } default: $contact[$fieldName] = $value; - break; + break; } } } @@ -873,7 +1277,9 @@ class addressbook_vcal extends addressbook_bo } $this->fixup_contact($contact); - //error_log(__FILE__ . __METHOD__ . "\nContact:\n " .print_r($contact,true)); + + Horde::logMessage("vCalAddressbook vcardtoegw: " . print_r($contact, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + return $contact; } @@ -883,7 +1289,7 @@ class addressbook_vcal extends addressbook_bo * @param array $ids contact-ids * @param string $file filename or null for download */ - function export($ids,$file=null) + function export($ids, $file=null) { if (!$file) { @@ -894,7 +1300,7 @@ class addressbook_vcal extends addressbook_bo { return false; } - foreach($ids as $id) + foreach ($ids as $id) { fwrite($fp,$this->getVCard($id)); }