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