From b6097fa156c07077385a90ded9b8a26f4513072e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Lehrke?= Date: Tue, 9 Feb 2010 21:56:39 +0000 Subject: [PATCH] SyncML Content Handling * Improved find-methods * Timezone support for InfoLog * SyncML Preferences - addressbook and address list are now joined - Primary User Group for addressbook and calendar * SlowSync uses old mapping information (can be disabled within the preferences) --- addressbook/inc/class.addressbook_bo.inc.php | 171 ++++-- .../inc/class.addressbook_contactform.inc.php | 2 +- addressbook/inc/class.addressbook_sif.inc.php | 54 +- .../inc/class.addressbook_tracking.inc.php | 8 +- .../inc/class.addressbook_vcal.inc.php | 21 +- calendar/inc/class.calendar_boupdate.inc.php | 169 +++--- calendar/inc/class.calendar_ical.inc.php | 125 ++-- calendar/inc/class.calendar_sif.inc.php | 50 +- infolog/inc/class.infolog_bo.inc.php | 541 +++++++++++++----- infolog/inc/class.infolog_ical.inc.php | 517 ++++++++++------- infolog/inc/class.infolog_sif.inc.php | 536 +++++++++-------- 11 files changed, 1374 insertions(+), 820 deletions(-) diff --git a/addressbook/inc/class.addressbook_bo.inc.php b/addressbook/inc/class.addressbook_bo.inc.php index 88fa97840d..51a4c21d2f 100755 --- a/addressbook/inc/class.addressbook_bo.inc.php +++ b/addressbook/inc/class.addressbook_bo.inc.php @@ -18,12 +18,6 @@ */ class addressbook_bo extends addressbook_so { - /** - * @var int $tz_offset_s offset in secconds between user and server-time, - * it need to be add to a server-time to get the user-time or substracted from a user-time to get the server-time - */ - var $tz_offset_s; - /** * @var int $now_su actual user (!) time */ @@ -95,6 +89,13 @@ class addressbook_bo extends addressbook_so var $business_contact_fields = array(); var $home_contact_fields = array(); + /** + * Set Logging + * + * @var boolean + */ + var $log = false; + /** * Number and message of last error or false if no error, atm. only used for saving * @@ -128,7 +129,7 @@ class addressbook_bo extends addressbook_so /** * Tracking changes - * + * * @var object */ protected $tracking; @@ -137,8 +138,7 @@ class addressbook_bo extends addressbook_so { parent::__construct($contact_app); - $this->tz_offset_s = 3600 * $GLOBALS['egw_info']['user']['preferences']['common']['tz_offset']; - $this->now_su = time() + $this->tz_offset_s; + $this->now_su = new egw_time('now'); $this->prefs =& $GLOBALS['egw_info']['user']['preferences']['addressbook']; // get the default addressbook from the users prefs @@ -400,7 +400,7 @@ class addressbook_bo extends addressbook_so */ function set_all_fileas($fileas_type,$all=false,&$errors=null,$ignore_acl=false) { - if ($type != '' && !in_array($type,$this->fileas_types)) + if ($fileas_type != '' && !in_array($fileas_type, $this->fileas_types)) { return false; } @@ -572,15 +572,18 @@ class addressbook_bo extends addressbook_so * This function needs to be reimplemented in the derived class * * @param array $data + * @param $date_format='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in server-time, 'array'=array or string with date-format + * + * @return array updated data */ - function db2data($data) + function db2data($data, $date_format='ts') { // convert timestamps from server-time in the db to user-time - foreach($this->timestamps as $name) + foreach ($this->timestamps as $name) { - if(isset($data[$name])) + if (isset($data[$name])) { - $data[$name] += $this->tz_offset_s; + $data[$name] = egw_time::server2user($data[$name], $date_format); } } $data['photo'] = $this->photo_src($data['id'],$data['jpegphoto']); @@ -618,15 +621,18 @@ class addressbook_bo extends addressbook_so * this needs to be reimplemented in the derived class * * @param array $data + * @param $date_format='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in server-time, 'array'=array or string with date-format + * + * @return array upated data */ - function data2db($data) + function data2db($data, $date_format='ts') { // convert timestamps from user-time to server-time in the db - foreach($this->timestamps as $name) + foreach ($this->timestamps as $name) { - if(isset($data[$name])) + if (isset($data[$name])) { - $data[$name] -= $this->tz_offset_s; + $data[$name] = egw_time::server2user($data[$name], $date_format); } } return $data; @@ -748,8 +754,8 @@ class addressbook_bo extends addressbook_so } // Get old record for tracking changes - $old = $this->read($contact['id']); - + $old = $this->data2db($this->read($contact['id'])); + // we dont update the content-history, if we run inside setup (admin-account-creation) if(!($this->error = parent::save($to_write)) && is_object($GLOBALS['egw']->contenthistory)) { @@ -1308,6 +1314,7 @@ class addressbook_bo extends addressbook_so function merge($ids) { $this->error = false; + $account = null; foreach(parent::search(array('id'=>$ids),false) as $contact) // $this->search calls the extended search from ui! { if ($contact['account_id']) @@ -1661,38 +1668,55 @@ class addressbook_bo extends addressbook_so * * @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) + * @return array od matching contact_ids */ function find_contact($contact, $relax=false) { - if (!empty($contact['uid'])) + if ($this->log) { - // Try the given UID first - Horde::logMessage('Addressbook find UID: '. $contact['uid'], - __FILE__, __LINE__, PEAR_LOG_DEBUG); - $criteria = array ('contact_uid' => $contact['uid']); - if (($found = parent::search($criteria)) - && ($uidmatch = array_shift($found))) - { - return $uidmatch['contact_id']; - } + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '('. ($relax ? 'RELAX': 'EXACT') . ')[ContactData]:' + . array2string($contact)); } - unset($contact['uid']); + $matchingContacts = array(); if ($contact['id'] && ($found = $this->read($contact['id']))) { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[ContactID]: ' . $contact['id']); + } // We only do a simple consistency check - Horde::logMessage('Addressbook find ID: '. $contact['id'], - __FILE__, __LINE__, PEAR_LOG_DEBUG); 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']; + return array($found['id']); } } unset($contact['id']); + if (!$relax && !empty($contact['uid'])) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[ContactUID]: ' . $contact['uid']); + } + // Try the given UID first + $criteria = array ('contact_uid' => $contact['uid']); + if (($foundContacts = parent::search($criteria))) + { + foreach ($foundContacts as $egwContact) + { + $matchingContacts[] = $egwContact['id']; + } + } + return $matchingContacts; + } + unset($contact['uid']); + $columns_to_search = array('n_family', 'n_given', 'n_middle', 'n_prefix', 'n_suffix', 'bday', 'org_name', 'org_unit', 'title', 'role', 'email', 'email_home'); @@ -1813,28 +1837,47 @@ class addressbook_bo extends addressbook_so } } } - - Horde::logMessage("Addressbook find step 1:\n" . print_r($criteria, true) . - "filter:\n" . print_r($filter, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[Addressbook FIND Step 1]: ' + . 'FILTER:' . array2string($filter) + . 'CRITERIA' . array2string($criteria)); + } // first try full match if (($foundContacts = parent::search($criteria, true, '', '', '', False, 'AND', false, $filter))) { - $result = $foundContacts[0]['id']; + foreach ($foundContacts as $egwContact) + { + $matchingContacts[] = $egwContact['id']; + } } // No need for more searches for relaxed matching - if (!$relax && !$result && !$this->all_empty($contact, $addr_one_fields) + if (!$relax || count($matchingContacts)) return $matchingContacts; + + + if (!$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 ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[Addressbook FIND Step 2]: ' + . 'FILTER:' . array2string($filter) + . 'CRITERIA' . array2string($criteria)); + } + if (($foundContacts = parent::search($criteria, true, '', '', '', False, 'AND', false, $filter))) { - $result = $foundContacts[0]['id']; + foreach ($foundContacts as $egwContact) + { + $matchingContacts[] = $egwContact['id']; + } } else { @@ -1859,16 +1902,25 @@ class addressbook_bo extends addressbook_so $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 ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[Addressbook FIND Step 3]: ' + . 'FILTER:' . array2string($filter) + . 'CRITERIA' . array2string($criteria)); + } + if (($foundContacts = parent::search($criteria, true, '', '', '', False, 'AND', false, $filter))) { - $result = $foundContacts[0]['id']; + foreach ($foundContacts as $egwContact) + { + $matchingContacts[] = $egwContact['id']; + } } } } - elseif (!$relax && !$result) - { // No more searches for relaxed matching, try again after address swap + else + { // try again after address swap $filter = $empty_columns; @@ -1907,15 +1959,26 @@ class addressbook_bo extends addressbook_so $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 ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[Addressbook FIND Step 4]: ' + . 'FILTER:' . array2string($filter) + . 'CRITERIA' . array2string($criteria)); + } if(($foundContacts = parent::search($criteria, true, '', '', '', False, 'AND', false, $filter))) { - $result = $foundContacts[0]['id']; + foreach ($foundContacts as $egwContact) + { + $matchingContacts[] = $egwContact['id']; + } } } - return $result; + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[FOUND]:' . array2string($matchingContacts)); + } + return $matchingContacts; } - } diff --git a/addressbook/inc/class.addressbook_contactform.inc.php b/addressbook/inc/class.addressbook_contactform.inc.php index 4ce5ab25ba..66a9005385 100644 --- a/addressbook/inc/class.addressbook_contactform.inc.php +++ b/addressbook/inc/class.addressbook_contactform.inc.php @@ -79,7 +79,7 @@ class addressbook_contactform require_once(EGW_INCLUDE_ROOT.'/addressbook/inc/class.addressbook_tracking.inc.php'); $tracking = new addressbook_tracking($contact); } - if ($tracking->do_notifications($content,null)) + if ($tracking->do_notifications($contact->data2db($content),null)) { return '

'.$content['msg'].'

'; } diff --git a/addressbook/inc/class.addressbook_sif.inc.php b/addressbook/inc/class.addressbook_sif.inc.php index af74004751..3188dde2ad 100644 --- a/addressbook/inc/class.addressbook_sif.inc.php +++ b/addressbook/inc/class.addressbook_sif.inc.php @@ -159,7 +159,7 @@ class addressbook_sif extends addressbook_bo break; default: - $finalContact[$key] = $value; + $finalContact[$key] = str_replace("\r\n", "\n", $value); break; } } @@ -173,18 +173,20 @@ class addressbook_sif extends addressbook_bo * Search an exactly matching entry (used for slow sync) * * @param string $_sifdata - * @return boolean/int/string contact-id or false, if not found + * @return array of matching contact-ids */ function search($_sifdata, $contentID=null, $relax=false) { - $result = false; + $result = array(); - if($contact = $this->siftoegw($_sifdata, $contentID)) { - if ($contentID) { - $contact['contact_id'] = $contentID; - } - $result = $this->find_contact($contact, $relax); - } + if($contact = $this->siftoegw($_sifdata, $contentID)) + { + if ($contentID) + { + $contact['contact_id'] = $contentID; + } + $result = $this->find_contact($contact, $relax); + } return $result; } @@ -198,22 +200,44 @@ class addressbook_sif extends addressbook_bo */ function addSIF($_sifdata, $_abID=null, $merge=false) { - #error_log('ABID: '.$_abID); - #error_log(base64_decode($_sifdata)); - - if(!$contact = $this->siftoegw($_sifdata, $_abID)) { + if(!$contact = $this->siftoegw($_sifdata, $_abID)) + { return false; } - if($_abID) { + if($_abID) + { + if (($old_contact = $this->read($_abID))) + { + if ($merge) + { + foreach ($contact as $key => $value) + { + if (!empty($old_contact[$key])) + { + $contact[$key] = $old_contact[$key]; + } + } + } + else + { + if (isset($old_contact['account_id'])) + { + $contact['account_id'] = $old_contact['account_id']; + } + } + } // update entry $contact['id'] = $_abID; } elseif (array_key_exists('filter_addressbook', $GLOBALS['egw_info']['user']['preferences']['syncml'])) { $contact['owner'] = (int) $GLOBALS['egw_info']['user']['preferences']['syncml']['filter_addressbook']; + if ($contact['owner'] == -1) + { + $contact['owner'] = $GLOBALS['egw_info']['user']['account_primary_group']; + } } - return $this->save($contact); } diff --git a/addressbook/inc/class.addressbook_tracking.inc.php b/addressbook/inc/class.addressbook_tracking.inc.php index dc82573a17..9af22ff8a6 100644 --- a/addressbook/inc/class.addressbook_tracking.inc.php +++ b/addressbook/inc/class.addressbook_tracking.inc.php @@ -72,7 +72,8 @@ class addressbook_tracking extends bo_tracking $this->contacts =& $bocontacts; - if(is_object($bocontacts->somain)) { + if (is_object($bocontacts->somain)) + { $this->field2history = array_combine($bocontacts->somain->db_cols, $bocontacts->somain->db_cols); unset($this->field2history['modified']); unset($this->field2history['modifier']); @@ -156,8 +157,7 @@ class addressbook_tracking extends bo_tracking * Get the details of an entry * * @param array $data - * @param string $datetime_format of user to notify, eg. 'Y-m-d H:i' - * @param int $tz_offset_s offset in sec to be add to server-time to get the user-time of the user to notify + * * @return array of details as array with values for keys 'label','value','type' */ function get_details($data) @@ -241,4 +241,4 @@ class addressbook_tracking extends bo_tracking } return $details; } -} +} \ No newline at end of file diff --git a/addressbook/inc/class.addressbook_vcal.inc.php b/addressbook/inc/class.addressbook_vcal.inc.php index 4723c7bcf5..b230c0148d 100644 --- a/addressbook/inc/class.addressbook_vcal.inc.php +++ b/addressbook/inc/class.addressbook_vcal.inc.php @@ -130,10 +130,9 @@ class addressbook_vcal extends addressbook_bo if($_abID) { - if ($merge) + if (($old_contact = $this->read($_abID))) { - $old_contact = $this->read($_abID); - if ($old_contact) + if ($merge) { foreach ($contact as $key => $value) { @@ -143,6 +142,13 @@ class addressbook_vcal extends addressbook_bo } } } + else + { + if (isset($old_contact['account_id'])) + { + $contact['account_id'] = $old_contact['account_id']; + } + } } // update entry $contact['id'] = $_abID; @@ -150,6 +156,10 @@ class addressbook_vcal extends addressbook_bo elseif (array_key_exists('filter_addressbook', $GLOBALS['egw_info']['user']['preferences']['syncml'])) { $contact['owner'] = (int) $GLOBALS['egw_info']['user']['preferences']['syncml']['filter_addressbook']; + if ($contact['owner'] == -1) + { + $contact['owner'] = $GLOBALS['egw_info']['user']['account_primary_group']; + } } return $this->save($contact); } @@ -443,7 +453,7 @@ class addressbook_vcal extends addressbook_bo function search($_vcard, $contentID=null, $relax=false) { - $result = false; + $result = array(); if (($contact = $this->vcardtoegw($_vcard, $contentID))) { @@ -887,8 +897,7 @@ class addressbook_vcal extends addressbook_bo break; case 'note': - // note may contain ','s but maybe this needs to be fixed in vcard parser... - $contact[$fieldName] = $vcardValues[$vcardKey]['value']; + $contact[$fieldName] = str_replace("\r\n", "\n", $vcardValues[$vcardKey]['value']); break; case 'uid': diff --git a/calendar/inc/class.calendar_boupdate.inc.php b/calendar/inc/class.calendar_boupdate.inc.php index 4d308baa0f..b3be9f0628 100644 --- a/calendar/inc/class.calendar_boupdate.inc.php +++ b/calendar/inc/class.calendar_boupdate.inc.php @@ -67,6 +67,13 @@ class calendar_boupdate extends calendar_bo */ var $log = false; + /** + * Cached timezone data + * + * @var array id => data + */ + protected static $tz_cache = array(); + /** * Constructor */ @@ -1417,7 +1424,7 @@ class calendar_boupdate extends calendar_bo * @param string filter='exact' exact -> check for identical matches * relax -> be more tolerant * master -> try to find a releated series master - * @return array calendar_id's of matching entries + * @return array calendar_ids of matching entries */ function find_event($event, $filter='exact') { @@ -1432,23 +1439,21 @@ class calendar_boupdate extends calendar_bo if ($filter == 'master') { - if (empty($event['uid']) || !isset($event['recurrence'])) - { - $recur_date = $event['start']; // without UID we ignore RECURRENCE-IDs - } - else + if (isset($event['recurrence'])) { $recur_date = $event['recurrence']; } + else + { + $recur_date = $event['start']; + } + $recur_date = $this->date2usertime($recur_date); } else { - $recur_date = $event['start']; + $recur_date = 0; } - $recur_date = $this->date2usertime($recur_date); - - if ($event['id']) { if ($this->log) @@ -1465,9 +1470,7 @@ class calendar_boupdate extends calendar_bo } // Just a simple consistency check if ($filter == 'master' && $egwEvent['recur_type'] != MCAL_RECUR_NONE || - $egwEvent['title'] == $event['title'] && - abs($egwEvent['start'] - $event['start']) < 60 && - abs($egwEvent['end'] - $event['end']) < 120) + strpos($egwEvent['title'], $event['title']) === 0) { $retval = $egwEvent['id']; if ($egwEvent['recur_type'] != MCAL_RECUR_NONE && @@ -1483,41 +1486,6 @@ class calendar_boupdate extends calendar_bo } unset($event['id']); - if (isset($event['whole_day']) && $event['whole_day']) - { - if ($filter == 'relax') - { - $delta = 1800; - } - else - { - $delta = 60; - } - - // check length with some tolerance - $length = $event['end'] - $event['start'] - $delta; - $query[] = ('(cal_end-cal_start)>' . $length); - $length += 2 * $delta; - $query[] = ('(cal_end-cal_start)<' . $length); - } - elseif (isset($event['start'])) - { - if ($filter != 'master') - { - if ($filter == 'relax') - { - $query[] = ('cal_start>' . ($event['start'] - 3600)); - $query[] = ('cal_start<' . ($event['start'] + 3600)); - } - else - { - // we accept a tiny tolerance - $query[] = ('cal_start>' . ($event['start'] - 2)); - $query[] = ('cal_start<' . ($event['start'] + 2)); - } - } - } - if ($filter == 'master') { $query[] = 'recur_type!='. MCAL_RECUR_NONE; @@ -1574,20 +1542,45 @@ class calendar_boupdate extends calendar_bo } } } - if (empty($event['uid'])) + if ($filter == 'relax' || empty($event['uid'])) { - $matchFields = array('title', 'priority', 'public', 'non_blocking'); - switch ($filter) + if (isset($event['whole_day']) && $event['whole_day']) { - case 'relax': - $matchFields[] = 'location'; - case 'master': - break; - default: - $matchFields[] = 'description'; - $matchFields[] = 'location'; - } + if ($filter == 'relax') + { + $delta = 1800; + } + else + { + $delta = 60; + } + // check length with some tolerance + $length = $event['end'] - $event['start'] - $delta; + $query[] = ('(cal_end-cal_start)>' . $length); + $length += 2 * $delta; + $query[] = ('(cal_end-cal_start)<' . $length); + $query[] = ('cal_start>' . ($event['start'] - 86400)); + $query[] = ('cal_start<' . ($event['start'] + 86400)); + } + elseif (isset($event['start'])) + { + if ($filter != 'master') + { + if ($filter == 'relax') + { + $query[] = ('cal_start>' . ($event['start'] - 3600)); + $query[] = ('cal_start<' . ($event['start'] + 3600)); + } + else + { + // we accept a tiny tolerance + $query[] = ('cal_start>' . ($event['start'] - 2)); + $query[] = ('cal_start<' . ($event['start'] + 2)); + } + } + } + $matchFields = array('priority', 'public', 'non_blocking', 'recurrence'); foreach ($matchFields as $key) { if (!empty($event[$key])) $query['cal_'.$key] = $event[$key]; @@ -1601,7 +1594,7 @@ class calendar_boupdate extends calendar_bo error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. '(' . $event['uid'] . ')[EventUID]'); } - if ($filter != 'relax' && isset($event['recurrence'])) + if (isset($event['recurrence'])) { $query['cal_recurrence'] = $event['recurrence']; } @@ -1635,6 +1628,12 @@ class calendar_boupdate extends calendar_bo '[FOUND]: ' . array2string($egwEvent)); } + if ($filter == 'exact' && !empty($event['uid'])) + { + $matchingEvents[] = $egwEvent['id']; // UID found + continue; + } + // convert timezone id of event to tzid (iCal id like 'Europe/Berlin') if (!$egwEvent['tz_id'] || !($egwEvent['tzid'] = calendar_timezones::id2tz($egwEvent['tz_id']))) { @@ -1671,10 +1670,35 @@ class calendar_boupdate extends calendar_bo } } - if ($filter != 'master') + // check for real match + $matchFields = array('title'); + switch ($filter) + { + case 'master': + break; + case 'relax': + $matchFields[] = 'location'; + default: + $matchFields[] = 'description'; + } + foreach ($matchFields as $key) + { + if (!empty($event[$key]) && (empty($egwEvent[$key]) + || strpos($egwEvent[$key], $event[$key]) !== 0)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "() event[$key] differ: '" . $event[$key] . + "' <> '" . $egwEvent[$key]) . "'"; + } + continue 2; // next foundEvent + } + } + + if ($filter != 'master' && is_array($event['category'])) { // check categories - if (!is_array($event['category'])) $event['category'] = array(); $egwCategories = explode(',', $egwEvent['category']); foreach ($egwCategories as $cat_id) { @@ -1700,24 +1724,6 @@ class calendar_boupdate extends calendar_bo continue; } } - /* - // check information - $matchFields = array(); - foreach ($matchFields as $key) - { - if (isset($event[$key]) - && $event[$key] != $egwEvent[$key]) - { - if ($this->log) - { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. - "() events[$key] differ: " . $event[$key] . - ' <> ' . $egwEvent[$key]); - } - continue 2; // next foundEvent - } - } - */ if ($filter != 'relax' && $filter != 'master') { @@ -1924,6 +1930,7 @@ class calendar_boupdate extends calendar_bo in_array($event['recurrence'], $master_event['recur_exception'])) { $type = 'SERIES-PSEUDO-EXCEPTION'; // could also be a real one + $recurrence_event = $event; break; } elseif (in_array($event['start'], $master_event['recur_exception'])) @@ -2028,7 +2035,7 @@ class calendar_boupdate extends calendar_bo ); } - /* + /** * Translates all timestamps for a given event from server-time to user-time. * The update() and save() methods expect timestamps in user-time. * @param &$event the event we are working on diff --git a/calendar/inc/class.calendar_ical.inc.php b/calendar/inc/class.calendar_ical.inc.php index 2a6ca28545..5ab0a284d0 100644 --- a/calendar/inc/class.calendar_ical.inc.php +++ b/calendar/inc/class.calendar_ical.inc.php @@ -128,13 +128,6 @@ class calendar_ical extends calendar_boupdate */ var $tzid = null; - /** - * Cached timezone data - * - * @var array id => data - */ - protected static $tz_cache = array(); - /** * Device CTCap Properties * @@ -257,7 +250,7 @@ class calendar_ical extends calendar_boupdate // explicit device timezone $tzid = $this->tzid; } - else + elseif ($this->tzid === false) { // use event's timezone $tzid = $event['tzid']; @@ -334,6 +327,7 @@ class calendar_ical extends calendar_boupdate { error_log(__METHOD__."() unknown TZID='$tzid', defaulting to user timezone '".egw_time::$user_timezone->getName()."'!"); $vtimezone = calendar_timezones::tz2id($tzid=egw_time::$user_timezone->getName(),'component'); + $tzid = null; } if (!isset(self::$tz_cache[$tzid])) { @@ -525,7 +519,6 @@ class calendar_ical extends calendar_boupdate foreach (array('start' => 'DTSTART','end-nextday' => 'DTEND') as $f => $t) { $time = new egw_time($event[$f],egw_time::$server_timezone); - $time->setTimezone(self::$tz_cache[$event['tzid']]); $arr = egw_time::to($time,'array'); $vevent->setAttribute($t, array('year' => $arr['year'],'month' => $arr['month'],'mday' => $arr['day']), array('VALUE' => 'DATE')); @@ -1005,6 +998,10 @@ class calendar_ical extends calendar_boupdate { $event['uid'] = $event_info['stored_event']['uid']; // restore the UID if it was not delivered } + elseif (empty($event['id'])) + { + $event['id'] = $event_info['stored_event']['id']; // CalDAV does only provide UIDs + } if ($merge) { if ($this->log) @@ -1246,7 +1243,6 @@ class calendar_ical extends calendar_boupdate if ($event_info['acl_edit']) { // Force SINGLE - unset($event['recurrence']); $event['reference'] = 0; $event_to_store = $event; // prevent $event from being changed by the update method $this->server2usertime($event_to_store); @@ -1515,9 +1511,8 @@ class calendar_ical extends calendar_boupdate if ($this->log) { - $recur_date = $this->date2usertime($event_info['stored_event']['start']); - $event_info['stored_event'] = $this->read($event_info['stored_event']['id'], $recur_date); - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + $event_info['stored_event'] = $this->read($event_info['stored_event']['id']); + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()[$updated_id]\n" . array2string($event_info['stored_event'])."\n",3,$this->logfile); } } @@ -1630,11 +1625,25 @@ class calendar_ical extends calendar_boupdate if (isset($deviceInfo['tzid']) && $deviceInfo['tzid']) { - $this->tzid = $deviceInfo['tzid']; + switch ($deviceInfo['tzid']) + { + case 1: + $this->tzid = false; + break; + case 2: + $this->tzid = null; + break; + default: + $this->tzid = $deviceInfo['tzid']; + } } if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['calendar_owner'])) { $owner = $GLOBALS['egw_info']['user']['preferences']['syncml']['calendar_owner']; + if ($owner == 0) + { + $owner = $GLOBALS['egw_info']['user']['account_primary_group']; + } if (0 < (int)$owner && $this->check_perms(EGW_ACL_EDIT,0,$owner)) { $this->calendarOwner = $owner; @@ -1656,11 +1665,15 @@ class calendar_ical extends calendar_boupdate { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. '(' . $this->productManufacturer . - ', '. $this->productName . ")\n",3,$this->logfile); + ', '. $this->productName .', ' . + ($this->tzid ? $this->tzid : egw_time::$user_timezone->getName()) . + ")\n" , 3, $this->logfile); } - //Horde::logMessage('setSupportedFields(' . $this->productManufacturer - // . ', ' . $this->productName .')', __FILE__, __LINE__, PEAR_LOG_DEBUG); + //Horde::logMessage('setSupportedFields(' . $this->productManufacturer . ', ' + // . $this->productName .', ' . + // ($this->tzid ? $this->tzid : egw_time::$user_timezone->getName()) .')', + // __FILE__, __LINE__, PEAR_LOG_DEBUG); $defaultFields['minimal'] = array( 'public' => 'public', @@ -1863,8 +1876,14 @@ class calendar_ical extends calendar_boupdate if ($this->tzid) { - date_default_timezone_set($this->tzid); + $tzid = $this->tzid; } + else + { + $tzid = egw_time::$user_timezone->getName(); + } + + date_default_timezone_set($tzid); $vcal = new Horde_iCalendar; if (!$vcal->parsevCalendar($_vcalData)) @@ -1912,20 +1931,21 @@ class calendar_ical extends calendar_boupdate } } $event['alarm'] = $alarms; + if ($this->tzid || empty($event['tzid'])) + { + $event['tzid'] = $tzid; + } $events[] = $event; } } } - if ($this->tzid) - { - date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); - } + date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); // if cal_id, etag or recur_date is given, use/set it for 1. event if ($cal_id > 0) $events[0]['id'] = $cal_id; - if (!is_null($etag)) $events[0]['etag'] = $etag; + if (!is_null($etag)) $events[0]['etag'] = (int) $etag; if ($recur_date) $events[0]['recurrence'] = $recur_date; return $events; @@ -1974,32 +1994,34 @@ class calendar_ical extends calendar_boupdate $dtstart_ts = is_numeric($attributes['value']) ? $attributes['value'] : $this->date2ts($attributes['value']); $vcardData['start'] = $dtstart_ts; - if ($this->tzid) { - // enforce device settings $event['tzid'] = $this->tzid; } - elseif (!empty($attributes['params']['TZID'])) - { - // import TZID, if PHP understands it (we only care about TZID of starttime, as we store only a TZID for the whole event) - try - { - $tz = calendar_timezones::DateTimeZone($attributes['params']['TZID']); - $event['tzid'] = $tz->getName(); - } - catch(Exception $e) - { - error_log(__METHOD__."() unknown TZID='{$attributes['params']['TZID']}', defaulting to user timezone '".egw_time::$user_timezone->getName()."'!"); - $tz = egw_time::$user_timezone; - $event['tzid'] = egw_time::$user_timezone->getName(); // default to user timezone - } - } else { - $event['tzid'] = egw_time::$user_timezone->getName(); // default to user timezone + $event['tzid'] = date_default_timezone_get(); + + if (!empty($attributes['params']['TZID'])) + { + // import TZID, if PHP understands it (we only care about TZID of starttime, + // as we store only a TZID for the whole event) + try + { + $tz = calendar_timezones::DateTimeZone($attributes['params']['TZID']); + $event['tzid'] = $tz->getName(); + } + catch(Exception $e) + { + error_log(__METHOD__ . '() unknown TZID=' + . $attributes['params']['TZID'] . ', defaulting to timezone "' + . date_default_timezone_get() . '".'); + $event['tzid'] = date_default_timezone_get(); // default to current timezone + } + } } break; + case 'DTEND': $dtend_ts = is_numeric($attributes['value']) ? $attributes['value'] : $this->date2ts($attributes['value']); if (date('H:i:s',$dtend_ts) == '00:00:00') @@ -2028,7 +2050,6 @@ class calendar_ical extends calendar_boupdate break; case 'DESCRIPTION': $vcardData['description'] = str_replace("\r\n", "\n", $attributes['value']); - $vcardData['description'] = str_replace("\r", "\n", $vcardData['description']); if (preg_match('/\s*\[UID:(.+)?\]/Usm', $attributes['value'], $matches)) { if (!isset($vCardData['uid']) @@ -2044,7 +2065,6 @@ class calendar_ical extends calendar_boupdate break; case 'LOCATION': $vcardData['location'] = str_replace("\r\n", "\n", $attributes['value']); - $vcardData['location'] = str_replace("\r", "\n", $vcardData['location']); break; case 'RRULE': $recurence = $attributes['value']; @@ -2301,7 +2321,6 @@ class calendar_ical extends calendar_boupdate break; case 'SUMMARY': $vcardData['title'] = str_replace("\r\n", "\n", $attributes['value']); - $vcardData['title'] = str_replace("\r", "\n", $vcardData['title']); break; case 'UID': if (strlen($attributes['value']) >= $minimum_uid_length) @@ -2457,11 +2476,16 @@ class calendar_ical extends calendar_boupdate { $attributes['params']['ROLE'] = 'CHAIR'; } - // add quantity and role - $event['participants'][$uid] = - calendar_so::combine_status($status, - $attributes['params']['X-EGROUPWARE-QUANTITY'], - $attributes['params']['ROLE']); + if (!isset($event['participants'][$uid]) || + $event['participants'][$uid][0] != 'A') + { + // for multiple entries the ACCEPT wins + // add quantity and role + $event['participants'][$uid] = + calendar_so::combine_status($status, + $attributes['params']['X-EGROUPWARE-QUANTITY'], + $attributes['params']['ROLE']); + } break; case 'ORGANIZER': @@ -2575,6 +2599,8 @@ class calendar_ical extends calendar_boupdate if ($this->calendarOwner) $event['owner'] = $this->calendarOwner; + if ($cal_id > 0) $event['id'] = $cal_id; + if ($this->log) { error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . @@ -2595,6 +2621,7 @@ class calendar_ical extends calendar_boupdate $filter = $relax ? 'relax' : 'exact'; $event = array_shift($events); if ($this->so->isWholeDay($event)) $event['whole_day'] = true; + if ($contentID) $event['id'] = $contentID; return $this->find_event($event, $filter); } if ($this->log) diff --git a/calendar/inc/class.calendar_sif.inc.php b/calendar/inc/class.calendar_sif.inc.php index be1d432d62..d5c70c8942 100644 --- a/calendar/inc/class.calendar_sif.inc.php +++ b/calendar/inc/class.calendar_sif.inc.php @@ -96,13 +96,6 @@ class calendar_sif extends calendar_boupdate */ var $tzid = null; - /** - * Cached timezone data - * - * @var array id => data - */ - protected static $tz_cache = array(); - /** * Device CTCap Properties * @@ -211,7 +204,7 @@ class calendar_sif extends calendar_boupdate } $time->setTimezone(self::$tz_cache[$tzid]); - return $this->vCalendar->_exportDateTime($time->format('Ymd\THis')); + return $time->format('Ymd\THis'); } function siftoegw($sifData, $_calID=-1) @@ -220,13 +213,18 @@ class calendar_sif extends calendar_boupdate $this->event = array(); $sysCharSet = $GLOBALS['egw']->translation->charset(); + if ($this->tzid) { - // enforce device settings - date_default_timezone_set($this->tzid); - $finalEvent['tzid'] = $this->tzid; + $tzid = $this->tzid; + } + else + { + $tzid = egw_time::$user_timezone->getName(); } + date_default_timezone_set($tzid); + $finalEvent['tzid'] = $tzid; $this->xml_parser = xml_parser_create('UTF-8'); xml_set_object($this->xml_parser, $this); @@ -239,10 +237,7 @@ class calendar_sif extends calendar_boupdate error_log(sprintf("XML error: %s at line %d", xml_error_string(xml_get_error_code($this->xml_parser)), xml_get_current_line_number($this->xml_parser))); - if ($this->tzid) - { - date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); - } + date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); return false; } @@ -379,17 +374,14 @@ class calendar_sif extends calendar_boupdate } default: - $finalEvent[$key] = $value; + $finalEvent[$key] = str_replace("\r\n", "\n", $value); break; } } if ($this->calendarOwner) $finalEvent['owner'] = $this->calendarOwner; - if ($this->tzid) - { - date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); - } + date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); if ($_calID > 0) $finalEvent['id'] = $_calID; @@ -862,7 +854,7 @@ class calendar_sif extends calendar_boupdate // explicit device timezone $tzid = $this->tzid; } - else + elseif ($this->tzid === false) { // use event's timezone $tzid = $event['tzid']; @@ -1203,11 +1195,25 @@ class calendar_sif extends calendar_boupdate if (isset($deviceInfo['tzid']) && $deviceInfo['tzid']) { - $this->tzid = $deviceInfo['tzid']; + switch ($deviceInfo['tzid']) + { + case 1: + $this->tzid = false; + break; + case 2: + $this->tzid = null; + break; + default: + $this->tzid = $deviceInfo['tzid']; + } } if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['calendar_owner'])) { $owner = $GLOBALS['egw_info']['user']['preferences']['syncml']['calendar_owner']; + if ($owner == 0) + { + $owner = $GLOBALS['egw_info']['user']['account_primary_group']; + } if (0 < (int)$owner && $this->check_perms(EGW_ACL_EDIT,0,$owner)) { $this->calendarOwner = $owner; diff --git a/infolog/inc/class.infolog_bo.inc.php b/infolog/inc/class.infolog_bo.inc.php index 265def164c..819dcbbd72 100644 --- a/infolog/inc/class.infolog_bo.inc.php +++ b/infolog/inc/class.infolog_bo.inc.php @@ -31,6 +31,21 @@ class infolog_bo var $link_pathes = array(); var $send_file_ips = array(); + /** + * Set Logging + * + * @var boolean + */ + var $log = false; + + + /** + * Cached timezone data + * + * @var array id => data + */ + protected static $tz_cache = array(); + /** * current time as timestamp in user-time and server-time * @@ -403,6 +418,85 @@ class infolog_bo return substr($des,0,60).' ...'; } + /** + * Convert the timestamps from given timezone to another and keep dates. + * The timestamps are mostly expected to be in server-time + * and $fromTZId is only used to qualify dates. + * + * @param array $values to modify + * @param string $fromTZId=null + * @param string $toTZId=false + * TZID timezone name e.g. 'UTC' + * or NULL for timestamps in user-time + * or false for timestamps in server-time + */ + function time2time(&$values, $fromTZId=false, $toTZId=null) + { + + if ($fromTZId === $toTZId) return; + + $tz = egw_time::$server_timezone; + + if ($fromTZId) + { + if (!isset(self::$tz_cache[$fromTZId])) + { + self::$tz_cache[$fromTZId] = calendar_timezones::DateTimeZone($fromTZId); + } + $fromTZ = self::$tz_cache[$fromTZId]; + } + elseif (is_null($fromTZId)) + { + $tz = egw_time::$user_timezone; + $fromTZ = egw_time::$user_timezone; + } + else + { + $fromTZ = egw_time::$server_timezone; + } + if ($toTZId) + { + if (!isset(self::$tz_cache[$toTZId])) + { + self::$tz_cache[$toTZId] = calendar_timezones::DateTimeZone($toTZId); + } + $toTZ = self::$tz_cache[$toTZId]; + } + elseif (is_null($toTZId)) + { + $toTZ = egw_time::$user_timezone; + } + else + { + $toTZ = egw_time::$server_timezone; + } + + foreach($this->timestamps as $key) + { + if ($values[$key]) + { + $time = new egw_time($values[$key], $tz); + $time->setTimezone($fromTZ); + if ($key == 'info_enddate') + { + // Set due date to 00:00 + $time->setTime(0, 0, 0); + } + if ($time->format('Hi') == '0000') + { + // we keep dates the same in new timezone + $arr = egw_time::to($time,'array'); + $time = new egw_time($arr, $toTZ); + } + else + { + $time->setTimezone($toTZ); + } + $values[$key] = egw_time::to($time,'ts'); + } + } + } + /** * convert a date from server to user-time * @@ -452,30 +546,21 @@ class infolog_bo } if ($run_link_id2from) $this->link_id2from($data); - if ($data['info_enddate']) - { - // Set due date to 00:00 - $due = new egw_time($data['info_enddate'], egw_time::$server_timezone); - $due->setTime(0, 0, 0); - if ($date_format != 'server') - { - $arr = egw_time::to($due,'array'); - $due = new egw_time($arr, egw_time::$user_timezone); - } - $data['info_enddate'] = egw_time::to($due,'server'); - } - - // convert system- to user-time - foreach($this->timestamps as $time) - { - if ($data[$time]) $data[$time] = $this->date2usertime($data[$time],$date_format); - } - + // convert server- to user-time if ($date_format == 'ts') { + $this->time2time($data); + // pre-cache title and file access self::set_link_cache($data); } + else + { + $time = new egw_time($data['info_enddate'], egw_time::$server_timezone); + // Set due date to 00:00 + $time->setTime(0, 0,0 ); + $data['info_enddate'] = egw_time::to($time,'ts'); + } return $data; } @@ -520,7 +605,7 @@ class infolog_bo } } } - if (!($info = $this->read($info_id, true, 'server'))) return false; // should not happen + if (!($info = $this->read($info_id, true, 'server'))) return false; // should not happen $deleted = $info; $deleted['info_status'] = 'deleted'; @@ -696,37 +781,21 @@ class infolog_bo $to_write = $values; if ($user2server) { - // convert user- to system-time - foreach($this->timestamps as $time) - { - if ($to_write[$time]) $to_write[$time] = egw_time::user2server($to_write[$time],'ts'); - } - if ($values['info_enddate']) - { - // Set due date to 00:00 - $due = new egw_time($values['info_enddate'], egw_time::$user_timezone); - $due->setTime(0, 0, 0); - $values['info_enddate'] = egw_time::to($due,'ts'); - $arr = egw_time::to($due,'array'); - $to_write['info_enddate'] = mktime(0, 0, 0, $arr['month'], $arr['day'], $arr['year']); - } + // convert user- to server-time + $this->time2time($to_write, null, false); + $time = new egw_time($values['info_enddate'], egw_time::$user_timezone); + // Set due date to 00:00 + $time->setTime(0, 0, 0); + $values['info_enddate'] = egw_time::to($time,'ts'); } else { + $time = new egw_time($values['info_enddate'], egw_time::$user_timezone); + // Set due date to 00:00 + $time->setTime(0, 0, 0); + $to_write['info_enddate'] = egw_time::to($time,'ts'); // convert server- to user-time - foreach($this->timestamps as $time) - { - if ($values[$time]) $values[$time] = egw_time::server2user($values[$time],'ts'); - } - if ($to_write['info_enddate']) - { - $due = new egw_time($to_write['info_enddate'], egw_time::$server_timezone); - $due->setTime(0, 0, 0); - $to_write['info_enddate'] = egw_time::to($due,'ts'); - $arr = egw_time::to($due,'array'); - $due = new egw_time($arr, egw_time::$user_timezone); - $values['info_enddate'] = egw_time::to($due,'ts'); - } + $this->time2time($values); } if ($touch_modified || !$values['info_datemodified']) @@ -776,7 +845,7 @@ class infolog_bo } $values['info_id'] = $info_id; $to_write['info_id'] = $info_id; - // if the info responsible array is not passed, fetch it from old. + // if the info responbsible array is not passed, fetch it from old. if (!array_key_exists('info_responsible',$values)) $values['info_responsible'] = $old['info_responsible']; if (!is_array($values['info_responsible'])) // this should not happen, bug it does ;-) { @@ -843,25 +912,35 @@ class infolog_bo } $ret = $this->so->search($query); - // convert system- to user-time if (is_array($ret)) { foreach($ret as $id => &$data) { - if ($data['info_enddate']) + if (!$this->check_access($data,EGW_ACL_READ)) { - // Set due date to 00:00 in user-time - $due = new egw_time($data['info_enddate'], egw_time::$server_timezone); - $due->setTime(0, 0, 0); - $arr = egw_time::to($due,'array'); - $due = new egw_time($arr, egw_time::$user_timezone); - $data['info_enddate'] = egw_time::to($due,'server'); + unset($ret[$id]); + continue; } - foreach($this->timestamps as $time) + // convert system- to user-time + foreach($this->timestamps as $key) { - if ($data[$time]) $data[$time] = $this->date2usertime($data[$time]); + if ($data[$key]) + { + $time = new egw_time($data[$key], egw_time::$server_timezone); + if ($key == 'info_enddate') $time->setTime(0, 0,0 ); // Set due date to 00:00 + if ($time->format('Hi') == '0000') + { + // we keep dates the same in user-time + $arr = egw_time::to($time,'array'); + $time = new egw_time($arr, egw_time::$user_timezone); + } + else + { + $time->setTimezone(egw_time::$user_timezone); + } + $data[$key] = egw_time::to($time,'ts'); + } } - // pre-cache title and file access self::set_link_cache($data); } @@ -1177,13 +1256,12 @@ class infolog_bo { $this->categories = new categories($this->user,'infolog'); } - - if($info_id && $info_id > 0) + $old_cats_preserve = array(); + if ($info_id && $info_id > 0) { // preserve categories without users read access $old_infolog = $this->read($info_id); $old_categories = explode(',',$old_infolog['info_cat']); - $old_cats_preserve = array(); if(is_array($old_categories) && count($old_categories) > 0) { foreach($old_categories as $cat_id) @@ -1197,7 +1275,7 @@ class infolog_bo } $cat_id_list = array(); - foreach($catname_list as $cat_name) + foreach ($catname_list as $cat_name) { $cat_name = trim($cat_name); $cat_id = $this->categories->name2id($cat_name, 'X-'); @@ -1218,7 +1296,7 @@ class infolog_bo } } - if(is_array($old_cats_preserve) && count($old_cats_preserve) > 0) + if (count($old_cats_preserve) > 0) { $cat_id_list = array_merge($old_cats_preserve, $cat_id_list); } @@ -1448,74 +1526,158 @@ class infolog_bo /** * Try to find a matching db entry - * This method is working completely in server-time and - * expects all time entriey to be 00:00 normalized. + * This expects timestamps to be in server-time. * - * @param array $egwData the vTODO data we try to find + * @param array $infoData the infolog data we try to find * @param boolean $relax=false if asked to relax, we only match against some key fields - * @return the infolog_id of the matching entry or false (if none matches) + * @param string $tzid=null timezone, null => user time + * + * @return array of infolog_ids of matching entries */ - function findVTODO($egwData, $relax=false) + function findInfo($infoData, $relax=false, $tzid=null) { - if (!empty($egwData['info_uid'])) - { - $filter = array('col_filter' => array('info_uid' => $egwData['info_uid'])); - if (($found = $this->so->search($filter)) - && ($uidmatch = array_shift($found))) - { - return $uidmatch['info_id']; - } - } - unset($egwData['info_uid']); - + $foundInfoLogs = array(); $filter = array(); - $description = ''; - if (!empty($egwData['info_des'])) { - $description = trim(preg_replace("/\r?\n?\\[[A-Z_]+:.*\\]/i", '', $egwData['info_des'])); - unset($egwData['info_des']); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '('. ($relax ? 'RELAX, ': 'EXACT, ') . $tzid . ')[InfoData]:' + . array2string($infoData)); + } + + if ($infoData['info_id'] + && ($egwData = $this->read($infoData['info_id'], true, 'server'))) + { + // we only do a simple consistency check + if (strpos($egwData['info_subject'], $infoData['info_subject']) === 0) + { + return array($egwData['info_id']); + } + if (!$relax) return array(); + } + unset($infoData['info_id']); + + if (!$relax && !empty($infoData['info_uid'])) + { + $filter = array('col_filter' => array('info_uid' => $infoData['info_uid'])); + foreach($this->so->search($filter) as $egwData) + { + if (!$this->check_access($egwData,EGW_ACL_READ)) continue; + $foundInfoLogs[$egwData['info_id']] = $egwData['info_id']; + } + return $foundInfoLogs; + } + unset($infoData['info_uid']); + + if (empty($infoData['info_des'])) + { + $description = false; + } + else + { + // ignore meta information appendices + $description = trim(preg_replace('/\s*\[[A-Z_]+:.*\].*/im', '', $infoData['info_des'])); + $text = trim(preg_replace('/\s*\[[A-Z_]+:.*\]/im', '', $infoData['info_des'])); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . "()[description]: $description"); + } // Avoid quotation problems - $description = preg_replace("/[^\x20-\x7F].*/", '', $description); - if (strlen($description)) { - $filter['search'] = $description; - } - } - - if ($egwData['info_id'] - && ($found = $this->read($egwData['info_id']))) - { - // We only do a simple consistency check - if ($found['info_subject'] == $egwData['info_subject'] - && strpos($found['info_des'], $description) === 0) + if (preg_match_all('/[\x20-\x7F]*/m', $text, $matches, PREG_SET_ORDER)) { - return $found['info_id']; + $text = ''; + foreach ($matches as $chunk) + { + if (strlen($text) < strlen($chunk[0])) + { + $text = $chunk[0]; + } + } + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . "()[search]: $text"); + } + $filter['search'] = $text; } } - unset($egwData['info_id']); + $this->time2time($infoData, $tzid, false); + $filter['col_filter'] = $infoData; // priority does not need to match - unset($egwData['info_priority']); + unset($filter['col_filter']['info_priority']); + // we ignore description and location first + unset($filter['col_filter']['info_des']); + unset($filter['col_filter']['info_location']); - $filter['col_filter'] = $egwData; + foreach ($this->so->search($filter) as $itemID => $egwData) + { + if (!$this->check_access($egwData,EGW_ACL_READ)) continue; - if($foundItems = $this->so->search($filter)) { - if(count($foundItems) > 0) { - $itemIDs = array_keys($foundItems); - return $itemIDs[0]; + switch ($infoData['info_type']) + { + case 'task': + if (!empty($egwData['info_location'])) + { + $egwData['info_location'] = str_replace("\r\n", "\n", $egwData['info_location']); + } + if (!$relax && + !empty($infoData['info_location']) && (empty($egwData['info_location']) + || strpos($egwData['info_location'], $infoData['info_location']) !== 0)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[location mismatch]: ' + . $infoData['info_location'] . ' <> ' . $egwData['info_location']); + } + continue; + } + default: + if (!empty($egwData['info_des'])) + { + $egwData['info_des'] = str_replace("\r\n", "\n", $egwData['info_des']); + } + if (!$relax && ($description && empty($egwData['info_des']) + || !empty($egwData['info_des']) && empty($infoData['info_des']) + || strpos($egwData['info_des'], $description) === false)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[description mismatch]: ' + . $infoData['info_des'] . ' <> ' . $egwData['info_des']); + } + continue; + } + // no further criteria to match + $foundInfoLogs[$egwData['info_id']] = $egwData['info_id']; } } - $filter = array(); - - if (!$relax && strlen($description)) { - $filter['search'] = $description; + if (!$relax && !empty($foundInfoLogs)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[FOUND]:' . array2string($foundInfoLogs)); + } + return $foundInfoLogs; } - $filter['col_filter'] = $egwData; + if ($relax) + { + unset($filter['search']); + } - // search for date only match + // search for matches by date only unset($filter['col_filter']['info_startdate']); + unset($filter['col_filter']['info_enddate']); unset($filter['col_filter']['info_datecompleted']); + // Some devices support lesser stati + unset($filter['col_filter']['info_status']); // try tasks without category unset($filter['col_filter']['info_cat']); @@ -1523,41 +1685,144 @@ class infolog_bo // Horde::logMessage("findVTODO Filter\n" // . print_r($filter, true), // __FILE__, __LINE__, PEAR_LOG_DEBUG); - foreach ($this->so->search($filter) as $itemID => $taskData) + foreach ($this->so->search($filter) as $itemID => $egwData) { + if (!$this->check_access($egwData,EGW_ACL_READ)) continue; // Horde::logMessage("findVTODO Trying\n" - // . print_r($taskData, true), + // . print_r($egwData, true), // __FILE__, __LINE__, PEAR_LOG_DEBUG); - if (isset($egwData['info_cat']) - && isset($taskData['info_cat']) && $taskData['info_cat'] - && $egwData['info_cat'] != $taskData['info_cat']) continue; - if (isset($egwData['info_startdate']) - && isset($taskData['info_startdate']) && $taskData['info_startdate']) + if (isset($infoData['info_cat']) + && isset($egwData['info_cat']) && $egwData['info_cat'] + && $infoData['info_cat'] != $egwData['info_cat']) { - $time = new egw_time($taskData['info_startdate'],egw_time::$server_timezone); - $time->setTime(0, 0, 0); - $startdate = egw_time::to($time,'server'); - if ($egwData['info_startdate'] != $startdate) continue; + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[category mismatch]: ' + . $infoData['info_cat'] . ' <> ' . $egwData['info_cat']); + } + continue; } - // some clients don't support DTSTART - if (isset($egwData['info_startdate']) - && (!isset($taskData['info_startdate']) || !$taskData['info_startdate']) - && !$relax) continue; - if (isset($egwData['info_datecompleted']) - && isset($taskData['info_datecompleted']) && $taskData['info_datecompleted']) + if (isset($infoData['info_startdate']) && $infoData['info_startdate']) { - $time = new egw_time($taskData['info_datecompleted'],egw_time::$server_timezone); - $time->setTime(0, 0, 0); - $enddate = egw_time::to($time,'server'); - if ($egwData['info_datecompleted'] != $enddate) continue; + // We got a startdate from client + if (isset($egwData['info_startdate']) && $egwData['info_startdate']) + { + // We compare the date only + $taskTime = new egw_time($infoData['info_startdate'],egw_time::$server_timezone); + $egwTime = new egw_time($egwData['info_startdate'],egw_time::$server_timezone); + if ($taskTime->format('Ymd') != $egwTime->format('Ymd')) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[start mismatch]: ' + . $taskTime->format('Ymd') . ' <> ' . $egwTime->format('Ymd')); + } + continue; + } + } + elseif (!$relax) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[start mismatch]'); + } + continue; + } } - if ((isset($egwData['info_datecompleted']) - && (!isset($taskData['info_datecompleted']) || !$taskData['info_datecompleted'])) || - (!isset($egwData['info_datecompleted']) - && isset($taskData['info_datecompleted']) && $taskData['info_datecompleted']) - && !$relax) continue; - return($itemID); + if ($infoData['info_type'] == 'task') + { + if (isset($infoData['info_status']) && isset($egwData['info_status']) + && $egwData['info_status'] == 'done' + && $infoData['info_status'] != 'done' || + $egwData['info_status'] != 'done' + && $infoData['info_status'] == 'done') + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[status mismatch]: ' + . $infoData['info_status'] . ' <> ' . $egwData['info_status']); + } + continue; + } + if (isset($infoData['info_enddate']) && $infoData['info_enddate']) + { + // We got a enddate from client + if (isset($egwData['info_enddate']) && $egwData['info_enddate']) + { + // We compare the date only + $taskTime = new egw_time($infoData['info_enddate'],egw_time::$server_timezone); + $egwTime = new egw_time($egwData['info_enddate'],egw_time::$server_timezone); + if ($taskTime->format('Ymd') != $egwTime->format('Ymd')) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[DUE mismatch]: ' + . $taskTime->format('Ymd') . ' <> ' . $egwTime->format('Ymd')); + } + continue; + } + } + elseif (!$relax) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[DUE mismatch]'); + } + continue; + } + } + if (isset($infoData['info_datecompleted']) && $infoData['info_datecompleted']) + { + // We got a completed date from client + if (isset($egwData['info_datecompleted']) && $egwData['info_datecompleted']) + { + // We compare the date only + $taskTime = new egw_time($infoData['info_datecompleted'],egw_time::$server_timezone); + $egwTime = new egw_time($egwData['info_datecompleted'],egw_time::$server_timezone); + if ($taskTime->format('Ymd') != $egwTime->format('Ymd')) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[completed mismatch]: ' + . $taskTime->format('Ymd') . ' <> ' . $egwTime->format('Ymd')); + } + continue; + } + } + elseif (!$relax) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[completed mismatch]'); + } + continue; + } + } + elseif (!$relax && isset($egwData['info_datecompleted']) && $egwData['info_datecompleted']) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[completed mismatch]'); + } + continue; + } + } + $foundInfoLogs[$itemID] = $itemID; } - return false; + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[FOUND]:' . array2string($foundInfoLogs)); + } + return $foundInfoLogs; } } diff --git a/infolog/inc/class.infolog_ical.inc.php b/infolog/inc/class.infolog_ical.inc.php index 617b6f0afc..3468c019c7 100644 --- a/infolog/inc/class.infolog_ical.inc.php +++ b/infolog/inc/class.infolog_ical.inc.php @@ -73,6 +73,13 @@ class infolog_ical extends infolog_bo */ var $uidExtension = false; + /** + * user preference: Use this timezone for import from and export to device + * + * @var string + */ + var $tzid = null; + /** * Client CTCap Properties * @@ -148,6 +155,39 @@ class infolog_ical extends infolog_bo $vcal->setAttribute('VERSION',$_version); $vcal->setAttribute('METHOD',$_method); + $tzid = $this->tzid; + + if ($tzid && $tzid != 'UTC') + { + // check if we have vtimezone component data for tzid of event, if not default to user timezone (default to server tz) + if (!($vtimezone = calendar_timezones::tz2id($tzid,'component'))) + { + error_log(__METHOD__."() unknown TZID='$tzid', defaulting to user timezone '".egw_time::$user_timezone->getName()."'!"); + $vtimezone = calendar_timezones::tz2id($tzid=egw_time::$user_timezone->getName(),'component'); + $tzid = null; + } + if (!isset(self::$tz_cache[$tzid])) + { + self::$tz_cache[$tzid] = calendar_timezones::DateTimeZone($tzid); + } + // $vtimezone is a string with a single VTIMEZONE component, afaik Horde_iCalendar can not add it directly + // --> we have to parse it and let Horde_iCalendar add it again + $horde_vtimezone = Horde_iCalendar::newComponent('VTIMEZONE',$container=false); + $horde_vtimezone->parsevCalendar($vtimezone,'VTIMEZONE'); + // DTSTART must be in local time! + $standard = $horde_vtimezone->findComponent('STANDARD'); + $dtstart = $standard->getAttribute('DTSTART'); + $dtstart = new egw_time($dtstart, egw_time::$server_timezone); + $dtstart->setTimezone(self::$tz_cache[$tzid]); + $standard->setAttribute('DTSTART', $dtstart->format('Ymd\THis'), array(), false); + $daylight = $horde_vtimezone->findComponent('DAYLIGHT'); + $dtstart = $daylight->getAttribute('DTSTART'); + $dtstart = new egw_time($dtstart, egw_time::$server_timezone); + $dtstart->setTimezone(self::$tz_cache[$tzid]); + $daylight->setAttribute('DTSTART', $dtstart->format('Ymd\THis'), array(), false); + $vcal->addComponent($horde_vtimezone); + } + $vevent = Horde_iCalendar::newComponent('VTODO',$vcal); if (!isset($this->clientProperties['SUMMARY']['Size'])) @@ -253,15 +293,15 @@ class infolog_ical extends infolog_bo if ($taskData['info_startdate']) { - self::setDateOrTime($vevent,'DTSTART',$taskData['info_startdate']); + self::setDateOrTime($vevent, 'DTSTART', $taskData['info_startdate'], $tzid); } if ($taskData['info_enddate']) { - self::setDateOrTime($vevent,'DUE',$taskData['info_enddate']); + self::setDateOrTime($vevent, 'DUE', $taskData['info_enddate'], false); // export always as date } if ($taskData['info_datecompleted']) { - self::setDateOrTime($vevent,'COMPLETED',$taskData['info_datecompleted']); + self::setDateOrTime($vevent, 'COMPLETED', $taskData['info_datecompleted'], $tzid); } $vevent->setAttribute('DTSTAMP',time()); @@ -298,27 +338,67 @@ class infolog_ical extends infolog_bo } /** - * Check if use set a date or date+time and export it as such + * set date-time attribute to DATE or DATE-TIME depending on value + * 00:00 uses DATE else DATE-TIME * * @param Horde_iCalendar_* $vevent * @param string $attr attribute name - * @param int $value timestamp + * @param int $time timestamp in server-time + * @param string $tzid timezone to use for client, null for user-time, false for server-time */ - static function setDateOrTime($vevent,$attr,$value) + static function setDateOrTime(&$vevent, $attr, $time, $tzid) { - // check if use set only a date --> export it as such - if (date('H:i',$value) == '00:00') + $params = array(); + + if ($tzid) { - $vevent->setAttribute($attr,array( - 'year' => date('Y',$value), - 'month' => date('m',$value), - 'mday' => date('d',$value), - ),array('VALUE' => 'DATE')); + if (!isset(self::$tz_cache[$tzid])) + { + self::$tz_cache[$tzid] = calendar_timezones::DateTimeZone($tzid); + } + $tz = self::$tz_cache[$tzid]; + } + elseif(is_null($tzid)) + { + $tz = egw_time::$user_timezone; } else { - $vevent->setAttribute($attr,$value); + $tz = egw_time::$server_timezone; } + if (!is_a($time,'DateTime')) + { + $time = new egw_time($time,egw_time::$server_timezone); + } + $time->setTimezone($tz); + + // check for date --> export it as such + if ($time->format('Hi') == '0000') + { + $arr = egw_time::to($time, 'array'); + $value = array( + 'year' => $arr['year'], + 'month' => $arr['month'], + 'mday' => $arr['day']); + $params['VALUE'] = 'DATE'; + } + else + { + if ($tzid == 'UTC') + { + $value = $time->format('Ymd\THis\Z'); + } + elseif ($tzid) + { + $value = $time->format('Ymd\THis'); + $params['TZID'] = $tzid; + } + else + { + $value = egw_time::to($time, 'ts'); + } + } + $vevent->setAttribute($attr, $value, $params); } /** @@ -331,7 +411,21 @@ class infolog_ical extends infolog_bo */ function importVTODO(&$_vcalData, $_taskID=-1, $merge=false) { - if (!($taskData = $this->vtodotoegw($_vcalData,$_taskID))) return false; + + if ($this->tzid) + { + date_default_timezone_set($this->tzid); + } + $taskData = $this->vtodotoegw($_vcalData,$_taskID); + if ($this->tzid) + { + date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); + } + + if (!$taskData) return false; + + // keep the dates + $this->time2time($taskData, $this->tzid, false); // we suppose that a not set status in a vtodo means that the task did not started yet if (empty($taskData['info_status'])) @@ -359,18 +453,29 @@ class infolog_ical extends infolog_bo * @param string $_vcalData VTODO * @param int $contentID=null infolog_id (or null, if unkown) * @param boolean $relax=false if true, a weaker match algorithm is used - * @return infolog_id of a matching entry or false, if nothing was found + * + * @return array of infolog_ids of matching entries */ - function searchVTODO($_vcalData, $contentID=null, $relax=false) { - $result = false; + function searchVTODO($_vcalData, $contentID=null, $relax=false) + { + $result = array(); - if (($egwData = $this->vtodotoegw($_vcalData,$contentID))) + if ($this->tzid) + { + date_default_timezone_set($this->tzid); + } + $taskData = $this->vtodotoegw($_vcalData,$contentID); + if ($this->tzid) + { + date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); + } + if ($taskData) { if ($contentID) { - $egwData['info_id'] = $contentID; + $taskData['info_id'] = $contentID; } - $result = $this->findVTODO($egwData, $relax); + $result = $this->findInfo($taskData, $relax, $this->tzid); } return $result; } @@ -386,7 +491,7 @@ class infolog_ical extends infolog_bo { if ($this->log) { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($_taskID)\n" . array2string($_vcalData)."\n",3,$this->logfile); } @@ -412,13 +517,10 @@ class infolog_ical extends infolog_bo $minimum_uid_length = 8; } + $taskData = false; + foreach ($vcal->getComponents() as $component) { - if ($this->log) - { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . - array2string($component)."\n",3,$this->logfile); - } if (!is_a($component, 'Horde_iCalendar_vtodo')) { if ($this->log) @@ -426,140 +528,135 @@ class infolog_ical extends infolog_bo error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. "(): Not a vTODO container, skipping...\n",3,$this->logfile); } + continue; } - else + + $taskData = array(); + $taskData['info_type'] = 'task'; + + if ($_taskID > 0) { - $taskData = array(); - $taskData['info_type'] = 'task'; - - if ($_taskID > 0) - { - $taskData['info_id'] = $_taskID; - } - foreach ($component->_attributes as $attributes) - { - //$attributes['value'] = trim($attributes['value']); - if (!strlen($attributes['value'])) continue; - - switch ($attributes['name']) - { - case 'CLASS': - $taskData['info_access'] = strtolower($attributes['value']); - break; - - case 'DESCRIPTION': - $value = $attributes['value']; - if (preg_match('/\s*\[UID:(.+)?\]/Usm', $value, $matches)) - { - if (!isset($taskData['info_uid']) - && strlen($matches[1]) >= $minimum_uid_length) - { - $taskData['info_uid'] = $matches[1]; - } - //$value = str_replace($matches[0], '', $value); - } - if (preg_match('/\s*\[PARENT_UID:(.+)?\]/Usm', $value, $matches)) - { - if (!isset($taskData['info_id_parent']) - && strlen($matches[1]) >= $minimum_uid_length) - { - $taskData['info_id_parent'] = $this->getParentID($matches[1]); - } - //$value = str_replace($matches[0], '', $value); - } - $taskData['info_des'] = $value; - break; - - case 'LOCATION': - $taskData['info_location'] = $attributes['value']; - break; - - case 'DUE': - // eGroupWare uses date only - $parts = @getdate($attributes['value']); - $value = @mktime(0, 0, 0, $parts['mon'], $parts['mday'], $parts['year']); - $taskData['info_enddate'] = $value; - break; - - case 'COMPLETED': - $taskData['info_datecompleted'] = $attributes['value']; - break; - - case 'DTSTART': - $taskData['info_startdate'] = $attributes['value']; - break; - - case 'PRIORITY': - if (0 <= $attributes['value'] && $attributes['value'] <= 9) { - if ($this->productManufacturer == 'funambol' && - (strpos($this->productName, 'outlook') !== false - || strpos($this->productName, 'pocket pc') !== false)) - { - $taskData['info_priority'] = (int) $this->priority_funambol2egw[$attributes['value']]; - } - else - { - $taskData['info_priority'] = (int) $this->priority_ical2egw[$attributes['value']]; - } - } else { - $taskData['info_priority'] = 1; // default = normal - } - break; - - case 'STATUS': - // check if we (still) have X-INFOLOG-STATUS set AND it would give an unchanged status (no change by the user) - foreach($component->_attributes as $attr) - { - if ($attr['name'] == 'X-INFOLOG-STATUS') break; - } - $taskData['info_status'] = $this->vtodo2status($attributes['value'], - $attr['name'] == 'X-INFOLOG-STATUS' ? $attr['value'] : null); - break; - - case 'SUMMARY': - $taskData['info_subject'] = $attributes['value']; - break; - - case 'RELATED-TO': - $taskData['info_id_parent'] = $this->getParentID($attributes['value']); - break; - - case 'CATEGORIES': - if ($attributes['value']) - { - if($version == '1.0') - { - $vcats = $this->find_or_add_categories(explode(';',$attributes['value']), $_taskID); - } - else - { - $cats = $this->find_or_add_categories(explode(',',$attributes['value']), $_taskID); - } - $taskData['info_cat'] = $cats[0]; - } - break; - - case 'UID': - if (strlen($attributes['value']) >= $minimum_uid_length) { - $taskData['info_uid'] = $attributes['value']; - } - break; - - case 'PERCENT-COMPLETE': - $taskData['info_percent'] = (int) $attributes['value']; - break; - } - } - # the horde ical class does already convert in parsevCalendar - # do NOT convert here - #$taskData = $GLOBALS['egw']->translation->convert($taskData, 'UTF-8'); - - Horde::logMessage("vtodotoegw:\n" . print_r($taskData, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); - - return $taskData; + $taskData['info_id'] = $_taskID; } + foreach ($component->_attributes as $attribute) + { + //$attribute['value'] = trim($attribute['value']); + if (!strlen($attribute['value'])) continue; + + switch ($attribute['name']) + { + case 'CLASS': + $taskData['info_access'] = strtolower($attribute['value']); + break; + + case 'DESCRIPTION': + $value = str_replace("\r\n", "\n", $attribute['value']); + if (preg_match('/\s*\[UID:(.+)?\]/Usm', $value, $matches)) + { + if (!isset($taskData['info_uid']) + && strlen($matches[1]) >= $minimum_uid_length) + { + $taskData['info_uid'] = $matches[1]; + } + //$value = str_replace($matches[0], '', $value); + } + if (preg_match('/\s*\[PARENT_UID:(.+)?\]/Usm', $value, $matches)) + { + if (!isset($taskData['info_id_parent']) + && strlen($matches[1]) >= $minimum_uid_length) + { + $taskData['info_id_parent'] = $this->getParentID($matches[1]); + } + //$value = str_replace($matches[0], '', $value); + } + $taskData['info_des'] = $value; + break; + + case 'LOCATION': + $taskData['info_location'] = str_replace("\r\n", "\n", $attribute['value']); + break; + + case 'DUE': + // eGroupWare uses date only + $parts = @getdate($attribute['value']); + $value = @mktime(0, 0, 0, $parts['mon'], $parts['mday'], $parts['year']); + $taskData['info_enddate'] = $value; + break; + + case 'COMPLETED': + $taskData['info_datecompleted'] = $attribute['value']; + break; + + case 'DTSTART': + $taskData['info_startdate'] = $attribute['value']; + break; + + case 'PRIORITY': + if (0 <= $attribute['value'] && $attribute['value'] <= 9) + { + if ($this->productManufacturer == 'funambol' && + (strpos($this->productName, 'outlook') !== false + || strpos($this->productName, 'pocket pc') !== false)) + { + $taskData['info_priority'] = (int) $this->priority_funambol2egw[$attribute['value']]; + } + else + { + $taskData['info_priority'] = (int) $this->priority_ical2egw[$attribute['value']]; + } + } + else + { + $taskData['info_priority'] = 1; // default = normal + } + break; + + case 'STATUS': + // check if we (still) have X-INFOLOG-STATUS set AND it would give an unchanged status (no change by the user) + foreach ($component->_attributes as $attr) + { + if ($attr['name'] == 'X-INFOLOG-STATUS') break; + } + $taskData['info_status'] = $this->vtodo2status($attribute['value'], + $attr['name'] == 'X-INFOLOG-STATUS' ? $attr['value'] : null); + break; + + case 'SUMMARY': + $taskData['info_subject'] = str_replace("\r\n", "\n", $attribute['value']); + break; + + case 'RELATED-TO': + $taskData['info_id_parent'] = $this->getParentID($attribute['value']); + break; + + case 'CATEGORIES': + if (!empty($attribute['value'])) + { + $cats = $this->find_or_add_categories(explode(',',$attribute['value']), $_taskID); + $taskData['info_cat'] = $cats[0]; + } + break; + + case 'UID': + if (strlen($attribute['value']) >= $minimum_uid_length) + { + $taskData['info_uid'] = $attribute['value']; + } + break; + + case 'PERCENT-COMPLETE': + $taskData['info_percent'] = (int) $attribute['value']; + break; + } + } + break; } - return false; + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($_taskID)\n" . + ($taskData ? array2string($taskData) : 'FALSE') . "\n",3,$this->logfile); + } + return $taskData; } /** @@ -580,7 +677,6 @@ class infolog_ical extends infolog_bo case 'text/plain': $txt = $note['info_subject']."\n\n".$note['info_des']; return $txt; - break; case 'text/x-vnote': if (!empty($note['info_cat'])) @@ -631,7 +727,10 @@ class infolog_ical extends infolog_bo { $vnote->setAttribute('DCREATED',$note['info_startdate']); } - $vnote->setAttribute('DCREATED',$GLOBALS['egw']->contenthistory->getTSforAction('infolog_note',$_noteID,'add')); + else + { + $vnote->setAttribute('DCREATED',$GLOBALS['egw']->contenthistory->getTSforAction('infolog_note',$_noteID,'add')); + } $vnote->setAttribute('LAST-MODIFIED',$GLOBALS['egw']->contenthistory->getTSforAction('infolog_note',$_noteID,'modify')); #$vnote->setAttribute('CLASS',$taskData['info_access'] == 'public' ? 'PUBLIC' : 'PRIVATE'); @@ -684,40 +783,19 @@ class infolog_ical extends infolog_bo * * @param string $_vcalData VNOTE * @param int $contentID=null infolog_id (or null, if unkown) + * @param boolean $relax=false if true, a weaker match algorithm is used + * * @return infolog_id of a matching entry or false, if nothing was found */ - function searchVNOTE($_vcalData, $_type, $contentID=null) + function searchVNOTE($_vcalData, $_type, $contentID=null, $relax=false) { - if (!($note = $this->vnotetoegw($_vcalData,$_type,$contentID))) return false; + if (!($note = $this->vnotetoegw($_vcalData,$_type,$contentID))) return array(); if ($contentID) $note['info_id'] = $contentID; unset($note['info_startdate']); - $filter = array(); - - if (!empty($note['info_des'])) - { - $description = trim(preg_replace("/\r?\n?\\[[A-Z_]+:.*\\]/i", '', $note['info_des'])); - unset($note['info_des']); - if (strlen($description)) - { - $filter['search'] = $description; - } - } - - $filter['col_filter'] = $note; - - if (($foundItems = $this->search($filter))) - { - if (count($foundItems) > 0) - { - $itemIDs = array_keys($foundItems); - return $itemIDs[0]; - } - } - - return false; + return $this->findInfo($note, $relax, $this->tzid); } /** @@ -730,6 +808,13 @@ class infolog_ical extends infolog_bo */ function vnotetoegw($_data, $_type, $_noteID=-1) { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($_type, $_noteID)\n" . + array2string($_data)."\n",3,$this->logfile); + } + $note = false; + switch ($_type) { case 'text/plain': @@ -739,19 +824,15 @@ class infolog_ical extends infolog_bo $txt = $botranslation->convert($_data, 'utf-8'); $txt = str_replace("\r\n", "\n", $txt); - if (preg_match("/^(^\n)\n\n(.*)$/", $txt, $match)) + if (preg_match('/([^\n]+)\n\n(.*)/m', $txt, $match)) { - $note['info_subject'] = $match[0]; - $note['info_des'] = $match[1]; + $note['info_subject'] = $match[1]; + $note['info_des'] = $match[2]; } else { - // should better be imported as subject, but causes duplicates - // TODO: should be examined - $note['info_des'] = $txt; + $note['info_subject'] = $txt; } - - return $note; break; case 'text/x-vnote': @@ -772,34 +853,31 @@ class infolog_ical extends infolog_bo switch ($attribute['name']) { case 'BODY': - $note['info_des'] = $attribute['value']; + $note['info_des'] = str_replace("\r\n", "\n", $attribute['value']); break; case 'SUMMARY': - $note['info_subject'] = $attribute['value']; + $note['info_subject'] = str_replace("\r\n", "\n", $attribute['value']); break; case 'CATEGORIES': if ($attribute['value']) { - if($version == '1.0') - { - $cats = $this->find_or_add_categories(explode(';',$attribute['value']), $_noteID); - } - else - { - $cats = $this->find_or_add_categories(explode(',',$attribute['value']), $_noteID); - } + $cats = $this->find_or_add_categories(explode(',',$attribute['value']), $_noteID); $note['info_cat'] = $cats[0]; } break; } } } - return $note; } } - return false; + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($_type, $_noteID)\n" . + ($note ? array2string($note) : 'FALSE') ."\n",3,$this->logfile); + } + return $note; } /** @@ -842,9 +920,36 @@ class infolog_ical extends infolog_bo { $this->uidExtension = true; } + if (isset($deviceInfo['tzid']) && + $deviceInfo['tzid']) + { + switch ($deviceInfo['tzid']) + { + case 1: + $this->tzid = false; + break; + case 2: + $this->tzid = null; + break; + default: + $this->tzid = $deviceInfo['tzid']; + } + } } - Horde::logMessage('setSupportedFields(' . $this->productManufacturer . ', ' . $this->productName .')', __FILE__, __LINE__, PEAR_LOG_DEBUG); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '(' . $this->productManufacturer . + ', '. $this->productName .', ' . + ($this->tzid ? $this->tzid : egw_time::$user_timezone->getName()) . + ")\n" , 3, $this->logfile); + } + + Horde::logMessage('setSupportedFields(' . $this->productManufacturer . ', ' + . $this->productName .', ' . + ($this->tzid ? $this->tzid : egw_time::$user_timezone->getName()) .')', + __FILE__, __LINE__, PEAR_LOG_DEBUG); } } diff --git a/infolog/inc/class.infolog_sif.inc.php b/infolog/inc/class.infolog_sif.inc.php index 92d1b05bb9..b8d6131036 100644 --- a/infolog/inc/class.infolog_sif.inc.php +++ b/infolog/inc/class.infolog_sif.inc.php @@ -90,6 +90,65 @@ class infolog_sif extends infolog_bo */ var $uidExtension = false; + /** + * user preference: Use this timezone for import from and export to device + * + * @var string + */ + var $tzid = null; + + /** + * Set Logging + * + * @var boolean + */ + var $log = false; + var $logfile="/tmp/log-infolog-sif"; + + /** + * Constructor + * + */ + function __construct() + { + parent::__construct(); + if ($this->log) $this->logfile = $GLOBALS['egw_info']['server']['temp_dir']."/log-infolog-sif"; + $this->vCalendar = new Horde_iCalendar; + } + + /** + * Get DateTime value for a given time and timezone + * + * @param int|string|DateTime $time in server-time as returned by calendar_bo for $data_format='server' + * @param string $tzid TZID of event or 'UTC' or NULL for palmos timestamps in usertime + * @return mixed attribute value to set: integer timestamp if $tzid == 'UTC' otherwise Ymd\THis string IN $tzid + */ + function getDateTime($time, $tzid) + { + if (empty($tzid) || $tzid == 'UTC') + { + return $this->vCalendar->_exportDateTime(egw_time::to($time,'ts')); + } + if (!is_a($time,'DateTime')) + { + $time = new egw_time($time,egw_time::$server_timezone); + } + if (!isset(self::$tz_cache[$tzid])) + { + self::$tz_cache[$tzid] = calendar_timezones::DateTimeZone($tzid); + } + // check for date --> export it as such + if ($time->format('Hi') == '0000') + { + $arr = egw_time::to($time, 'array'); + $time = new egw_time($arr, self::$tz_cache[$tzid]); + } + else + { + $time->setTimezone(self::$tz_cache[$tzid]); + } + return $time->format('Ymd\THis'); + } function startElement($_parser, $_tag, $_attributes) { @@ -121,14 +180,15 @@ class infolog_sif extends infolog_bo */ function siftoegw($sifData, $_sifType, $_id=-1) { + + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($_sifType, $_id)\n" . + array2string($sifData) . "\n", 3, $this->logfile); + } + $sysCharSet = $GLOBALS['egw']->translation->charset(); - #$tmpfname = tempnam('/tmp/sync/contents','sift_'); - - #$handle = fopen($tmpfname, "w"); - #fwrite($handle, $sifData); - #fclose($handle); - switch ($_sifType) { case 'note': @@ -136,9 +196,12 @@ class infolog_sif extends infolog_bo break; case 'task': - default: $this->_currentSIFMapping = $this->_sifTaskMapping; break; + + default: + // we don't know how to handle this + return false; } $this->xml_parser = xml_parser_create('UTF-8'); @@ -156,16 +219,22 @@ class infolog_sif extends infolog_bo return false; } - if (!array($this->_extractedSIFData)) return false; + if (!array($this->_extractedSIFData)) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()[PARSER FAILD]\n", + 3, $this->logfile); + } + return false; + } + $infoData = array(); switch ($_sifType) { case 'task': - $taskData = array(); - $vcal = new Horde_iCalendar; - - $taskData['info_type'] = 'task'; - $taskData['info_status'] = 'not-started'; + $infoData['info_type'] = 'task'; + $infoData['info_status'] = 'not-started'; if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) { @@ -186,7 +255,7 @@ class infolog_sif extends infolog_bo switch($key) { case 'info_access': - $taskData[$key] = ((int)$value > 0) ? 'private' : 'public'; + $infoData[$key] = ((int)$value > 0) ? 'private' : 'public'; break; case 'info_datecompleted': @@ -194,9 +263,9 @@ class infolog_sif extends infolog_bo case 'info_startdate': if (!empty($value)) { - $taskData[$key] = $vcal->_parseDateTime($value); + $infoData[$key] = $this->vCalendar->_parseDateTime($value); // somehow the client always deliver a timestamp about 3538 seconds, when no startdate set. - if ($taskData[$key] < 10000) unset($taskData[$key]); + if ($infoData[$key] < 10000) unset($infoData[$key]); } break; @@ -205,49 +274,48 @@ class infolog_sif extends infolog_bo if (!empty($value)) { $categories = $this->find_or_add_categories(explode(';', $value), $_id); - $taskData['info_cat'] = $categories[0]; + $infoData['info_cat'] = $categories[0]; } break; case 'info_priority': - $taskData[$key] = (int)$value; + $infoData[$key] = (int)$value; break; case 'info_status': switch ($value) { case '0': - $taskData[$key] = 'not-started'; + $infoData[$key] = 'not-started'; break; case '1': - $taskData[$key] = 'ongoing'; + $infoData[$key] = 'ongoing'; break; case '2': - $taskData[$key] = 'done'; - $taskData['info_percent'] = 100; + $infoData[$key] = 'done'; + $infoData['info_percent'] = 100; break; case '3': - $taskData[$key] = 'waiting'; + $infoData[$key] = 'waiting'; break; case '4': if ($this->productName == 'blackberry plug-in') { - $taskData[$key] = 'deferred'; + $infoData[$key] = 'deferred'; } else { - $taskData[$key] = 'cancelled'; + $infoData[$key] = 'cancelled'; } break; default: - $taskData[$key] = 'ongoing'; - break; + $infoData[$key] = 'ongoing'; } break; case 'complete': - $taskData['info_status'] = 'done'; - $taskData['info_percent'] = 100; + $infoData['info_status'] = 'done'; + $infoData['info_percent'] = 100; break; case 'info_des': @@ -256,7 +324,7 @@ class infolog_sif extends infolog_bo { if (strlen($matches[1]) >= $minimum_uid_length) { - $taskData['info_uid'] = $matches[1]; + $infoData['info_uid'] = $matches[1]; } //$value = str_replace($matches[0], '', $value); } @@ -264,25 +332,28 @@ class infolog_sif extends infolog_bo { if (strlen($matches[1]) >= $minimum_uid_length) { - $taskData['info_id_parent'] = $this->getParentID($matches[1]); + $infoData['info_id_parent'] = $this->getParentID($matches[1]); } //$value = str_replace($matches[0], '', $value); } default: - $taskData[$key] = $value; - break; + $infoData[$key] = str_replace("\r\n", "\n", $value); + } + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + "key=$key => value=" . $infoData[$key] . "\n", 3, $this->logfile); } - #error_log("infolog task key=$key => value=" . $taskData[$key]); } - - return $taskData; + if (empty($infoData['info_datecompleted'])) + { + $infoData['info_datecompleted'] = 0; + } break; case 'note': - $noteData = array(); - $noteData['info_type'] = 'note'; - $vcal = new Horde_iCalendar; + $infoData['info_type'] = 'note'; foreach ($this->_extractedSIFData as $key => $value) { @@ -295,13 +366,13 @@ class infolog_sif extends infolog_bo case 'info_startdate': if (!empty($value)) { - $noteData[$key] = $vcal->_parseDateTime($value); + $infoData[$key] = $this->vCalendar->_parseDateTime($value); // somehow the client always deliver a timestamp about 3538 seconds, when no startdate set. - if ($noteData[$key] < 10000) $noteData[$key] = ''; + if ($infoData[$key] < 10000) $infoData[$key] = ''; } else { - $noteData[$key] = ''; + $infoData[$key] = ''; } break; @@ -309,23 +380,26 @@ class infolog_sif extends infolog_bo if (!empty($value)) { $categories = $this->find_or_add_categories(explode(';', $value), $_id); - $noteData['info_cat'] = $categories[0]; + $infoData['info_cat'] = $categories[0]; } break; default: - $noteData[$key] = $value; - break; + $infoData[$key] = str_replace("\r\n", "\n", $value); + } + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + "key=$key => value=" . $infoData[$key] . "\n", 3, $this->logfile); } - #error_log("infolog note key=$key => value=".$noteData[$key]); } - return $noteData; - break; - - - default: - return false; } + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($infoData) . "\n", 3, $this->logfile); + } + return $infoData; } /** @@ -339,28 +413,13 @@ class infolog_sif extends infolog_bo */ function searchSIF($_sifData, $_sifType, $contentID=null, $relax=false) { - if (!($egwData = $this->siftoegw($_sifData, $_sifType, $contentID))) return false; + if (!($egwData = $this->siftoegw($_sifData, $_sifType, $contentID))) return array(); if ($contentID) $egwData['info_id'] = $contentID; - if ($_sifType == 'task') return $this->findVTODO($egwData, $relax); - if ($_sifType == 'note') unset($egwData['info_startdate']); - $filter = array(); - - $filter['col_filter'] = $egwData; - - if ($foundItems = $this->search($filter)) - { - if (count($foundItems) > 0) - { - $itemIDs = array_keys($foundItems); - return $itemIDs[0]; - } - } - - return false; + return $this->findInfo($egwData, $relax, $this->tzid); } /** @@ -373,18 +432,20 @@ class infolog_sif extends infolog_bo */ function addSIF($_sifData, $_id, $_sifType, $merge=false) { - if (!($egwData = $this->siftoegw($_sifData, $_sifType, $_id))) return false; + if ($this->tzid) + { + date_default_timezone_set($this->tzid); + } + $egwData = $this->siftoegw($_sifData, $_sifType, $_id); + if ($this->tzid) + { + date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); + } + if (!$egwData) return false; if ($_id > 0) $egwData['info_id'] = $_id; - if (empty($taskData['info_datecompleted'])) - { - $taskData['info_datecompleted'] = 0; - } - - $egwID = $this->write($egwData, false, true, false); - - return $egwID; + return $this->write($egwData, true, true, false); } @@ -399,189 +460,161 @@ class infolog_sif extends infolog_bo { $sysCharSet = $GLOBALS['egw']->translation->charset(); + if (!($infoData = $this->read($_id, true, 'server'))) return false; + switch($_sifType) { case 'task': - if (($taskData = $this->read($_id, true, 'server'))) + if ($infoData['info_id_parent']) { - $vcal = new Horde_iCalendar('1.0'); - - if ($taskData['info_id_parent']) - { - $parent = $this->read($taskData['info_id_parent']); - $taskData['info_id_parent'] = $parent['info_uid']; - } - else - { - $taskData['info_id_parent'] = ''; - } - - if (!preg_match('/\[UID:.+\]/m', $taskData['info_des'])) - { - $taskData['info_des'] .= "\r\n[UID:" . $taskData['info_uid'] . "]"; - if ($taskData['info_id_parent'] != '') - { - $taskData['info_des'] .= "\r\n[PARENT_UID:" . $taskData['info_id_parent'] . "]"; - } - } - - $sifTask = self::xml_decl . "\n" . self::SIF_decl; - - foreach ($this->_sifTaskMapping as $sifField => $egwField) - { - if (empty($egwField)) continue; - - $value = $GLOBALS['egw']->translation->convert($taskData[$egwField], $sysCharSet, 'utf-8'); - - switch ($sifField) - { - - case 'Complete': - // is handled with DateCompleted - break; - - case 'DateCompleted': - if ($taskData[info_status] == 'done') - { - $sifTask .= "1"; - } - else - { - $sifTask .= "0"; - continue; - } - case 'DueDate': - if (!empty($value)) - { - $hdate = new Horde_Date($value); - $value = $vcal->_exportDate($hdate, '000000Z'); - $sifTask .= "<$sifField>$value"; - } - else - { - $sifTask .= "<$sifField>"; - } - break; - case 'StartDate': - if (!empty($value)) - { - $value = $vcal->_exportDateTime($value); - $sifTask .= "<$sifField>$value"; - } - else - { - $sifTask .= "<$sifField>"; - } - break; - - case 'Importance': - if ($value > 3) $value = 3; - $sifTask .= "<$sifField>$value"; - break; - - case 'Sensitivity': - $value = ($value == 'private' ? '2' : '0'); - $sifTask .= "<$sifField>$value"; - break; - - case 'Status': - switch ($value) - { - case 'cancelled': - case 'deferred': - $value = '4'; - break; - case 'waiting': - case 'nonactive': - $value = '3'; - break; - case 'done': - case 'archive': - case 'billed': - $value = '2'; - break; - case 'not-started': - case 'template': - $value = '0'; - break; - default: //ongoing - $value = 1; - break; - } - $sifTask .= "<$sifField>$value"; - break; - - case 'Categories': - if (!empty($value) && $value) - { - $value = implode('; ', $this->get_categories(array($value))); - $value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8'); - } - else - { - break; - } - - default: - $value = @htmlspecialchars($value, ENT_QUOTES, 'utf-8'); - $sifTask .= "<$sifField>$value"; - break; - } - } - $sifTask .= '00'; - return $sifTask; + $parent = $this->read($infoData['info_id_parent']); + $infoData['info_id_parent'] = $parent['info_uid']; } - break; + else + { + $infoData['info_id_parent'] = ''; + } + + if (!preg_match('/\[UID:.+\]/m', $infoData['info_des'])) + { + $infoData['info_des'] .= "\r\n[UID:" . $infoData['info_uid'] . "]"; + if ($infoData['info_id_parent'] != '') + { + $infoData['info_des'] .= "\r\n[PARENT_UID:" . $infoData['info_id_parent'] . "]"; + } + } + + $sifTask = self::xml_decl . "\n" . self::SIF_decl; + + foreach ($this->_sifTaskMapping as $sifField => $egwField) + { + if (empty($egwField)) continue; + + $value = $GLOBALS['egw']->translation->convert($infoData[$egwField], $sysCharSet, 'utf-8'); + + switch ($sifField) + { + + case 'Complete': + // is handled with DateCompleted + break; + + case 'DateCompleted': + if ($infoData[info_status] != 'done') + { + $sifTask .= "0"; + continue; + } + $sifTask .= "1"; + + case 'DueDate': + case 'StartDate': + $sifTask .= '<$sifField>'; + if (!empty($value)) + { + $sifTask .= $this->getDateTime($value, $this->tzid); + } + $sifTask .= ''; + break; + + case 'Importance': + if ($value > 3) $value = 3; + $sifTask .= "<$sifField>$value"; + break; + + case 'Sensitivity': + $value = ($value == 'private' ? '2' : '0'); + $sifTask .= "<$sifField>$value"; + break; + + case 'Status': + switch ($value) + { + case 'cancelled': + case 'deferred': + $value = '4'; + break; + case 'waiting': + case 'nonactive': + $value = '3'; + break; + case 'done': + case 'archive': + case 'billed': + $value = '2'; + break; + case 'not-started': + case 'template': + $value = '0'; + break; + default: //ongoing + $value = 1; + break; + } + $sifTask .= "<$sifField>$value"; + break; + + case 'Categories': + if (!empty($value) && $value) + { + $value = implode('; ', $this->get_categories(array($value))); + $value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8'); + } + else + { + break; + } + + default: + $value = @htmlspecialchars($value, ENT_QUOTES, 'utf-8'); + $sifTask .= "<$sifField>$value"; + break; + } + } + $sifTask .= '00'; + return $sifTask; case 'note': - if (($taskData = $this->read($_id, true, 'server'))) + $sifNote = self::xml_decl . "\n" . self::SIF_decl; + + foreach ($this->_sifNoteMapping as $sifField => $egwField) { - $vcal = new Horde_iCalendar('1.0'); + if(empty($egwField)) continue; - $sifNote = self::xml_decl . "\n" . self::SIF_decl; + $value = $GLOBALS['egw']->translation->convert($infoData[$egwField], $sysCharSet, 'utf-8'); - foreach ($this->_sifNoteMapping as $sifField => $egwField) + switch ($sifField) { - if(empty($egwField)) continue; + case 'Date': + $sifNote .= '<$sifField>'; + if (!empty($value)) + { + $sifNote .= $this->getDateTime($value, $this->tzid); + } + $sifNote .= ''; + break; - $value = $GLOBALS['egw']->translation->convert($taskData[$egwField], $sysCharSet, 'utf-8'); - - switch ($sifField) - { - case 'Date': - if (!empty($value)) - { - $value = $vcal->_exportDateTime($value); - } - $sifNote .= "<$sifField>$value"; + case 'Categories': + if (!empty($value)) + { + $value = implode('; ', $this->get_categories(array($value))); + $value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8'); + } + else + { break; + } - case 'Categories': - if (!empty($value)) - { - $value = implode('; ', $this->get_categories(array($value))); - $value = $GLOBALS['egw']->translation->convert($value, $sysCharSet, 'utf-8'); - } - else - { - break; - } - - default: - $value = @htmlspecialchars($value, ENT_QUOTES, 'utf-8'); - $sifNote .= "<$sifField>$value"; - break; - } + default: + $value = @htmlspecialchars($value, ENT_QUOTES, 'utf-8'); + $sifNote .= "<$sifField>$value"; + break; } - $sifNote .= ''; - return $sifNote; } - break; - - default; - return false; + $sifNote .= ''; + return $sifNote; } - + return false; } /** @@ -604,6 +637,21 @@ class infolog_sif extends infolog_bo { $this->uidExtension = true; } + if (isset($deviceInfo['tzid']) && + $deviceInfo['tzid']) + { + switch ($deviceInfo['tzid']) + { + case 1: + $this->tzid = false; + break; + case 2: + $this->tzid = null; + break; + default: + $this->tzid = $deviceInfo['tzid']; + } + } } // store product name and version, to be able to use it elsewhere if ($_productName)