From 081445949ff5c7854a92e7742ecaa36b3f2c41dc Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Mon, 11 Jan 2010 23:48:42 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20J=C3=B6rgs=20commits:=20-=20r28917:=20?= =?UTF-8?q?=20Fix=20encoding=20issues;=20improvements=20for=20Funambol=20c?= =?UTF-8?q?lients=20-=20r28918:=20=20Fix=20SyncML=20client=20recognition?= =?UTF-8?q?=20-=20r28919:=20=20More=20Funambol=20adjustments=20and=20worka?= =?UTF-8?q?rounds=20-=20r28920:=20=20Fix=20task=20priorities=20for=20Funam?= =?UTF-8?q?bol=20WM=20Client=20-=20r28921:=20=20Fix=20various=20issues=20i?= =?UTF-8?q?ntroduced=20with=20Funambol=20adjustments;=20code=20=20=20clean?= =?UTF-8?q?up=20-=20r28922:=20=20Enforce=20SINGLE,=20if=20detected;=20clea?= =?UTF-8?q?nup=20logging=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inc/class.addressbook_vcal.inc.php | 152 +++-- calendar/inc/class.calendar_ical.inc.php | 197 +++++-- infolog/inc/class.infolog_ical.inc.php | 275 +++++++-- .../Command/Sync/ContentSyncElement.php | 2 +- phpgwapi/inc/horde/Horde/iCalendar.php | 547 ++++++++++-------- 5 files changed, 769 insertions(+), 404 deletions(-) diff --git a/addressbook/inc/class.addressbook_vcal.inc.php b/addressbook/inc/class.addressbook_vcal.inc.php index 1a61543fb7..4723c7bcf5 100644 --- a/addressbook/inc/class.addressbook_vcal.inc.php +++ b/addressbook/inc/class.addressbook_vcal.inc.php @@ -96,8 +96,12 @@ class addressbook_vcal extends addressbook_bo function __construct($contact_app='addressbook', $_contentType='text/x-vcard', &$_clientProperties = array()) { parent::__construct($contact_app); - if($this->log)$this->logfile = $GLOBALS['egw_info']['server']['temp_dir']."/log-vcard"; - if($this->log)error_log(__LINE__.__METHOD__.__FILE__.array2string($_contentType)."\n",3,$this->logfile); + if ($this->log) + { + $this->logfile = $GLOBALS['egw_info']['server']['temp_dir']."/log-vcard"; + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($_contentType)."\n",3,$this->logfile); + } switch($_contentType) { case 'text/vcard': @@ -215,6 +219,12 @@ class addressbook_vcal extends addressbook_bo { $size = $this->clientProperties[$vcardField]['Size']; $noTruncate = $this->clientProperties[$vcardField]['NoTruncate']; + if ($this->log && $size > 0) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() $vcardField Size: $size, NoTruncate: " . + ($noTruncate ? 'TRUE' : 'FALSE') . "\n",3,$this->logfile); + } //Horde::logMessage("vCalAddressbook $vcardField Size: $size, NoTruncate: " . // ($noTruncate ? 'TRUE' : 'FALSE'), __FILE__, __LINE__, PEAR_LOG_DEBUG); } @@ -225,7 +235,7 @@ class addressbook_vcal extends addressbook_bo } foreach ($databaseFields as $databaseField) { - $value = ""; + $value = ''; if (!empty($databaseField)) { @@ -282,21 +292,48 @@ class addressbook_vcal extends addressbook_bo { $values = (array) $GLOBALS['egw']->translation->convert($values, $sysCharSet, $_charset); $value = implode(',', $values); // just for the CHARSET recognition - if ($extra_charset_attribute && preg_match('/([\177-\377])/', $value)) + if (($size > 0) && strlen($value) > $size) { - $options['CHARSET'] = $_charset; + // let us try with only the first category + $value = $values[0]; + if (strlen($value) > $size) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() $vcardField omitted due to maximum size $size\n",3,$this->logfile); + } + // Horde::logMessage("vCalAddressbook $vcardField omitted due to maximum size $size", + // __FILE__, __LINE__, PEAR_LOG_WARNING); + continue; + } + $values = array(); + } + if (preg_match('/[^\x20-\x7F]/', $value)) + { + if ($extra_charset_attribute || $this->productName == 'kde') + { + $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' || - ($this->productManufacturer == 'funambol' && $this->productName == 'blackberry plug-in')) + if ($this->productName == 'kde') { $options['ENCODING'] = 'QUOTED-PRINTABLE'; } - else + elseif ($this->productManufacturer == 'funambol') + { + $options['ENCODING'] = 'FUNAMBOL-QP'; + } + elseif (preg_match('/([\000-\012\015\016\020-\037\075])/', $value)) + { + $options['ENCODING'] = 'QUOTED-PRINTABLE'; + } + elseif (!$extra_charset_attribute) { $options['ENCODING'] = ''; } } - $hasdata++; + $hasdata++; } break; @@ -305,7 +342,11 @@ class addressbook_vcal extends addressbook_bo { if ($noTruncate) { - error_log(__FILE__ . __LINE__ . __METHOD__ . " vCalAddressbook $vcardField omitted due to maximum size $size"); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() $vcardField omitted due to maximum size $size\n",3,$this->logfile); + } // Horde::logMessage("vCalAddressbook $vcardField omitted due to maximum size $size", // __FILE__, __LINE__, PEAR_LOG_WARNING); continue; @@ -321,7 +362,11 @@ class addressbook_vcal extends addressbook_bo { $value = ''; } - error_log(__FILE__ . __LINE__ . __METHOD__ . " vCalAddressbook $vcardField truncated to maximum size $size"); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() $vcardField truncated to maximum size $size\n",3,$this->logfile); + } //Horde::logMessage("vCalAddressbook $vcardField truncated to maximum size $size", // __FILE__, __LINE__, PEAR_LOG_INFO); } @@ -331,33 +376,29 @@ class addressbook_vcal extends addressbook_bo { $value = $GLOBALS['egw']->translation->convert(trim($value), $sysCharSet, $_charset); $values[] = $value; - if ($extra_charset_attribute) + if (preg_match('/[^\x20-\x7F]/', $value)) { - if (preg_match('/([\177-\377])/', $value)) + if ($extra_charset_attribute || $this->productName == 'kde') { $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' || - ($this->productManufacturer == 'funambol' && $this->productName == 'blackberry plug-in')) - { - $options['ENCODING'] = 'QUOTED-PRINTABLE'; - } - else - { - $options['ENCODING'] = ''; - } } - // protect the CardDAV - if (preg_match('/([\000-\012\015\016\020-\037\075])/', $value)) + // 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'; } - } - else - { - // avoid that these options are inserted from horde code - $options['CHARSET'] = ''; - $options['ENCODING'] = ''; + elseif ($this->productManufacturer == 'funambol') + { + $options['ENCODING'] = 'FUNAMBOL-QP'; + } + elseif (preg_match('/([\000-\012\015\016\020-\037\075])/', $value)) + { + $options['ENCODING'] = 'QUOTED-PRINTABLE'; + } + elseif (!$extra_charset_attribute) + { + $options['ENCODING'] = ''; + } } if ($vcardField == 'TEL' && $entry['tel_prefer'] && ($databaseField == $entry['tel_prefer'])) @@ -392,8 +433,10 @@ class addressbook_vcal extends addressbook_bo $result = $vCard->exportvCalendar(); if ($this->log) { - error_log(__LINE__.__METHOD__.__FILE__."'$this->productManufacturer','$this->productName'"."\n",3,$this->logfile); - error_log(__LINE__.__METHOD__.__FILE__."\n".array2string($result)."\n",3,$this->logfile); + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() '$this->productManufacturer','$this->productName'\n",3,$this->logfile); + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($result)."\n",3,$this->logfile); } return $result; } @@ -465,7 +508,11 @@ class addressbook_vcal extends addressbook_bo 'UID' => array('uid'), ); - if ($this->log) error_log(__LINE__.__METHOD__.__FILE__."\n".array2string($_vcard)."\n",3,$this->logfile); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($_vcard)."\n",3,$this->logfile); + } //Horde::logMessage("vCalAddressbook vcardtoegw:\n$_vcard", __FILE__, __LINE__, PEAR_LOG_DEBUG); @@ -639,17 +686,16 @@ class addressbook_vcal extends addressbook_bo $rowNames[$key] = $rowName; } + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($rowNames)."\n",3,$this->logfile); + } - - if($this->log)error_log(__LINE__.__METHOD__.__FILE__."\n".array2string($rowNames)."\n",3,$this->logfile); - - // 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. - - //error_log(print_r($rowNames, true)); - + // 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. $finalRowNames = array(); @@ -796,10 +842,11 @@ class addressbook_vcal extends addressbook_bo } - if($this->log)error_log(__LINE__.__METHOD__.__FILE__."\n".array2string($finalRowNames)."\n",3,$this->logfile); - - //error_log(print_r($finalRowNames, true)); - + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($finalRowNames)."\n",3,$this->logfile); + } $contact = array(); @@ -865,8 +912,13 @@ class addressbook_vcal extends addressbook_bo $this->fixup_contact($contact); - if ($this->log) error_log(__LINE__.__METHOD__.__FILE__."'$this->productManufacturer','$this->productName'"."\n",3,$this->logfile); - if ($this->log) error_log(__LINE__.__METHOD__.__FILE__."\n".array2string($contact)."\n",3,$this->logfile); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() '$this->productManufacturer','$this->productName'\n",3,$this->logfile); + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($contact)."\n",3,$this->logfile); + } return $contact; } diff --git a/calendar/inc/class.calendar_ical.inc.php b/calendar/inc/class.calendar_ical.inc.php index a3e5316d74..a6aa24818b 100644 --- a/calendar/inc/class.calendar_ical.inc.php +++ b/calendar/inc/class.calendar_ical.inc.php @@ -259,7 +259,11 @@ class calendar_ical extends calendar_boupdate $event['recur_enddate'] = $this->date2ts($time); } - if ($this->log) error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n".array2string($event)."\n",3,$this->logfile); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($event)."\n",3,$this->logfile); + } if ($this->tzid === false) { @@ -491,7 +495,9 @@ class calendar_ical extends calendar_boupdate break; case 'PRIORITY': - if($this->productManufacturer == 'funambol') + if ($this->productManufacturer == 'funambol' && + (strpos($this->productName, 'outlook') !== false + || strpos($this->productName, 'pocket pc') !== false)) { $attributes['PRIORITY'] = (int) $this->priority_egw2funambol[$event['priority']]; } @@ -513,10 +519,16 @@ class calendar_ical extends calendar_boupdate break; case 'CATEGORIES': - if ($event['category']) + if ($event['category'] && ($values['CATEGORIES'] = $this->get_categories($event['category']))) { - $attributes['CATEGORIES'] = ''; - $values['CATEGORIES'] = $this->get_categories($event['category']); + if (count($values['CATEGORIES']) == 1) + { + $attributes['CATEGORIES'] = array_shift($values['CATEGORIES']); + } + else + { + $attributes['CATEGORIES'] = ''; + } } break; @@ -571,8 +583,14 @@ class calendar_ical extends calendar_boupdate { $size = $this->clientProperties[$icalFieldName]['Size']; $noTruncate = $this->clientProperties[$icalFieldName]['NoTruncate']; - #Horde::logMessage("vCalendar $icalFieldName Size: $size, NoTruncate: " . - # ($noTruncate ? 'TRUE' : 'FALSE'), __FILE__, __LINE__, PEAR_LOG_DEBUG); + if ($this->log && $size > 0) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() $icalFieldName Size: $size, NoTruncate: " . + ($noTruncate ? 'TRUE' : 'FALSE') . "\n",3,$this->logfile); + } + //Horde::logMessage("vCalendar $icalFieldName Size: $size, NoTruncate: " . + // ($noTruncate ? 'TRUE' : 'FALSE'), __FILE__, __LINE__, PEAR_LOG_DEBUG); } else { @@ -585,14 +603,24 @@ class calendar_ical extends calendar_boupdate { if ($noTruncate) { - Horde::logMessage("vCalendar $icalFieldName omitted due to maximum size $size", - __FILE__, __LINE__, PEAR_LOG_WARNING); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() $icalFieldName omitted due to maximum size $size\n",3,$this->logfile); + } + //Horde::logMessage("vCalendar $icalFieldName omitted due to maximum size $size", + // __FILE__, __LINE__, PEAR_LOG_WARNING); continue; // skip field } // truncate the value to size $value = substr($value, 0, $size - 1); - Horde::logMessage("vCalendar $icalFieldName truncated to maximum size $size", - __FILE__, __LINE__, PEAR_LOG_INFO); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() $icalFieldName truncated to maximum size $size\n",3,$this->logfile); + } + //Horde::logMessage("vCalendar $icalFieldName truncated to maximum size $size", + // __FILE__, __LINE__, PEAR_LOG_INFO); } if (!empty($value) || ($size >= 0 && !$noTruncate)) { @@ -694,30 +722,59 @@ class calendar_ical extends calendar_boupdate $GLOBALS['egw']->translation->charset(),'UTF-8'); $valuesData = (array) $GLOBALS['egw']->translation->convert($values[$key], $GLOBALS['egw']->translation->charset(),'UTF-8'); - //echo "$key:$valueID: value=$valueData, param=".print_r($paramDate,true)."\n"; - $vevent->setAttribute($key, $valueData, $paramData, true, $valuesData); - $options = array(); - if ($paramData['CN']) $valueData .= $paramData['CN']; // attendees or organizer CN can contain utf-8 content - /*if($key != 'RRULE' && preg_match('/([\000-\012\015\016\020-\037\075])/',$valueData)) { - $options['ENCODING'] = 'QUOTED-PRINTABLE'; - }*/ - if ($this->productManufacturer != 'groupdav' && preg_match('/([\177-\377])/', $valueData)) + + if (preg_match('/[^\x20-\x7F]/', $valueData) || + ($paramData['CN'] && preg_match('/[^\x20-\x7F]/', $paramData['CN']))) { - $options['CHARSET'] = 'UTF-8'; + $paramData['CHARSET'] = 'UTF-8'; + switch ($this->productManufacturer) + { + case 'groupdav': + if ($this->productName == 'kde') + { + $paramData['ENCODING'] = 'QUOTED-PRINTABLE'; + } + else + { + $paramData['CHARSET'] = ''; + if (preg_match('/([\000-\012\015\016\020-\037\075])/', $valueData)) + { + $paramData['ENCODING'] = 'QUOTED-PRINTABLE'; + } + else + { + $paramData['ENCODING'] = ''; + } + } + break; + case 'funambol': + $paramData['ENCODING'] = 'FUNAMBOL-QP'; + } } + /* if (preg_match('/([\000-\012])/', $valueData)) { - if ($this->log) error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."() Has invalid XML data: $valueData",3,$this->logfile); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() Has invalid XML data: $valueData",3,$this->logfile); + } } - $vevent->setParameter($key, $options); + */ + $vevent->setAttribute($key, $valueData, $paramData, true, $valuesData); } } $vcal->addComponent($vevent); } $retval = $vcal->exportvCalendar(); - if ($this->log) error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n".array2string($retval)."\n",3,$this->logfile); - + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() '$this->productManufacturer','$this->productName'\n",3,$this->logfile); + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "()\n".array2string($retval)."\n",3,$this->logfile); + } return $retval; } @@ -762,15 +819,19 @@ class calendar_ical extends calendar_boupdate */ function importVCal($_vcalData, $cal_id=-1, $etag=null, $merge=false, $recur_date=0) { - if ($this->log) error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n".array2string($_vcalData)."\n",3,$this->logfile); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($_vcalData)."\n",3,$this->logfile); + } + + if (!is_array($this->supportedFields)) $this->setSupportedFields(); if (!($events = $this->icaltoegw($_vcalData,$cal_id,$etag,$recur_date))) { return false; } - if (!is_array($this->supportedFields)) $this->setSupportedFields(); - // check if we are importing an event series with exceptions in CalDAV // only first event / series master get's cal_id from URL // other events are exceptions and need to be checked if they are new @@ -784,6 +845,11 @@ class calendar_ical extends calendar_boupdate } foreach ($events as $event) { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($event)."\n",3,$this->logfile); + } $updated_id = false; $event_info = $this->get_event_info($event); @@ -815,6 +881,10 @@ class calendar_ical extends calendar_boupdate // common adjustments for existing events if (is_array($event_info['stored_event'])) { + if (empty($event['uid'])) + { + $event['uid'] = $event_info['stored_event']['uid']; // restore the UID if it was not delivered + } if ($merge) { // overwrite with server data for merge @@ -934,11 +1004,20 @@ class calendar_ical extends calendar_boupdate switch ($event_info['type']) { case 'SINGLE': - Horde::logMessage('importVCAL event SINGLE',__FILE__, __LINE__, PEAR_LOG_DEBUG); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "(): event SINGLE\n",3,$this->logfile); + } + //Horde::logMessage('importVCAL event SINGLE', + // __FILE__, __LINE__, PEAR_LOG_DEBUG); // update the event 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 $updated_id = $this->update($event_to_store, true); unset($event_to_store); @@ -946,7 +1025,13 @@ class calendar_ical extends calendar_boupdate break; case 'SERIES-MASTER': - Horde::logMessage('importVCAL event SERIES-MASTER',__FILE__, __LINE__, PEAR_LOG_DEBUG); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "(): event SERIES-MASTER\n",3,$this->logfile); + } + //Horde::logMessage('importVCAL event SERIES-MASTER', + // __FILE__, __LINE__, PEAR_LOG_DEBUG); // remove all known "status only" exceptions and update the event if ($event_info['acl_edit']) @@ -973,7 +1058,13 @@ class calendar_ical extends calendar_boupdate case 'SERIES-EXCEPTION': case 'SERIES-EXCEPTION-PROPAGATE': - Horde::logMessage('importVCAL event SERIES-EXCEPTION',__FILE__, __LINE__, PEAR_LOG_DEBUG); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "(): event SERIES-EXCEPTION\n",3,$this->logfile); + } + //Horde::logMessage('importVCAL event SERIES-EXCEPTION', + // __FILE__, __LINE__, PEAR_LOG_DEBUG); // update event if ($event_info['acl_edit']) @@ -1004,7 +1095,13 @@ class calendar_ical extends calendar_boupdate break; case 'SERIES-EXCEPTION-STATUS': - Horde::logMessage('importVCAL event SERIES-EXCEPTION-STATUS',__FILE__, __LINE__, PEAR_LOG_DEBUG); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "(): event SERIES-EXCEPTION-STATUS\n",3,$this->logfile); + } + //Horde::logMessage('importVCAL event SERIES-EXCEPTION-STATUS', + // __FILE__, __LINE__, PEAR_LOG_DEBUG); if ($event_info['acl_edit']) { @@ -1105,7 +1202,8 @@ class calendar_ical extends calendar_boupdate if ($this->log) { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n".array2string($event_info['stored_event'])."\n",3,$this->logfile); + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($event_info['stored_event'])."\n",3,$this->logfile); } } @@ -1233,8 +1331,15 @@ class calendar_ical extends calendar_boupdate } } - Horde::logMessage('setSupportedFields(' . $this->productManufacturer - . ', ' . $this->productName .')', __FILE__, __LINE__, PEAR_LOG_DEBUG); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + '(' . $this->productManufacturer . + ', '. $this->productName . ")\n",3,$this->logfile); + } + + //Horde::logMessage('setSupportedFields(' . $this->productManufacturer + // . ', ' . $this->productName .')', __FILE__, __LINE__, PEAR_LOG_DEBUG); $defaultFields['minimal'] = array( 'public' => 'public', @@ -1275,6 +1380,13 @@ class calendar_ical extends calendar_boupdate 'etag' => 'etag', ); + $defaultFields['funambol'] = $defaultFields['basic'] + array( + 'participants' => 'participants', + 'owner' => 'owner', + 'category' => 'category', + 'non_blocking' => 'non_blocking', + ); + $defaultFields['evolution'] = $defaultFields['basic'] + array( 'participants' => 'participants', 'owner' => 'owner', @@ -1402,7 +1514,7 @@ class calendar_ical extends calendar_boupdate break; case 'funambol': - $this->supportedFields = $defaultFields['synthesis']; + $this->supportedFields = $defaultFields['funambol']; break; // the fallback for SyncML @@ -1430,6 +1542,11 @@ class calendar_ical extends calendar_boupdate $vcal = new Horde_iCalendar; if (!$vcal->parsevCalendar($_vcalData)) { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "(): No vCalendar Container found!\n",3,$this->logfile); + } if ($this->tzid) { date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); @@ -1867,7 +1984,9 @@ class calendar_ical extends calendar_boupdate } break; case 'PRIORITY': - if($this->productManufacturer == 'funambol') + if ($this->productManufacturer == 'funambol' && + (strpos($this->productName, 'outlook') !== false + || strpos($this->productName, 'pocket pc') !== false)) { $vcardData['priority'] = (int) $this->priority_funambol2egw[$attributes['value']]; } @@ -2047,7 +2166,6 @@ class calendar_ical extends calendar_boupdate // fall through case 'LAST-MODIFIED': // will be written direct to the event $event['modified'] = $attributes['value']; - break; } } @@ -2127,6 +2245,11 @@ class calendar_ical extends calendar_boupdate } if ($this->calendarOwner) $event['owner'] = $this->calendarOwner; + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($event)."\n",3,$this->logfile); + } //Horde::logMessage("vevent2egw:\n" . print_r($event, true), // __FILE__, __LINE__, PEAR_LOG_DEBUG); return $event; diff --git a/infolog/inc/class.infolog_ical.inc.php b/infolog/inc/class.infolog_ical.inc.php index defbfdf203..62bc3bb19e 100644 --- a/infolog/inc/class.infolog_ical.inc.php +++ b/infolog/inc/class.infolog_ical.inc.php @@ -20,23 +20,42 @@ require_once EGW_SERVER_ROOT.'/phpgwapi/inc/horde/lib/core.php'; class infolog_ical extends infolog_bo { /** - * @var array conversion of the priority egw => ical + * @var array $priority_egw2ical conversion of the priority egw => ical */ - var $egw_priority2vcal_priority = array( - 0 => 9, // low - 1 => 5, // normal - 2 => 3, // high - 3 => 1, // urgent + var $priority_egw2ical = array( + 0 => 9, // low + 1 => 5, // normal + 2 => 3, // high + 3 => 1, // urgent ); /** - * @var array conversion of the priority ical => egw + * @var array $priority_ical2egw conversion of the priority ical => egw */ - var $vcal_priority2egw_priority = array( - 9 => 0, 8 => 0, 7 => 0, // low - 6 => 1, 5 => 1, 4 => 1, 0 => 1, // normal - 3 => 2, 2 => 2, // high - 1 => 3, // urgent + var $priority_ical2egw = array( + 9 => 0, 8 => 0, 7 => 0, // low + 6 => 1, 5 => 1, 4 => 1, 0 => 1, // normal + 3 => 2, 2 => 2, // high + 1 => 3, // urgent + ); + + /** + * @var array $priority_egw2funambol conversion of the priority egw => funambol + */ + var $priority_egw2funambol = array( + 0 => 0, // low + 1 => 1, // normal + 2 => 2, // high + 3 => 2, // urgent + ); + + /** + * @var array $priority_funambol2egw conversion of the priority funambol => egw + */ + var $priority_funambol2egw = array( + 0 => 0, // low + 1 => 1, // normal + 2 => 3, // high ); /** @@ -61,6 +80,15 @@ class infolog_ical extends infolog_bo */ var $clientProperties; + /** + * Set Logging + * + * @var boolean + */ + var $log = false; + var $logfile="/tmp/log-infolog-vcal"; + + /** * Constructor * @@ -69,7 +97,7 @@ class infolog_ical extends infolog_bo function __construct(&$_clientProperties = array()) { parent::__construct(); - + if ($this->log) $this->logfile = $GLOBALS['egw_info']['server']['temp_dir']."/log-infolog-vcal"; $this->clientProperties = $_clientProperties; } @@ -142,8 +170,14 @@ class infolog_ical extends infolog_bo { $size = $this->clientProperties[$field]['Size']; $noTruncate = $this->clientProperties[$field]['NoTruncate']; - #Horde::logMessage("VTODO $field Size: $size, NoTruncate: " . - # ($noTruncate ? 'TRUE' : 'FALSE'), __FILE__, __LINE__, PEAR_LOG_DEBUG); + if ($this->log && $size > 0) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() $field Size: $size, NoTruncate: " . + ($noTruncate ? 'TRUE' : 'FALSE') . "\n",3,$this->logfile); + } + //Horde::logMessage("VTODO $field Size: $size, NoTruncate: " . + // ($noTruncate ? 'TRUE' : 'FALSE'), __FILE__, __LINE__, PEAR_LOG_DEBUG); } else { @@ -155,34 +189,64 @@ class infolog_ical extends infolog_bo { if ($noTruncate) { - Horde::logMessage("VTODO $field omitted due to maximum size $size", - __FILE__, __LINE__, PEAR_LOG_WARNING); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() $field omitted due to maximum size $size\n",3,$this->logfile); + } + //Horde::logMessage("VTODO $field omitted due to maximum size $size", + // __FILE__, __LINE__, PEAR_LOG_WARNING); continue; // skip field } // truncate the value to size $value = substr($value, 0, $size -1); - #Horde::logMessage("VTODO $field truncated to maximum size $size", - # __FILE__, __LINE__, PEAR_LOG_INFO); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ . + "() $field truncated to maximum size $size\n",3,$this->logfile); + } + //Horde::logMessage("VTODO $field truncated to maximum size $size", + // __FILE__, __LINE__, PEAR_LOG_INFO); } if (empty($value) && ($size < 0 || $noTruncate)) continue; if ($field == 'RELATED-TO') { - $options = array('RELTYPE' => 'PARENT'); + $options = array('RELTYPE' => 'PARENT'); } else { $options = array(); } - /*if(preg_match('/([\000-\012\015\016\020-\037\075])/', $value)) { - $options['ENCODING'] = 'QUOTED-PRINTABLE'; - }*/ - if ($this->productManufacturer != 'groupdav' - && preg_match('/([\177-\377])/',$value)) + if (preg_match('/[^\x20-\x7F]/', $value)) { - $options['CHARSET'] = 'UTF-8'; + $options['CHARSET'] = 'UTF-8'; + switch ($this->productManufacturer) + { + case 'groupdav': + if ($this->productName == 'kde') + { + $options['ENCODING'] = 'QUOTED-PRINTABLE'; + } + else + { + $options['CHARSET'] = ''; + + if (preg_match('/([\000-\012\015\016\020-\037\075])/', $value)) + { + $options['ENCODING'] = 'QUOTED-PRINTABLE'; + } + else + { + $options['ENCODING'] = ''; + } + } + break; + case 'funambol': + $options['ENCODING'] = 'FUNAMBOL-QP'; + } } $vevent->setAttribute($field, $value, $options); } @@ -208,13 +272,28 @@ class infolog_ical extends infolog_bo // we try to preserv the original infolog status as X-INFOLOG-STATUS, so we can restore it, if the user does not modify STATUS $vevent->setAttribute('X-INFOLOG-STATUS',$taskData['info_status']); $vevent->setAttribute('PERCENT-COMPLETE',$taskData['info_percent']); - $vevent->setAttribute('PRIORITY',$this->egw_priority2vcal_priority[$taskData['info_priority']]); - + if ($this->productManufacturer == 'funambol' && + (strpos($this->productName, 'outlook') !== false + || strpos($this->productName, 'pocket pc') !== false)) + { + $priority = (int) $this->priority_egw2funambol[$taskData['info_priority']]; + } + else + { + $priority = (int) $this->priority_egw2ical[$taskData['info_priority']]; + } + $vevent->setAttribute('PRIORITY', $priority); $vcal->addComponent($vevent); $retval = $vcal->exportvCalendar(); - Horde::logMessage("exportVTODO:\n" . print_r($retval, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($retval)."\n",3,$this->logfile); + } + // Horde::logMessage("exportVTODO:\n" . print_r($retval, true), + // __FILE__, __LINE__, PEAR_LOG_DEBUG); return $retval; } @@ -252,7 +331,7 @@ class infolog_ical extends infolog_bo */ function importVTODO(&$_vcalData, $_taskID=-1, $merge=false) { - if (!$taskData = $this->vtodotoegw($_vcalData,$_taskID)) return false; + if (!($taskData = $this->vtodotoegw($_vcalData,$_taskID))) return false; // we suppose that a not set status in a vtodo means that the task did not started yet if (empty($taskData['info_status'])) @@ -265,6 +344,12 @@ class infolog_ical extends infolog_bo $taskData['info_datecompleted'] = 0; } + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($taskData)."\n",3,$this->logfile); + } + return $this->write($taskData); } @@ -299,8 +384,23 @@ class infolog_ical extends infolog_bo */ function vtodotoegw($_vcalData, $_taskID=-1) { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($_vcalData)."\n",3,$this->logfile); + } + $vcal = new Horde_iCalendar; - if (!($vcal->parsevCalendar($_vcalData))) return false; + if (!($vcal->parsevCalendar($_vcalData))) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "(): No vCalendar Container found!\n",3,$this->logfile); + } + return false; + } + $version = $vcal->getAttribute('VERSION'); if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) @@ -312,11 +412,22 @@ class infolog_ical extends infolog_bo $minimum_uid_length = 8; } - $components = $vcal->getComponents(); - - foreach ($components as $component) + foreach ($vcal->getComponents() as $component) { - if (is_a($component, 'Horde_iCalendar_vtodo')) + 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) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. + "(): Not a vTODO container, skipping...\n",3,$this->logfile); + } + } + else { $taskData = array(); $taskData['info_type'] = 'task'; @@ -328,7 +439,8 @@ class infolog_ical extends infolog_bo foreach ($component->_attributes as $attributes) { //$attributes['value'] = trim($attributes['value']); - if (empty($attributes['value'])) continue; + if (!strlen($attributes['value'])) continue; + switch ($attributes['name']) { case 'CLASS': @@ -378,8 +490,17 @@ class infolog_ical extends infolog_bo break; case 'PRIORITY': - if (1 <= $attributes['value'] && $attributes['value'] <= 9) { - $taskData['info_priority'] = $this->vcal_priority2egw_priority[$attributes['value']]; + 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 } @@ -462,19 +583,49 @@ class infolog_ical extends infolog_bo break; case 'text/x-vnote': + if (!empty($note['info_cat'])) + { + $cats = $this->get_categories(array($note['info_cat'])); + $note['info_cat'] = $GLOBALS['egw']->translation->convert($cats[0], + $GLOBALS['egw']->translation->charset(), 'UTF-8'); + } $vnote = new Horde_iCalendar_vnote(); - $options = array('CHARSET' => 'UTF-8'); $vNote->setAttribute('VERSION', '1.1'); - foreach (array( 'SUMMARY' => $note['info_subject'], - 'BODY' => $note['info_des'], + foreach (array( 'SUMMARY' => $note['info_subject'], + 'BODY' => $note['info_des'], + 'CATEGORIES' => $note['info_cat'], ) as $field => $value) { - $vnote->setAttribute($field, $value); - if ($this->productManufacturer != 'groupdav' - && preg_match('/([\177-\377])/', $value)) + $options = array(); + if (preg_match('/[^\x20-\x7F]/', $value)) { - $vevent->setParameter($field, $options); + $options['CHARSET'] = 'UTF-8'; + switch ($this->productManufacturer) + { + case 'groupdav': + if ($this->productName == 'kde') + { + $options['ENCODING'] = 'QUOTED-PRINTABLE'; + } + else + { + $options['CHARSET'] = ''; + + if (preg_match('/([\000-\012\015\016\020-\037\075])/', $value)) + { + $options['ENCODING'] = 'QUOTED-PRINTABLE'; + } + else + { + $options['ENCODING'] = ''; + } + } + break; + case 'funambol': + $options['ENCODING'] = 'FUNAMBOL-QP'; + } } + $vevent->setAttribute($field, $value, $options); } if ($note['info_startdate']) { @@ -483,22 +634,15 @@ class infolog_ical extends infolog_bo $vnote->setAttribute('DCREATED',$GLOBALS['egw']->contenthistory->getTSforAction('infolog_note',$_noteID,'add')); $vnote->setAttribute('LAST-MODIFIED',$GLOBALS['egw']->contenthistory->getTSforAction('infolog_note',$_noteID,'modify')); - if (!empty($note['info_cat'])) - { - $cats = $this->get_categories(array($note['info_cat'])); - $value = $cats[0]; - $vnote->setAttribute('CATEGORIES', $value); - if ($this->productManufacturer != 'groupdav' - && preg_match('/([\177-\377])/', $value)) - { - $vevent->setParameter('CATEGORIES', $options); - } - } - #$vnote->setAttribute('CLASS',$taskData['info_access'] == 'public' ? 'PUBLIC' : 'PRIVATE'); - return $vnote->exportvCalendar(); - break; + $retval = $vnote->exportvCalendar(); + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($retval)."\n",3,$this->logfile); + } + return $retval; } return false; } @@ -514,13 +658,24 @@ class infolog_ical extends infolog_bo */ function importVNOTE(&$_vcalData, $_type, $_noteID=-1, $merge=false) { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($_vcalData)."\n",3,$this->logfile); + } + if (!($note = $this->vnotetoegw($_vcalData, $_type, $_noteID))) return false; if($_noteID > 0) $note['info_id'] = $_noteID; if (empty($note['info_status'])) $note['info_status'] = 'done'; - #_debug_array($taskData);exit; + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."()\n" . + array2string($note)."\n",3,$this->logfile); + } + return $this->write($note); } diff --git a/phpgwapi/inc/horde/Horde/SyncML/Command/Sync/ContentSyncElement.php b/phpgwapi/inc/horde/Horde/SyncML/Command/Sync/ContentSyncElement.php index 4d6f7fac20..8a4ee363ce 100644 --- a/phpgwapi/inc/horde/Horde/SyncML/Command/Sync/ContentSyncElement.php +++ b/phpgwapi/inc/horde/Horde/SyncML/Command/Sync/ContentSyncElement.php @@ -48,7 +48,7 @@ class Horde_SyncML_Command_Sync_ContentSyncElement extends Horde_SyncML_Command_ } */ if (isset($this->_content) && !$this->_moreData) { - $this->_content = trim($this->_content); + //$this->_content = trim($this->_content); $this->_contentSize = strlen($this->_content); if (strtolower($this->_contentFormat) == 'b64') { $this->_content = base64_encode($this->_content); diff --git a/phpgwapi/inc/horde/Horde/iCalendar.php b/phpgwapi/inc/horde/Horde/iCalendar.php index 94efde396d..4850c85162 100644 --- a/phpgwapi/inc/horde/Horde/iCalendar.php +++ b/phpgwapi/inc/horde/Horde/iCalendar.php @@ -520,14 +520,20 @@ class Horde_iCalendar { * * @return boolean True on successful import, false otherwise. */ - function parsevCalendar($text, $base = 'VCALENDAR', $charset = null, - $clear = true) + function parsevCalendar($text, $base = 'VCALENDAR', $charset = null, $clear = true) { if ($clear) { $this->clear(); } - if (preg_match('/^BEGIN:' . $base . '(.*)^END:' . $base . '/ism', $text, $matches)) { + if ($base == 'VTODO' && + preg_match('/^BEGIN:VTODO(.*)^END:VEVENT/ism', $text, $matches)) { + // Workaround for Funambol VTODO bug in Mozilla Sync Plugins + Horde::logMessage('iCalendar: Funambol VTODO-bug detected, workaround activated...', + __FILE__, __LINE__, PEAR_LOG_WARNING); + $container = true; + $vCal = $matches[1]; + } elseif (preg_match('/^BEGIN:' . $base . '(.*)^END:' . $base . '/ism', $text, $matches)) { $container = true; $vCal = $matches[1]; } else { @@ -543,12 +549,14 @@ class Horde_iCalendar { $this->setAttribute('VERSION', $matches[1]); } - // Preserve a trailing CR + // Preserve a trailing CR $vCal = trim($vCal) . "\n"; // All subcomponents. $matches = null; - if (preg_match_all('/^BEGIN:(.*)(\r\n|\r|\n)(.*)^END:\1/Uims', $vCal, $matches)) { + // Workaround for Funambol VTODO bug in Mozilla Sync Plugins + if (preg_match_all('/^BEGIN:(VTODO)(\r\n|\r|\n)(.*)^END:VEVENT/Uims', $vCal, $matches) || + preg_match_all('/^BEGIN:(.*)(\r\n|\r|\n)(.*)^END:\1/Uims', $vCal, $matches)) { // vTimezone components are processed first. They are // needed to process vEvents that may use a TZID. foreach ($matches[0] as $key => $data) { @@ -602,6 +610,7 @@ class Horde_iCalendar { // Unfold any folded lines. if ($this->isOldFormat()) { + // old formats force folding at whitespace which must therefore be preserved $vCal = preg_replace('/[\r\n]+([ \t])/', '\1', $vCal); } else { $vCal = preg_replace('/[\r\n]+[ \t]/', '', $vCal); @@ -640,222 +649,224 @@ class Horde_iCalendar { if (isset($params['BASE64'])) { $params['ENCODING'] = 'BASE64'; } - if (isset($params['ENCODING'])) { - switch (String::upper($params['ENCODING'])) { - case 'Q': - case 'QUOTED-PRINTABLE': - $value = quoted_printable_decode($value); - if (isset($params['CHARSET'])) { - $value = $GLOBALS['egw']->translation->convert($value, $params['CHARSET']); - } else { - $value = $GLOBALS['egw']->translation->convert($value, - empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset); - } - break; - case 'B': - case 'BASE64': - $value = base64_decode($value); - break; - } - } elseif (isset($params['CHARSET'])) { - $value = $GLOBALS['egw']->translation->convert($value, $params['CHARSET']); - } else { - // As per RFC 2279, assume UTF8 if we don't have an - // explicit charset parameter. - $value = $GLOBALS['egw']->translation->convert($value, - empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset); - } + if (isset($params['ENCODING'])) { + switch (String::upper($params['ENCODING'])) { + case 'Q': + case 'QUOTED-PRINTABLE': + $value = quoted_printable_decode($value); + if (isset($params['CHARSET'])) { + $value = $GLOBALS['egw']->translation->convert($value, $params['CHARSET']); + } else { + $value = $GLOBALS['egw']->translation->convert($value, + empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset); + } + // Funambol hack :-( + $value = str_replace('\\\\n', "\n", $value); + break; + case 'B': + case 'BASE64': + $value = base64_decode($value); + break; + } + } elseif (isset($params['CHARSET'])) { + $value = $GLOBALS['egw']->translation->convert($value, $params['CHARSET']); + } else { + // As per RFC 2279, assume UTF8 if we don't have an + // explicit charset parameter. + $value = $GLOBALS['egw']->translation->convert($value, + empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset); + } - // Get timezone info for date fields from $params. - $tzid = isset($params['TZID']) ? trim($params['TZID'], '\"') : false; + // Get timezone info for date fields from $params. + $tzid = isset($params['TZID']) ? trim($params['TZID'], '\"') : false; - switch ($tag) { - case 'VERSION': // already processed + switch ($tag) { + case 'VERSION': // already processed + break; + // Date fields. + case 'COMPLETED': + case 'CREATED': + case 'LAST-MODIFIED': + $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params); + break; + + case 'BDAY': + case 'X-SYNCJE-ANNIVERSARY': + $this->setAttribute($tag, $value, $params, true, $this->_parseDate($value)); + break; + + case 'DTEND': + case 'DTSTART': + case 'DTSTAMP': + case 'DUE': + case 'AALARM': + case 'DALARM': + case 'RECURRENCE-ID': + case 'X-RECURRENCE-ID': + // types like AALARM may contain additional data after a ; + // ignore these. + $ts = explode(';', $value); + if (isset($params['VALUE']) && $params['VALUE'] == 'DATE') { + $isDate = true; + $this->setAttribute($tag, $this->_parseDateTime($ts[0], $tzid), $params, true, $this->_parseDate($ts[0])); + } else { + $this->setAttribute($tag, $this->_parseDateTime($ts[0], $tzid), $params); + } + break; + + case 'TRIGGER': + if (isset($params['VALUE'])) { + if ($params['VALUE'] == 'DATE-TIME') { + $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params); + } else { + $this->setAttribute($tag, $this->_parseDuration($value), $params); + } + } else { + $this->setAttribute($tag, $this->_parseDuration($value), $params); + } + break; + + // Comma or semicolon seperated dates. + case 'EXDATE': + case 'RDATE': + $dates = array(); + preg_match_all('/[;,]([^;,]*)/', ';' . $value, $values); + + foreach ($values[1] as $value) { + if ((isset($params['VALUE']) + && $params['VALUE'] == 'DATE') || (!isset($params['VALUE']) && $isDate)) { + $dates[] = $this->_parseDate($value); + } else { + $dates[] = $this->_parseDateTime($value, $tzid); + } + } + $this->setAttribute($tag, isset($dates[0]) ? $dates[0] : null, $params, true, $dates); + break; + + // Duration fields. + case 'DURATION': + $this->setAttribute($tag, $this->_parseDuration($value), $params); + break; + + // Period of time fields. + case 'FREEBUSY': + $periods = array(); + preg_match_all('/,([^,]*)/', ',' . $value, $values); + foreach ($values[1] as $value) { + $periods[] = $this->_parsePeriod($value); + } + + $this->setAttribute($tag, isset($periods[0]) ? $periods[0] : null, $params, true, $periods); + break; + + // UTC offset fields. + case 'TZOFFSETFROM': + case 'TZOFFSETTO': + $this->setAttribute($tag, $this->_parseUtcOffset($value), $params); + break; + + // Integer fields. + case 'PERCENT-COMPLETE': + case 'PRIORITY': + case 'REPEAT': + case 'SEQUENCE': + $this->setAttribute($tag, intval($value), $params); + break; + + // Geo fields. + case 'GEO': + if ($this->isOldFormat()) { + $floats = explode(',', $value); + $value = array('latitude' => floatval($floats[1]), + 'longitude' => floatval($floats[0])); + } else { + $floats = explode(';', $value); + $value = array('latitude' => floatval($floats[0]), + 'longitude' => floatval($floats[1])); + } + $this->setAttribute($tag, $value, $params); + break; + + // Recursion fields. # add more flexibility + #case 'EXRULE': + #case 'RRULE': + # $this->setAttribute($tag, trim($value), $params); + # break; + + // Binary fields. + case 'PHOTO': + $this->setAttribute($tag, $value, $params); + break; + + // ADR, ORG and N are lists seperated by unescaped semicolons + // with a specific number of slots. + case 'ADR': + case 'N': + case 'ORG': + $value = trim($value); + // As of rfc 2426 2.4.2 semicolon, comma, and colon must + // be escaped (comma is unescaped after splitting below). + $value = str_replace(array('\\n', '\\N', '\\;', '\\:'), + array("\n", "\n", ';', ':'), + $value); + + // Split by unescaped semicolons: + $values = preg_split('/(?setAttribute($tag, trim($value), $params, true, $values); + break; + + // CATEGORIES is a lists seperated by unescaped commas + // with a unspecific number of slots. + case 'CATEGORIES': + $value = trim($value); + // As of rfc 2426 2.4.2 semicolon, comma, and colon must + // be escaped (semicolon is unescaped after splitting below). + $value = str_replace(array('\\n', '\\N', '\\,', '\\:'), + array("\n", "\n", ',', ':'), + $value); + + // Split by unescaped commas: + $values = preg_split('/(?setAttribute($tag, trim($value), $params, true, $values); + break; + + // String fields. + default: + if ($this->isOldFormat()) { + // vCalendar 1.0 and vCard 2.1 only escape semicolons + // and use unescaped semicolons to create lists. + $value = trim($value); + // Split by unescaped semicolons: + $values = preg_split('/(?setAttribute($tag, trim($value), $params, true, $values); + } else { + $value = trim($value); + // As of rfc 2426 2.4.2 semicolon, comma, and colon + // must be escaped (comma is unescaped after splitting + // below). + $value = str_replace(array('\\n', '\\N', '\\;', '\\:', '\\\\'), + array("\n", "\n", ';', ':', '\\'), + $value); + + // Split by unescaped commas. + $values = preg_split('/(?setAttribute($tag, trim($value), $params, true, $values); + } break; - // Date fields. - case 'COMPLETED': - case 'CREATED': - case 'LAST-MODIFIED': - $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params); - break; - - case 'BDAY': - case 'X-SYNCJE-ANNIVERSARY': - $this->setAttribute($tag, $value, $params, true, $this->_parseDate($value)); - break; - - case 'DTEND': - case 'DTSTART': - case 'DTSTAMP': - case 'DUE': - case 'AALARM': - case 'DALARM': - case 'RECURRENCE-ID': - case 'X-RECURRENCE-ID': - // types like AALARM may contain additional data after a ; - // ignore these. - $ts = explode(';', $value); - if (isset($params['VALUE']) && $params['VALUE'] == 'DATE') { - $isDate = true; - $this->setAttribute($tag, $this->_parseDateTime($ts[0], $tzid), $params, true, $this->_parseDate($ts[0])); - } else { - $this->setAttribute($tag, $this->_parseDateTime($ts[0], $tzid), $params); - } - break; - - case 'TRIGGER': - if (isset($params['VALUE'])) { - if ($params['VALUE'] == 'DATE-TIME') { - $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params); - } else { - $this->setAttribute($tag, $this->_parseDuration($value), $params); - } - } else { - $this->setAttribute($tag, $this->_parseDuration($value), $params); - } - break; - - // Comma or semicolon seperated dates. - case 'EXDATE': - case 'RDATE': - $dates = array(); - preg_match_all('/[;,]([^;,]*)/', ';' . $value, $values); - - foreach ($values[1] as $value) { - if ((isset($params['VALUE']) - && $params['VALUE'] == 'DATE') || (!isset($params['VALUE']) && $isDate)) { - $dates[] = $this->_parseDate($value); - } else { - $dates[] = $this->_parseDateTime($value, $tzid); - } - } - $this->setAttribute($tag, isset($dates[0]) ? $dates[0] : null, $params, true, $dates); - break; - - // Duration fields. - case 'DURATION': - $this->setAttribute($tag, $this->_parseDuration($value), $params); - break; - - // Period of time fields. - case 'FREEBUSY': - $periods = array(); - preg_match_all('/,([^,]*)/', ',' . $value, $values); - foreach ($values[1] as $value) { - $periods[] = $this->_parsePeriod($value); - } - - $this->setAttribute($tag, isset($periods[0]) ? $periods[0] : null, $params, true, $periods); - break; - - // UTC offset fields. - case 'TZOFFSETFROM': - case 'TZOFFSETTO': - $this->setAttribute($tag, $this->_parseUtcOffset($value), $params); - break; - - // Integer fields. - case 'PERCENT-COMPLETE': - case 'PRIORITY': - case 'REPEAT': - case 'SEQUENCE': - $this->setAttribute($tag, intval($value), $params); - break; - - // Geo fields. - case 'GEO': - if ($this->isOldFormat()) { - $floats = explode(',', $value); - $value = array('latitude' => floatval($floats[1]), - 'longitude' => floatval($floats[0])); - } else { - $floats = explode(';', $value); - $value = array('latitude' => floatval($floats[0]), - 'longitude' => floatval($floats[1])); - } - $this->setAttribute($tag, $value, $params); - break; - - // Recursion fields. # add more flexibility - #case 'EXRULE': - #case 'RRULE': - # $this->setAttribute($tag, trim($value), $params); - # break; - - // Binary fields. - case 'PHOTO': - $this->setAttribute($tag, $value, $params); - break; - - // ADR, ORG and N are lists seperated by unescaped semicolons - // with a specific number of slots. - case 'ADR': - case 'N': - case 'ORG': - $value = trim($value); - // As of rfc 2426 2.4.2 semicolon, comma, and colon must - // be escaped (comma is unescaped after splitting below). - $value = str_replace(array('\\n', '\\N', '\\;', '\\:'), - array("\n", "\n", ';', ':'), - $value); - - // Split by unescaped semicolons: - $values = preg_split('/(?setAttribute($tag, trim($value), $params, true, $values); - break; - - // CATEGORIES is a lists seperated by unescaped commas - // with a unspecific number of slots. - case 'CATEGORIES': - $value = trim($value); - // As of rfc 2426 2.4.2 semicolon, comma, and colon must - // be escaped (semicolon is unescaped after splitting below). - $value = str_replace(array('\\n', '\\N', '\\,', '\\:'), - array("\n", "\n", ',', ':'), - $value); - - // Split by unescaped commas: - $values = preg_split('/(?setAttribute($tag, trim($value), $params, true, $values); - break; - - // String fields. - default: - if ($this->isOldFormat()) { - // vCalendar 1.0 and vCard 2.1 only escape semicolons - // and use unescaped semicolons to create lists. - $value = trim($value); - // Split by unescaped semicolons: - $values = preg_split('/(?setAttribute($tag, trim($value), $params, true, $values); - } else { - $value = trim($value); - // As of rfc 2426 2.4.2 semicolon, comma, and colon - // must be escaped (comma is unescaped after splitting - // below). - $value = str_replace(array('\\n', '\\N', '\\;', '\\:', '\\\\'), - array("\n", "\n", ';', ':', '\\'), - $value); - - // Split by unescaped commas. - $values = preg_split('/(?setAttribute($tag, trim($value), $params, true, $values); - } - break; - } + } } } @@ -899,7 +910,7 @@ class Horde_iCalendar { && (!$this->isOldFormat() || empty($param_value))) { continue; } - if ($param_name == 'ENCODING' && empty($param_value)) { + if ($param_name == 'ENCODING') { continue; } /* Skip VALUE=DATE for vCalendar 1.0 data, not allowed. */ @@ -1080,20 +1091,20 @@ class Horde_iCalendar { // Text containing newlines or ASCII >= 127 must be BASE64 // or QUOTED-PRINTABLE encoded. Currently we use // QUOTED-PRINTABLE as default. - if (preg_match("/[^\x20-\x7F]/", $value) && - !isset($params['ENCODING'])) { - $params['ENCODING'] = 'QUOTED-PRINTABLE'; - $params_str .= ';ENCODING=QUOTED-PRINTABLE'; - // Add CHARSET as well. At least the synthesis client - // gets confused otherwise - if (!isset($params['CHARSET'])) { - $params['CHARSET'] = NLS::getCharset(); - $params_str .= ';CHARSET=' . $params['CHARSET']; - } + if (preg_match('/[^\x20-\x7F]/', $value) && + !isset($params['ENCODING'])) { + $params['ENCODING'] = 'QUOTED-PRINTABLE'; + } + if (preg_match('/([\177-\377])/', $value) && + !isset($params['CHARSET'])) { + // Add CHARSET as well. At least the synthesis client + // gets confused otherwise + $params['CHARSET'] = NLS::getCharset(); + $params_str .= ';CHARSET=' . $params['CHARSET']; } } else { if (is_array($attribute['values']) && - count($attribute['values'])) { + count($attribute['values']) > 1) { $values = $attribute['values']; if ($name == 'N' || $name == 'ADR' || $name == 'ORG') { $glue = ';'; @@ -1117,31 +1128,55 @@ class Horde_iCalendar { } } - if (!empty($params['ENCODING']) && strlen(trim($value))) { - switch($params['ENCODING']) { - case 'Q': - case 'QUOTED-PRINTABLE': - $value = str_replace("\r", '', $value); - $result .= $name . $params_str . ':' - . str_replace('=0A', '=0D=0A', - $this->_quotedPrintableEncode($value)) - . $this->_newline; - break; - case 'B': - case 'BASE64': - $attr_string = $name . $params_str . ":" . $this->_newline . ' ' . $this->_base64Encode($value); - $attr_string = String::wordwrap($attr_string, 75, $this->_newline . ' ', - true, 'utf-8', true); - $result .= $attr_string . $this->_newline; - if ($this->isOldFormat()) { - $result .= $this->_newline; // Append an empty line - } - break; - } - } else { + $encoding = (!empty($params['ENCODING']) && strlen(trim($value)) > 0); + + if ($encoding) { + switch($params['ENCODING']) { + case 'Q': + case 'QUOTED-PRINTABLE': + if (!$this->isOldFormat()) + { + $enconding = false; + break; + } + $params_str .= ';ENCODING=' . $params['ENCODING']; + $value = str_replace("\r", '', $value); + $result .= $name . $params_str . ':' + . str_replace('=0A', '=0D=0A', + $this->_quotedPrintableEncode($value)) + . $this->_newline; + break; + case 'FUNAMBOL-QP': + // Funambol needs some special quoting + $value = str_replace(array('<', "\r"), array('<', ''), $value); + if (!$this->isOldFormat()) + { + $encoding = false; + break; + } + $params_str .= ';ENCODING=QUOTED-PRINTABLE'; + $result .= $name . $params_str . ':' + . str_replace('=0A', '=0D=0A', + $this->_quotedPrintableEncode($value, false)) + . $this->_newline; + break; + case 'B': + case 'BASE64': + $params_str .= ';ENCODING=' . $params['ENCODING']; + $attr_string = $name . $params_str . ':' . $this->_newline . ' ' . $this->_base64Encode($value); + $attr_string = String::wordwrap($attr_string, 75, $this->_newline . ' ', + true, 'utf-8', true); + $result .= $attr_string . $this->_newline; + if ($this->isOldFormat()) { + $result .= $this->_newline; // Append an empty line + } + } + } + + if (!$encoding) { $value = str_replace(array("\r", "\n"), array('', '\\n'), $value); $attr_string = $name . $params_str; - if (!empty($value) || $value === 0 || (is_string($value) && strlen($value) > 0)) { + if (strlen($value) > 0) { $attr_string .= ':' . $value; } elseif ($name != 'RRULE') { $attr_string .= ':'; @@ -1537,7 +1572,7 @@ class Horde_iCalendar { * * @return string The quoted-printable encoded string. */ - function _quotedPrintableEncode($input = '') + function _quotedPrintableEncode($input = '', $withFolding=true) { $output = $line = ''; $len = strlen($input); @@ -1555,7 +1590,7 @@ class Horde_iCalendar { } $line .= $chunk; // Wrap long lines (rule 5) - if (strlen($line) + 1 > 76) { + if ($withFolding && strlen($line) + 1 > 76) { $line = String::wordwrap($line, 75, "=\r\n", true, 'us-ascii', true); $newline = strrchr($line, "\r\n"); if ($newline !== false) { @@ -1567,7 +1602,7 @@ class Horde_iCalendar { continue; } // Wrap at line breaks for better readability (rule 4). - if (substr($line, -3) == '=0A') { + if ($withFolding && substr($line, -3) == '=0A') { $output .= $line . "=\r\n"; $line = ''; }