0011428: support caldav sync token
[tine20] / tine20 / Calendar / Frontend / WebDAV / Container.php
1 <?php
2
3 use Sabre\VObject;
4 use Sabre\DAVACL;
5 use Sabre\CalDAV;
6
7 /**
8  * Tine 2.0
9  *
10  * @package     Calendar
11  * @subpackage  Frontend
12  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
13  * @author      Lars Kneschke <l.kneschke@metaways.de>
14  * @copyright   Copyright (c) 2011-2013 Metaways Infosystems GmbH (http://www.metaways.de)
15  */
16
17 /**
18  * class to handle containers in CalDAV tree
19  *
20  * @package     Calendar
21  * @subpackage  Frontend
22  */
23 class Calendar_Frontend_WebDAV_Container extends Tinebase_WebDav_Container_Abstract implements Sabre\CalDAV\ICalendar, Sabre\CalDAV\IShareableCalendar
24 {
25     protected $_applicationName = 'Calendar';
26     
27     protected $_model = 'Event';
28     
29     protected $_suffix = '.ics';
30     
31     /**
32      * @var array
33      */
34     protected $_calendarQueryCache = null;
35
36
37
38     /**
39      * (non-PHPdoc)
40      * @see Sabre\DAV\Collection::getChild()
41      */
42     public function getChild($_name)
43     {
44         $eventId   = $_name instanceof Tinebase_Record_Interface ? $_name->getId() : $this->_getIdFromName($_name);
45         
46         // check if child exists in calendarQuery cache
47         if ($this->_calendarQueryCache &&
48             isset($this->_calendarQueryCache[$eventId])) {
49             
50             $child = $this->_calendarQueryCache[$eventId];
51             
52             // remove entries from cache / they will not be used anymore
53             unset($this->_calendarQueryCache[$eventId]);
54             if (empty($this->_calendarQueryCache)) {
55                 $this->_calendarQueryCache = null;
56             }
57             
58             return $child;
59         }
60         
61         $modelName = $this->_application->name . '_Model_' . $this->_model;
62         
63         if ($_name instanceof $modelName) {
64             $object = $_name;
65         } else {
66             $filterClass = $this->_application->name . '_Model_' . $this->_model . 'Filter';
67             $filter = new $filterClass(array(
68                 array(
69                     'field'     => 'container_id',
70                     'operator'  => 'equals',
71                     'value'     => $this->_container->getId()
72                 ),
73                 array('condition' => 'OR', 'filters' => array(
74                     array(
75                         'field'     => 'id',
76                         'operator'  => 'equals',
77                         'value'     => $eventId
78                     ),
79                     array(
80                         'field'     => 'uid',
81                         'operator'  => 'equals',
82                         'value'     => $eventId
83                     )
84                 ))
85             ));
86             $object = $this->_getController()->search($filter, null, false, false, 'sync')->getFirstRecord();
87         
88             if ($object == null) {
89                 throw new Sabre\DAV\Exception\NotFound('Object not found');
90             }
91         }
92         
93         $httpRequest = new Sabre\HTTP\Request();
94         
95         // lie about existence of event of request is a PUT request from an ATTENDEE for an already existing event 
96         // to prevent ugly (and not helpful) error messages on the client
97         if (isset($_SERVER['REQUEST_METHOD']) && $httpRequest->getMethod() == 'PUT' && $httpRequest->getHeader('If-None-Match') === '*') {
98             if (
99                 $object->organizer != Tinebase_Core::getUser()->contact_id && 
100                 Calendar_Model_Attender::getOwnAttender($object->attendee) !== null
101             ) {
102                 throw new Sabre\DAV\Exception\NotFound('Object not found');
103             }
104         }
105         
106         $objectClass = $this->_application->name . '_Frontend_WebDAV_' . $this->_model;
107         
108         return new $objectClass($this->_container, $object);
109     }
110     
111     /**
112      * Returns an array with all the child nodes
113      *
114      * @return Sabre\DAV\INode[]
115      */
116     function getChildren($filter = null)
117     {
118         if ($filter === null) {
119             $filterClass = $this->_application->name . '_Model_' . $this->_model . 'Filter';
120             $filter = new $filterClass(array(
121                 array(
122                     'field'     => 'container_id',
123                     'operator'  => 'equals',
124                     'value'     => $this->_container->getId()
125                 ),
126                 array(
127                     'field'    => 'period',
128                     'operator'  => 'within',
129                     'value'     => array(
130                         'from'  => Tinebase_DateTime::now()->subMonth($this->_getMaxPeriodFrom()),
131                         'until' => Tinebase_DateTime::now()->addYear(4)
132                     )
133                 )
134             ));
135
136             if (Calendar_Config::getInstance()->get(Calendar_Config::SKIP_DOUBLE_EVENTS) == 'shared' && $this->_container->type == Tinebase_Model_Container::TYPE_SHARED) {
137                 $skipSharedFilter = $filter->createFilter('attender', 'not', array(
138                     'user_type' => Calendar_Model_Attender::USERTYPE_USER,
139                     'user_id'   => Addressbook_Model_Contact::CURRENTCONTACT
140                 ));
141
142                 $filter->addFilter($skipSharedFilter);
143             }
144
145             if (Calendar_Config::getInstance()->get(Calendar_Config::SKIP_DOUBLE_EVENTS) == 'personal' && $this->_container->type == Tinebase_Model_Container::TYPE_PERSONAL) {
146                 $skipPersonalFilter = new Tinebase_Model_Filter_Container('container_id', 'equals', '/personal/' . Tinebase_Core::getUser()->getId(), array('applicationName' => 'Calendar'));
147                 $filter->addFilter($skipPersonalFilter);
148             }
149
150             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE))
151                 Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Event filter: ' . print_r($filter->toArray(), true));
152
153         }
154         
155         /**
156          * see http://forge.tine20.org/mantisbt/view.php?id=5122
157          * we must use action 'sync' and not 'get' as
158          * otherwise the calendar also return events the user only can see because of freebusy
159          */
160         $objects = $this->_getController()->search($filter, null, false, false, 'sync');
161         
162         $children = array();
163         
164         foreach ($objects as $object) {
165             $children[$object->getId()] = $this->getChild($object);
166         }
167         
168         return $children;
169     }
170
171     /**
172      * Returns the list of properties
173      *
174      * @param array $requestedProperties
175      * @return array
176      */
177     public function getProperties($requestedProperties) 
178     {
179         $ctags = Tinebase_Container::getInstance()->getContentSequence($this->_container);
180         
181         $properties = array(
182             '{http://calendarserver.org/ns/}getctag' => $ctags,
183             '{DAV:}sync-token'  => Tinebase_WebDav_Plugin_SyncToken::SYNCTOKEN_PREFIX . $ctags,
184             'id'                => $this->_container->getId(),
185             'uri'               => $this->_useIdAsName == true ? $this->_container->getId() : $this->_container->name,
186             '{DAV:}resource-id' => 'urn:uuid:' . $this->_container->getId(),
187             '{DAV:}owner'       => new Sabre\DAVACL\Property\Principal(Sabre\DAVACL\Property\Principal::HREF, 'principals/users/' . Tinebase_Core::getUser()->contact_id),
188             '{DAV:}displayname' => $this->_container->name,
189             '{http://apple.com/ns/ical/}calendar-color' => (empty($this->_container->color)) ? '#000000' : $this->_container->color,
190             
191             '{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new Sabre\CalDAV\Property\SupportedCalendarComponentSet(array('VEVENT')),
192             '{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-data'          => new Sabre\CalDAV\Property\SupportedCalendarData(),
193             '{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}calendar-description'             => 'Calendar ' . $this->_container->name,
194             '{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}calendar-timezone'                => Tinebase_WebDav_Container_Abstract::getCalendarVTimezone($this->_application)
195         );
196         
197         if (!empty(Tinebase_Core::getUser()->accountEmailAddress)) {
198             $properties['{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}calendar-user-address-set'    ] = new Sabre\DAV\Property\HrefList(array('mailto:' . Tinebase_Core::getUser()->accountEmailAddress), false);
199         }
200         
201         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
202             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . print_r($properties, true));
203         
204         $response = array();
205     
206         foreach($requestedProperties as $prop) {
207             if (isset($properties[$prop])) {
208                 $response[$prop] = $properties[$prop];
209             }
210         }
211         
212         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
213             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . print_r($response, true));
214         
215         return $response;
216     }
217     
218     protected function _getController()
219     {
220         if ($this->_controller === null) {
221             $this->_controller = Calendar_Controller_MSEventFacade::getInstance();
222         }
223         
224         return $this->_controller;
225     }
226     
227     /**
228      * Performs a calendar-query on the contents of this calendar.
229      *
230      * The calendar-query is defined in RFC4791 : CalDAV. Using the
231      * calendar-query it is possible for a client to request a specific set of
232      * object, based on contents of iCalendar properties, date-ranges and
233      * iCalendar component types (VTODO, VEVENT).
234      *
235      * This method should just return a list of (relative) urls that match this
236      * query.
237      *
238      * The list of filters are specified as an array. The exact array is
239      * documented by \Sabre\CalDAV\CalendarQueryParser.
240      *
241      * @param array $filters
242      * @return array
243      */
244     public function calendarQuery(array $filters)
245     {
246         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
247             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' filters ' . print_r($filters, true));
248         
249         $filterArray = array(array(
250             'field'    => 'container_id',
251             'operator' => 'equals',
252             'value'    => $this->_container->getId()
253         ));
254         
255         $periodFrom = null;
256         $periodUntil = null;
257         
258         if (isset($filters['comp-filters']) && is_array($filters['comp-filters'])) {
259             foreach ($filters['comp-filters'] as $filter) {
260                 if (isset($filter['time-range']) && is_array($filter['time-range'])) {
261                     $timeRange = $filter['time-range'];
262                     if (isset($timeRange['start'])) {
263                         if (! isset($timeRange['end'])) {
264                             // create default time-range end in 4 years from now 
265                             $timeRange['end'] = new DateTime('NOW');
266                             $timeRange['end']->add(new DateInterval('P4Y'));
267                         }
268                         
269                         $periodFrom = new Tinebase_DateTime($timeRange['start']);
270                         $periodUntil = new Tinebase_DateTime($timeRange['end']);
271                     }
272                 }
273                 
274                 if (isset($filter['prop-filters']) && is_array($filter['prop-filters'])) {
275                     $uids = array();
276
277                     foreach ($filter['prop-filters'] as $propertyFilter) {
278                         if ($propertyFilter['name'] === 'UID') {
279                             $uids[] = $this->_getIdFromName($propertyFilter['text-match']['value']);
280                         }
281                     }
282                     
283                     if (!empty($uids)) {
284                         $filterArray[] = array(
285                             'condition' => 'OR', 
286                             'filters' => array(
287                                 array(
288                                     'field'     => 'id',
289                                     'operator'  => 'in',
290                                     'value'     => $uids
291                                 ),
292                                 array(
293                                     'field'     => 'uid',
294                                     'operator'  => 'in',
295                                     'value'     => $uids
296                                 )
297                             )
298                         );
299                     }
300                 }
301             }
302         }
303
304         // @see 0009162: CalDAV Performance issues for many events
305         // create default time-range end in 4 years from now and 2 months back (configurable) if no filter was set by client
306         if ($periodFrom === null) {
307             $periodFrom = Tinebase_DateTime::now()->subMonth($this->_getMaxPeriodFrom());
308         }
309         if ($periodUntil === null) {
310             $periodUntil = Tinebase_DateTime::now()->addYear(4);
311         }
312         
313         $filterArray[] = array(
314             'field' => 'period',
315             'operator' => 'within',
316             'value' => array(
317                 'from'  => $periodFrom,
318                 'until' => $periodUntil
319             )
320         );
321         
322         $filterClass = $this->_application->name . '_Model_' . $this->_model . 'Filter';
323         $filter = new $filterClass($filterArray);
324     
325         $this->_calendarQueryCache = $this->getChildren($filter);
326         
327         return array_keys($this->_calendarQueryCache);
328     }
329     
330     /**
331      * get max period (from) in months (default: 2)
332      * 
333      * @return integer
334      */
335     protected function _getMaxPeriodFrom()
336     {
337         //if the client does support sync tokens and the plugin Tinebase_WebDav_Plugin_SyncToken is active, allow to filter for all calendar events => return 100 years
338         if (Calendar_Convert_Event_VCalendar_Factory::supportsSyncToken($_SERVER['HTTP_USER_AGENT'])) {
339             return 100;
340         }
341         return Calendar_Config::getInstance()->get(Calendar_Config::MAX_FILTER_PERIOD_CALDAV, 2);
342     }
343     
344     /**
345      * (non-PHPdoc)
346      * @see \Sabre\CalDAV\IShareableCalendar::getShares()
347      */
348     public function getShares()
349     {
350         $result = array();
351         
352         try {
353             $grants = Tinebase_Container::getInstance()->getGrantsOfContainer($this->_container);
354         } catch (Tinebase_Exception_AccessDenied $e) {
355             // user has no right/grant to see all grants of this container
356             $grants = new Tinebase_Record_RecordSet('Tinebase_Model_Grants');
357             $grants->addRecord(Tinebase_Container::getInstance()->getGrantsOfAccount(Tinebase_Core::getUser(), $this->_container));
358         }
359         
360         foreach ($grants as $grant) {
361             
362             switch ($grant->account_type) {
363                 case 'anyone':
364                     $href       = '/principals/groups/anyone';
365                     $commonName = 'Anyone';
366                     break;
367                 
368                 case 'group':
369                     try {
370                         $list       = Tinebase_Group::getInstance()->getGroupById($grant->account_id);
371                     } catch (Tinebase_Exception_NotFound $tenf) {
372                         continue;
373                     }
374                      
375                     $href       = '/principals/groups/' . $list->list_id;
376                     $commonName = $list->name;
377                     
378                     break;
379                     
380                 case 'user':
381                     try {
382                         $contact = Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend('accountId', $grant->account_id);
383                     } catch (Tinebase_Exception_NotFound $tenf) {
384                         continue;
385                     }
386                      
387                     $href       = '/principals/users/' . $contact->contact_id;
388                     $commonName = $contact->accountDisplayName;
389                     break;
390             }
391             
392             $writeAble = $grant[Tinebase_Model_Grants::GRANT_ADMIN] || 
393                          ( $grant[Tinebase_Model_Grants::GRANT_READ] && 
394                            $grant[Tinebase_Model_Grants::GRANT_ADD]  && 
395                            $grant[Tinebase_Model_Grants::GRANT_EDIT] &&
396                            $grant[Tinebase_Model_Grants::GRANT_DELETE] );
397             
398             $result[] = array(
399                 'href'       => $href,
400                 'commonName' => $commonName,
401                 'status'     => Sabre\CalDAV\SharingPlugin::STATUS_ACCEPTED,
402                 'readOnly'   => !$writeAble, 
403                 'summary'    => null            //optional
404             ); 
405         }
406         
407         return $result;
408     }
409     
410     /**
411      * Returns the list of supported privileges for this node.
412      *
413      * The returned data structure is a list of nested privileges.
414      * See \Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
415      * standard structure.
416      *
417      * If null is returned from this method, the default privilege set is used,
418      * which is fine for most common usecases.
419      *
420      * @return array|null
421      */
422     public function getSupportedPrivilegeSet() 
423     {
424         $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet();
425
426         // We need to inject 'read-free-busy' in the tree, aggregated under
427         // {DAV:}read.
428         foreach($default['aggregates'] as &$agg) {
429
430             if ($agg['privilege'] !== '{DAV:}read') continue;
431
432             $agg['aggregates'][] = array(
433                 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}read-free-busy',
434             );
435
436         }
437         
438         return $default;
439     }
440     
441     /**
442      * (non-PHPdoc)
443      * @see \Sabre\CalDAV\IShareableCalendar::updateShares()
444      */
445     public function updateShares(array $add, array $remove)
446     {
447         
448     }
449
450     /**
451      * indicates whether the concrete class supports sync-token
452      *
453      * @return bool
454      */
455     public function supportsSyncToken()
456     {
457         return true;
458     }
459 }