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