generalize VEVENT/VTODO handling
[tine20] / tine20 / Tinebase / Convert / VCalendar / Abstract.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Tinebase
6  * @subpackage  Convert
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Philipp Schüle <p.schuele@metaways.de>
9  * @copyright   Copyright (c) 2014 Metaways Infosystems GmbH (http://www.metaways.de)
10  *
11  */
12
13 /**
14  * abstract class for VCALENDAR/VTODO/VCARD/... conversion
15  *
16  * @package     Tinebase
17  * @subpackage  Convert
18  */
19 abstract class Tinebase_Convert_VCalendar_Abstract
20 {
21     /**
22      * use servers modlogProperties instead of given DTSTAMP & SEQUENCE
23      * use this if the concurrency checks are done differntly like in CalDAV
24      * where the etag is checked
25      */
26     const OPTION_USE_SERVER_MODLOG = 'useServerModlog';
27     
28     protected $_supportedFields = array();
29     
30     protected $_version;
31     
32     protected $_modelName = null;
33     
34     /**
35      * @param  string  $version  the version of the client
36      */
37     public function __construct($version = null)
38     {
39         if (! $this->_modelName) {
40             throw new Tinebase_Exception('modelName is required');
41         }
42         $this->_version = $version;
43     }
44
45     /**
46      * returns VObject of input data
47      * 
48      * @param   mixed  $blob
49      * @return  \Sabre\VObject\Component\VCalendar
50      */
51     public static function getVObject($blob)
52     {
53         if ($blob instanceof \Sabre\VObject\Component\VCalendar) {
54             return $blob;
55         }
56         
57         if (is_resource($blob)) {
58             $blob = stream_get_contents($blob);
59         }
60         
61         $blob = Tinebase_Core::filterInputForDatabase($blob);
62         
63         $vcalendar = self::readVCalBlob($blob);
64         
65         return $vcalendar;
66     }
67     
68     /**
69      * reads vcal blob and tries to repair some parsing problems that Sabre has
70      *
71      * @param string $blob
72      * @param integer $failcount
73      * @param integer $spacecount
74      * @param integer $lastBrokenLineNumber
75      * @param array $lastLines
76      * @throws Sabre\VObject\ParseException
77      * @return Sabre\VObject\Component\VCalendar
78      *
79      * @see 0006110: handle iMIP messages from outlook
80      *
81      * @todo maybe we can remove this when #7438 is resolved
82      */
83     public static function readVCalBlob($blob, $failcount = 0, $spacecount = 0, $lastBrokenLineNumber = 0, $lastLines = array())
84     {
85         // convert to utf-8
86         $blob = mbConvertTo($blob);
87     
88         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
89                 ' ' . $blob);
90     
91         try {
92             $vcalendar = \Sabre\VObject\Reader::read($blob);
93         } catch (Sabre\VObject\ParseException $svpe) {
94             // NOTE: we try to repair\Sabre\VObject\Reader as it fails to detect followup lines that do not begin with a space or tab
95             if ($failcount < 10 && preg_match(
96                     '/Invalid VObject, line ([0-9]+) did not follow the icalendar\/vcard format/', $svpe->getMessage(), $matches
97             )) {
98                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
99                         ' ' . $svpe->getMessage() .
100                         ' lastBrokenLineNumber: ' . $lastBrokenLineNumber);
101     
102                 $brokenLineNumber = $matches[1] - 1 + $spacecount;
103     
104                 if ($lastBrokenLineNumber === $brokenLineNumber) {
105                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
106                             ' Try again: concat this line to previous line.');
107                     $lines = $lastLines;
108                     $brokenLineNumber--;
109                     // increase spacecount because one line got removed
110                     $spacecount++;
111                 } else {
112                     $lines = preg_split('/[\r\n]*\n/', $blob);
113                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
114                             ' Concat next line to this one.');
115                     $lastLines = $lines; // for retry
116                 }
117                 $lines[$brokenLineNumber] .= $lines[$brokenLineNumber + 1];
118                 unset($lines[$brokenLineNumber + 1]);
119     
120                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
121                         ' failcount: ' . $failcount .
122                         ' brokenLineNumber: ' . $brokenLineNumber .
123                         ' spacecount: ' . $spacecount);
124     
125                 $vcalendar = self::readVCalBlob(implode("\n", $lines), $failcount + 1, $spacecount, $brokenLineNumber, $lastLines);
126             } else {
127                 throw $svpe;
128             }
129         }
130     
131         return $vcalendar;
132     }
133     
134     /**
135      * to be overwriten in extended classes to modify/cleanup $_vcalendar
136      *
137      * @param \Sabre\VObject\Component\VCalendar $vcalendar
138      */
139     protected function _afterFromTine20Model(\Sabre\VObject\Component\VCalendar $vcalendar)
140     {
141     }
142     
143     /**
144      * parse valarm properties
145      * 
146      * @param Tinebase_Record_Abstract $record
147      * @param iteratable $valarms
148      * @param \Sabre\VObject\Component $vcalendar
149      */
150     protected function _parseAlarm(Tinebase_Record_Abstract $record, $valarms, \Sabre\VObject\Component $vcomponent)
151     {
152         foreach ($valarms as $valarm) {
153             
154             if ($valarm->ACTION == 'NONE') {
155                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
156                         . ' We can\'t cope with action NONE: iCal 6.0 sends default alarms in the year 1976 with action NONE. Skipping alarm.');
157                 continue;
158             }
159             
160             if (! is_object($valarm->TRIGGER)) {
161                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
162                 . ' Alarm has no TRIGGER value. Skipping it.');
163                 continue;
164             }
165             
166             # TRIGGER:-PT15M
167             if (is_string($valarm->TRIGGER->getValue()) && $valarm->TRIGGER instanceof Sabre\VObject\Property\ICalendar\Duration) {
168                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
169                 . ' Adding DURATION trigger value for ' . $valarm->TRIGGER->getValue());
170                 $valarm->TRIGGER->add('VALUE', 'DURATION');
171             }
172             
173             $trigger = is_object($valarm->TRIGGER['VALUE']) ? $valarm->TRIGGER['VALUE'] : (is_object($valarm->TRIGGER['RELATED']) ? $valarm->TRIGGER['RELATED'] : NULL);
174             
175             if ($trigger === NULL) {
176                 // added Trigger/Related for eM Client alarms
177                 // 2014-01-03 - Bullshit, why don't we have testdata for emclient alarms?
178                         //              this alarm handling should be refactored, the logic is scrambled
179                 // @see 0006110: handle iMIP messages from outlook
180                 // @todo fix 0007446: handle broken alarm in outlook invitation message
181                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
182                 . ' Alarm has no TRIGGER value. Skipping it.');
183                 continue;
184             }
185             
186             switch (strtoupper($trigger->getValue())) {
187                 # TRIGGER;VALUE=DATE-TIME:20111031T130000Z
188                 case 'DATE-TIME':
189                     $alarmTime = new Tinebase_DateTime($valarm->TRIGGER->getValue());
190                     $alarmTime->setTimezone('UTC');
191                     
192                     $alarm = new Tinebase_Model_Alarm(array(
193                         'alarm_time'        => $alarmTime,
194                         'minutes_before'    => 'custom',
195                         'model'             => $this->_modelName,
196                     ));
197                     
198                     break;
199                 
200                 # TRIGGER;VALUE=DURATION:-PT1H15M
201                 case 'DURATION':
202                 default:
203                     $durationBaseTime = isset($vcomponent->DTSTART) ? $vcomponent->DTSTART : $vcomponent->DUE;
204                     $alarmTime = $this->_convertToTinebaseDateTime($durationBaseTime);
205                     $alarmTime->setTimezone('UTC');
206                     
207                     preg_match('/(?P<invert>[+-]?)(?P<spec>P.*)/', $valarm->TRIGGER->getValue(), $matches);
208                     $duration = new DateInterval($matches['spec']);
209                     $duration->invert = !!($matches['invert'] === '-');
210                     
211                     $alarm = new Tinebase_Model_Alarm(array(
212                         'alarm_time'        => $alarmTime->add($duration),
213                         'minutes_before'    => ($duration->format('%d') * 60 * 24) + ($duration->format('%h') * 60) + ($duration->format('%i')),
214                         'model'             => $this->_modelName,
215                     ));
216                     if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
217                         . ' Adding DURATION alarm ' . print_r($alarm->toArray(), true));
218             }
219             
220             if ($valarm->ACKNOWLEDGED) {
221                 $dtack = $valarm->ACKNOWLEDGED->getDateTime();
222                 Calendar_Controller_Alarm::setAcknowledgeTime($alarm, $dtack);
223             }
224             
225             $record->alarms->addRecord($alarm);
226         }
227     }
228     
229     /**
230      * get datetime from sabredav datetime property (user TZ is fallback)
231      * 
232      * @param  Sabre\VObject\Property  $dateTimeProperty
233      * @param  boolean                 $_useUserTZ
234      * @return Tinebase_DateTime
235      * 
236      * @todo try to guess some common timezones
237      */
238     protected function _convertToTinebaseDateTime(\Sabre\VObject\Property $dateTimeProperty, $_useUserTZ = FALSE)
239     {
240         $defaultTimezone = date_default_timezone_get();
241         date_default_timezone_set((string) Tinebase_Core::get(Tinebase_Core::USERTIMEZONE));
242         
243         if ($dateTimeProperty instanceof Sabre\VObject\Property\ICalendar\DateTime) {
244             $dateTime = $dateTimeProperty->getDateTime();
245             $tz = ($_useUserTZ || (isset($dateTimeProperty['VALUE']) && strtoupper($dateTimeProperty['VALUE']) == 'DATE')) ? 
246                 (string) Tinebase_Core::get(Tinebase_Core::USERTIMEZONE) : 
247                 $dateTime->getTimezone();
248             
249             $result = new Tinebase_DateTime($dateTime->format(Tinebase_Record_Abstract::ISO8601LONG), $tz);
250         } else {
251             $result = new Tinebase_DateTime($dateTimeProperty->getValue());
252         }
253         
254         date_default_timezone_set($defaultTimezone);
255         
256         return $result;
257     }
258 }