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