improve default attendee for CalDAV/ActiveSync
[tine20] / tine20 / Calendar / Frontend / WebDAV / Event.php
1 <?php
2
3 use Sabre\VObject;
4
5 /**
6  * Tine 2.0
7  *
8  * @package     Calendar
9  * @subpackage  Frontend
10  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
11  * @author      Lars Kneschke <l.kneschke@metaways.de>
12  * @copyright   Copyright (c) 2011-2013 Metaways Infosystems GmbH (http://www.metaways.de)
13  */
14
15 /**
16  * class to handle a single event
17  *
18  * This class handles the creation, update and deletion of vevents
19  *
20  * @package     Calendar
21  * @subpackage  Frontend
22  */
23 class Calendar_Frontend_WebDAV_Event extends Sabre\DAV\File implements Sabre\CalDAV\ICalendarObject, Sabre\DAVACL\IACL
24 {
25     /**
26      * @var Tinebase_Model_Container
27      */
28     protected $_container;
29     
30     /**
31      * @var Calendar_Model_Event
32      */
33     protected $_event;
34     
35     /**
36      * holds the vevent returned to the client
37      * 
38      * @var string
39      */
40     protected $_vevent;
41     
42     /**
43      * @var Calendar_Convert_Event_VCalendar
44      */
45     protected $_converter;
46     
47     /**
48      * Constructor 
49      * 
50      * @param  string|Calendar_Model_Event  $_event  the id of a event or the event itself 
51      */
52     public function __construct(Tinebase_Model_Container $_container, $_event = null) 
53     {
54         $this->_container = $_container;
55         $this->_event     = $_event;
56         
57         if (! $this->_event instanceof Calendar_Model_Event) {
58             $this->_event = ($pos = strpos($this->_event, '.')) === false ? $this->_event : substr($this->_event, 0, $pos);
59         }
60         
61         list($backend, $version) = Calendar_Convert_Event_VCalendar_Factory::parseUserAgent($_SERVER['HTTP_USER_AGENT']);
62         
63         $this->_assertEventFacadeParams($this->_container);
64         
65         $this->_converter = Calendar_Convert_Event_VCalendar_Factory::factory($backend, $version);
66     }
67     
68     /**
69      * add attachment to event
70      * 
71      * @param string $name
72      * @param string $contentType
73      * @param stream $attachment
74      * @return string  id of attachment
75      */
76     public function addAttachment($rid, $name, $contentType, $attachment)
77     {
78         $record = $this->getRecord();
79         
80         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
81             Tinebase_Core::getLogger()->DEBUG(__METHOD__ . '::' . __LINE__ . 
82                 " add attachment $name ($contentType) to event {$record->getId()}");
83         }
84         
85         $node = new Tinebase_Model_Tree_Node(array(
86             'name'         => $name,
87             'type'         => Tinebase_Model_Tree_Node::TYPE_FILE,
88             'contenttype'  => $contentType,
89             'stream'       => $attachment,
90         ), true);
91         
92         $record->attachments->addRecord($node);
93         
94         $this->_event = Calendar_Controller_MSEventFacade::getInstance()->update($record);
95         $newAttachmentNode = $this->_event->attachments->filter('name', $name)->getFirstRecord();
96         
97         return $newAttachmentNode->object_id;
98     }
99     
100     /**
101      * this function creates a Calendar_Model_Event and stores it in the database
102      * 
103      * @todo the header handling does not belong here. It should be moved to the DAV_Server class when supported
104      * 
105      * @param  Tinebase_Model_Container  $container
106      * @param  stream|string             $vobjectData
107      * @return Calendar_Frontend_WebDAV_Event
108      */
109     public static function create(Tinebase_Model_Container $container, $name, $vobjectData, $onlyCurrentUserOrganizer = false)
110     {
111         if (is_resource($vobjectData)) {
112             $vobjectData = stream_get_contents($vobjectData);
113         }
114         // Converting to UTF-8, if needed
115         $vobjectData = Sabre\DAV\StringUtil::ensureUTF8($vobjectData);
116         
117         #Sabre\CalDAV\ICalendarUtil::validateICalendarObject($vobjectData, array('VEVENT', 'VFREEBUSY'));
118         
119         list($backend, $version) = Calendar_Convert_Event_VCalendar_Factory::parseUserAgent($_SERVER['HTTP_USER_AGENT']);
120         
121         $event = Calendar_Convert_Event_VCalendar_Factory::factory($backend, $version)->toTine20Model($vobjectData);
122         
123         if (true === $onlyCurrentUserOrganizer) {
124             if ($event->organizer && $event->organizer != Tinebase_Core::getUser()->contact_id) {
125                 return null;
126             }
127         }
128         
129         $event->container_id = $container->getId();
130         $id = ($pos = strpos($name, '.')) === false ? $name : substr($name, 0, $pos);
131         if (strlen($id) > 40) {
132             $id = sha1($id);
133         }
134         
135         $event->setId($id);
136         
137         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
138             . " Event to create: " . print_r($event->toArray(), TRUE));
139         
140         self::_assertEventFacadeParams($container);
141         
142         // check if there is already an existing event with this ID
143         // this can happen when the invitation email is faster then the caldav update or
144         // or when an event gets moved to another container
145         
146         $filter = new Calendar_Model_EventFilter(array(
147             array(
148                 'field' => 'dtstart', 
149                 'operator' => 'equals', 
150                 'value' => $event->dtstart
151             ),
152             array(
153                 'field' => 'dtend', 
154                 'operator' => 'equals', 
155                 'value' => $event->dtend
156             ),
157             array('condition' => 'OR', 'filters' => array(
158                 array(
159                     'field'     => 'id',
160                     'operator'  => 'equals',
161                     'value'     => $id
162                 ),
163                 array(
164                     'field'     => 'uid',
165                     'operator'  => 'equals',
166                     'value'     => $id
167                 )
168             ))
169         ));
170         $existingEvent = Calendar_Controller_MSEventFacade::getInstance()->search($filter, null, false, false, 'sync')->getFirstRecord();
171         
172         if ($existingEvent === null) {
173             $event = Calendar_Controller_MSEventFacade::getInstance()->create($event);
174             
175             $vevent = new self($container, $event);
176         } else {
177             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
178                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' update existing event');
179             $vevent = new self($container, $existingEvent);
180             $vevent->put($vobjectData);
181         }
182         
183         return $vevent;
184     }
185     
186     /**
187      * Deletes the card
188      *
189      * @todo improve handling
190      * @return void
191      */
192     public function delete() 
193     {
194         // when a move occurs, thunderbird first sends to delete command and immediately a put command
195         // we must delay the delete command, otherwise the put command fails
196         sleep(1);
197         
198         // (re) fetch event as tree move does not refresh src node before delete
199         $this->_assertEventFacadeParams($this->_container);
200         $event = Calendar_Controller_MSEventFacade::getInstance()->get($this->_event);
201         
202         // disallow event cleanup in the past
203         if (max($event->dtend, $event->rrule_until) < Tinebase_DateTime::now()->subMonth(2)) {
204             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
205                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " deleting events in the past is not allowed via CalDAV");
206             return;
207         }
208         
209         // allow delete only if deleted in origin calendar
210         if ($event->container_id == $this->_container->getId()) {
211             if (strpos($_SERVER['REQUEST_URI'], Calendar_Frontend_CalDAV_ScheduleInbox::NAME) === false) {
212                 Calendar_Controller_MSEventFacade::getInstance()->delete($event->getId());
213             }
214         }
215         // implicitly DECLINE event 
216         else {
217             $attendee = $event->attendee instanceof Tinebase_Record_RecordSet ? 
218                 $event->attendee->filter('displaycontainer_id', $this->_container->getId())->getFirstRecord() :
219                 NULL;
220             
221             // NOTE: don't allow organizer to instantly delete after update, otherwise we can't handle move @see{Calendar_Frontend_WebDAV_EventTest::testMoveOriginPersonalToShared}
222             if ($attendee && ($attendee->user_id != $event->organizer || Tinebase_DateTime::now()->subSecond(20) > $event->last_modified_time)) {
223                 $attendee->status = Calendar_Model_Attender::STATUS_DECLINED;
224                 
225                 $this->_event = Calendar_Controller_MSEventFacade::getInstance()->update($event);
226             } 
227         }
228     }
229     
230     /**
231      * Returns the VCard-formatted object 
232      * 
233      * @return stream
234      */
235     public function get() 
236     {
237         $s = fopen('php://temp','r+');
238         fwrite($s, $this->_getVEvent());
239         rewind($s);
240         
241         return $s;
242     }
243     
244     /**
245      * Returns the uri for this object 
246      * 
247      * @return string 
248      */
249     public function getName() 
250     {
251         return $this->getRecord()->getId() . '.ics';
252     }
253     
254     /**
255      * Returns the owner principal
256      *
257      * This must be a url to a principal, or null if there's no owner 
258      * 
259      * @todo add real owner
260      * @return string|null
261      */
262     public function getOwner() 
263     {
264         return null;
265         return $this->addressBookInfo['principaluri'];
266     }
267
268     /**
269      * Returns a group principal
270      *
271      * This must be a url to a principal, or null if there's no owner
272      * 
273      * @todo add real group
274      * @return string|null 
275      */
276     public function getGroup() 
277     {
278         return null;
279     }
280     
281     /**
282      * Returns a list of ACE's for this node.
283      *
284      * Each ACE has the following properties:
285      *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are 
286      *     currently the only supported privileges
287      *   * 'principal', a url to the principal who owns the node
288      *   * 'protected' (optional), indicating that this ACE is not allowed to 
289      *      be updated. 
290      * 
291      * @todo add the real logic
292      * @return array 
293      */
294     public function getACL() 
295     {
296         return null;
297         
298         return array(
299             array(
300                 'privilege' => '{DAV:}read',
301                 'principal' => $this->addressBookInfo['principaluri'],
302                 'protected' => true,
303             ),
304             array(
305                 'privilege' => '{DAV:}write',
306                 'principal' => $this->addressBookInfo['principaluri'],
307                 'protected' => true,
308             ),
309         );
310
311     }
312     
313     /**
314      * Returns the mime content-type
315      *
316      * @return string
317      */
318     public function getContentType() {
319     
320         return 'text/calendar';
321     
322     }
323     
324     /**
325      * Returns an ETag for this object
326      *
327      * How to calculate the etag?
328      * The etag consists of 2 parts. The part, which is equal for all users (subject, dtstart, dtend, ...) and
329      * the part which is different for all users(X-MOZ-LASTACK for example).
330      * Because of the this we have to generate the etag as the hash of the record id, the lastmodified time stamp and the
331      * hash of the json encoded attendee object.
332      * This way the etag changes when the general properties or the user specific properties change.
333      * 
334      * @return string
335      * 
336      * @todo add a unittest for this function to verify desired behavior
337      */
338     public function getETag() 
339     {
340         // NOTE: We don't distinguish between scheduling and attendee sequences.
341         //       Every action increases the record sequence atm.
342         //       If we once should implement different sequences we also need 
343         //       to consider sequences for non-attendee for X-MOZ-LASTACK
344         $record = $this->getRecord();
345         return '"' . sha1($record->getId() . $record->seq) . '"';
346     }
347     
348     /**
349      * Returns the last modification date as a unix timestamp
350      *
351      * @return time
352      */
353     public function getLastModified() 
354     {
355         return ($this->getRecord()->last_modified_time instanceof Tinebase_DateTime) ? $this->getRecord()->last_modified_time->toString() : $this->getRecord()->creation_time->toString();
356     }
357     
358     /**
359      * Returns the size of the vcard in bytes
360      *
361      * @return int
362      */
363     public function getSize() 
364     {
365         return strlen($this->_getVEvent());
366     }
367     
368     /**
369      * Updates the VCard-formatted object
370      *
371      * @param string $cardData
372      * @return void
373      */
374     public function put($cardData) 
375     {
376         $this->_assertEventFacadeParams($this->_container);
377         if (get_class($this->_converter) == 'Calendar_Convert_Event_VCalendar_Generic') {
378             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) 
379                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " update by generic client not allowed. See Calendar_Convert_Event_VCalendar_Factory for supported clients.");
380             throw new Sabre\DAV\Exception\Forbidden('Update denied for unknown client');
381         }
382         
383         if (is_resource($cardData)) {
384             $cardData = stream_get_contents($cardData);
385         }
386         // Converting to UTF-8, if needed
387         $cardData = Sabre\DAV\StringUtil::ensureUTF8($cardData);
388         
389         #Sabre_CalDAV_ICalendarUtil::validateICalendarObject($cardData, array('VEVENT', 'VFREEBUSY'));
390         
391         $vobject = Calendar_Convert_Event_VCalendar_Abstract::getVObject($cardData);
392         foreach ($vobject->children() as $component) {
393             if (isset($component->{'X-TINE20-CONTAINER'})) {
394                 $xTine20Container = $component->{'X-TINE20-CONTAINER'};
395                 break;
396             }
397         }
398         
399         // keep old record for reference
400         $recordBeforeUpdate = clone $this->getRecord();
401         
402         // concurrency management is based on etag in CalDAV
403         $event = $this->_converter->toTine20Model($vobject, $this->getRecord(), array(
404             Calendar_Convert_Event_VCalendar_Abstract::OPTION_USE_SERVER_MODLOG => true,
405         ));
406         
407         $currentContainer = Tinebase_Container::getInstance()->getContainerById($this->getRecord()->container_id);
408         
409         // event 'belongs' current user -> allow container move
410         if ($currentContainer->isPersonalOf(Tinebase_Core::getUser())) {
411             $event->container_id = $this->_container->getId();
412         }
413         
414         // client sends CalDAV event -> handle a container move
415         else if (isset($xTine20Container)) {
416             if ($xTine20Container->getValue() == $currentContainer->getId()) {
417                 $event->container_id = $this->_container->getId();
418             } else {
419                 // @TODO allow organizer to move original cal when he edits the displaycal event?
420                 if ($this->_container->type == Tinebase_Model_Container::TYPE_PERSONAL) {
421                     Calendar_Controller_MSEventFacade::getInstance()->setDisplaycontainer($event, $this->_container->getId());
422                 }
423             }
424         }
425         
426         // client sends event from iMIP invitation -> only allow displaycontainer move
427         else {
428             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
429                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " X-TINE20-CONTAINER not present -> restrict container moves");
430             if ($this->_container->type == Tinebase_Model_Container::TYPE_PERSONAL) {
431                 Calendar_Controller_MSEventFacade::getInstance()->setDisplaycontainer($event, $this->_container->getId());
432             }
433         }
434         
435         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
436             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " " . print_r($event->toArray(), true));
437         
438         $this->update($event);
439         
440         return $this->getETag();
441     }
442     
443     /**
444      * update this node with given event
445      * 
446      * @param Calendar_Model_Event $event
447      */
448     public function update(Calendar_Model_Event $event)
449     {
450         try {
451             $this->_event = Calendar_Controller_MSEventFacade::getInstance()->update($event);
452         } catch (Tinebase_Timemachine_Exception_ConcurrencyConflict $ttecc) {
453             throw new Sabre\DAV\Exception\PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.','If-Match');
454         }
455     }
456     
457     /**
458      * reset alarms to previous values
459      * 
460      * we don't reset the alarms in the vcalendar parser already, because this it is a limitation
461      * of our current calendar implementation to not allow user specific alarms
462      * 
463      * @param Calendar_Model_Event $event
464      * @param Calendar_Model_Event $recordBeforeUpdate
465      */
466     protected function _resetAlarms(Calendar_Model_Event $event, Calendar_Model_Event $recordBeforeUpdate)
467     {
468         $event->alarms = $recordBeforeUpdate->alarms;
469     
470         if ($event->exdate instanceof Tinebase_Record_RecordSet) {
471             foreach ($event->exdate as $exdate) {
472                 $recurId = $event->id . '-' . (string) $exdate->recurid;
473                 
474                 if ($recordBeforeUpdate->exdate instanceof Tinebase_Record_RecordSet && ($matchingRecord = $recordBeforeUpdate->exdate->find('recurid', $recurId)) !== null) {
475                     $exdate->alarms = $matchingRecord->alarms;
476                 } else {
477                     $exdate->alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
478                 }
479             }
480         }
481     }
482     
483     /**
484      * asserts correct event filter and calendar user in MSEventFacade
485      * 
486      * NOTE: this is nessesary as MSEventFacade is a singleton and in some operations (e.g. move) there are 
487      *       multiple instances of self
488      */
489     protected static function _assertEventFacadeParams($container)
490     {
491         $eventFilter = new Calendar_Model_EventFilter(array(
492                 array('field' => 'container_id', 'operator' => 'equals', 'value' => $container->getId())
493         ));
494         
495         try {
496             $calendarUserId = $container->type == Tinebase_Model_Container::TYPE_PERSONAL ?
497             Addressbook_Controller_Contact::getInstance()->getContactByUserId($container->getOwner(), true)->getId() :
498             Tinebase_Core::getUser()->contact_id;
499         } catch (Exception $e) {
500             $calendarUserId = Tinebase_Core::getUser()->contact_id;
501         }
502         
503         $calendarUser = new Calendar_Model_Attender(array(
504             'user_type' => Calendar_Model_Attender::USERTYPE_USER,
505             'user_id'   => $calendarUserId,
506         ));
507         
508         Calendar_Controller_MSEventFacade::getInstance()->setEventFilter($eventFilter);
509         Calendar_Controller_MSEventFacade::getInstance()->setCalendarUser($calendarUser);
510     }
511     
512     /**
513      * Updates the ACL
514      *
515      * This method will receive a list of new ACE's. 
516      * 
517      * @param array $acl 
518      * @return void
519      */
520     public function setACL(array $acl) 
521     {
522         throw new Sabre\DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
523     }
524     
525     /**
526      * return Calendar_Model_Event and convert contact id to model if needed
527      * 
528      * @return Calendar_Model_Event
529      */
530     public function getRecord()
531     {
532         if (! $this->_event instanceof Calendar_Model_Event) {
533             $this->_assertEventFacadeParams($this->_container);
534             $this->_event = Calendar_Controller_MSEventFacade::getInstance()->get($this->_event);
535             
536             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " " . print_r($this->_event->toArray(), true));
537         }
538
539         return $this->_event;
540     }
541     
542     /**
543      * returns container of this event
544      *
545      * @return Tinebase_Model_Container
546      */
547     public function getContainer()
548     {
549         return $this->_container;
550     }
551     
552     /**
553      * return vcard and convert Calendar_Model_Event to vcard if needed
554      * 
555      * @return string
556      */
557     protected function _getVEvent()
558     {
559         if ($this->_vevent == null) {
560             $this->_vevent = $this->_converter->fromTine20Model($this->getRecord());
561             
562             foreach ($this->_vevent->children() as $component) {
563                 if ($component->name == 'VEVENT') {
564                     // NOTE: we store the requested container here to have an origin when the event is moved
565                     $component->add('X-TINE20-CONTAINER', $this->_container->getId());
566                     
567                     if (isset($component->{'VALARM'}) && !$this->_container->isPersonalOf(Tinebase_Core::getUser())) {
568                         // prevent duplicate alarms
569                         $component->add('X-MOZ-LASTACK', Tinebase_DateTime::now()->addYear(100)->setTimezone('UTC'), array('VALUE' => 'DATE-TIME'));
570                     }
571                 }
572             }
573         }
574         
575         return $this->_vevent->serialize();
576     }
577     
578     /**
579      * 
580      */
581     public function getSupportedPrivilegeSet()
582     {
583         return null;
584     }
585 }