42e57c869d52b6f0e623a79167e07fe11cdf973d
[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      * (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                 array(
125                     'field'    => 'period',
126                     'operator'  => 'within',
127                     'value'     => array(
128                         'from'  => Tinebase_DateTime::now()->subMonth($this->_getMaxPeriodFrom()),
129                         'until' => Tinebase_DateTime::now()->addYear(4)
130                     )
131                 )
132             ));
133
134             if (Calendar_Config::getInstance()->get(Calendar_Config::SKIP_DOUBLE_EVENTS) == 'shared' && $this->_container->type == Tinebase_Model_Container::TYPE_SHARED) {
135                 $skipSharedFilter = $filter->createFilter('attender', 'not', array(
136                     'user_type' => Calendar_Model_Attender::USERTYPE_USER,
137                     'user_id'   => Addressbook_Model_Contact::CURRENTCONTACT
138                 ));
139
140                 $filter->addFilter($skipSharedFilter);
141             }
142
143             if (Calendar_Config::getInstance()->get(Calendar_Config::SKIP_DOUBLE_EVENTS) == 'personal' && $this->_container->type == Tinebase_Model_Container::TYPE_PERSONAL) {
144                 $skipPersonalFilter = new Tinebase_Model_Filter_Container('container_id', 'equals', '/personal/' . Tinebase_Core::getUser()->getId(), array('applicationName' => 'Calendar'));
145                 $filter->addFilter($skipPersonalFilter);
146             }
147
148             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE))
149                 Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Event filter: ' . print_r($filter->toArray(), true));
150
151         }
152         
153         /**
154          * see http://forge.tine20.org/mantisbt/view.php?id=5122
155          * we must use action 'sync' and not 'get' as
156          * otherwise the calendar also return events the user only can see because of freebusy
157          */
158         $objects = $this->_getController()->search($filter, null, false, false, 'sync');
159         
160         $children = array();
161         
162         foreach ($objects as $object) {
163             $children[$object->getId()] = $this->getChild($object);
164         }
165         
166         return $children;
167     }
168     
169     /**
170      * Returns the list of properties
171      *
172      * @param array $requestedProperties
173      * @return array
174      */
175     public function getProperties($requestedProperties) 
176     {
177         $ctags = Tinebase_Container::getInstance()->getContentSequence($this->_container);
178         
179         $properties = array(
180             '{http://calendarserver.org/ns/}getctag' => $ctags,
181             'id'                => $this->_container->getId(),
182             'uri'               => $this->_useIdAsName == true ? $this->_container->getId() : $this->_container->name,
183             '{DAV:}resource-id' => 'urn:uuid:' . $this->_container->getId(),
184             '{DAV:}owner'       => new Sabre\DAVACL\Property\Principal(Sabre\DAVACL\Property\Principal::HREF, 'principals/users/' . Tinebase_Core::getUser()->contact_id),
185             '{DAV:}displayname' => $this->_container->name,
186             '{http://apple.com/ns/ical/}calendar-color' => (empty($this->_container->color)) ? '#000000' : $this->_container->color,
187             
188             '{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new Sabre\CalDAV\Property\SupportedCalendarComponentSet(array('VEVENT')),
189             '{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-data'          => new Sabre\CalDAV\Property\SupportedCalendarData(),
190             '{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}calendar-description'             => 'Calendar ' . $this->_container->name,
191             '{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}calendar-timezone'                => Tinebase_WebDav_Container_Abstract::getCalendarVTimezone($this->_application)
192         );
193         
194         if (!empty(Tinebase_Core::getUser()->accountEmailAddress)) {
195             $properties['{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}calendar-user-address-set'    ] = new Sabre\DAV\Property\HrefList(array('mailto:' . Tinebase_Core::getUser()->accountEmailAddress), false);
196         }
197         
198         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
199             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . print_r($properties, true));
200         
201         $response = array();
202     
203         foreach($requestedProperties as $prop) {
204             if (isset($properties[$prop])) {
205                 $response[$prop] = $properties[$prop];
206             }
207         }
208         
209         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
210             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . print_r($response, true));
211         
212         return $response;
213     }
214     
215     protected function _getController()
216     {
217         if ($this->_controller === null) {
218             $this->_controller = Calendar_Controller_MSEventFacade::getInstance();
219         }
220         
221         return $this->_controller;
222     }
223     
224     /**
225      * Performs a calendar-query on the contents of this calendar.
226      *
227      * The calendar-query is defined in RFC4791 : CalDAV. Using the
228      * calendar-query it is possible for a client to request a specific set of
229      * object, based on contents of iCalendar properties, date-ranges and
230      * iCalendar component types (VTODO, VEVENT).
231      *
232      * This method should just return a list of (relative) urls that match this
233      * query.
234      *
235      * The list of filters are specified as an array. The exact array is
236      * documented by \Sabre\CalDAV\CalendarQueryParser.
237      *
238      * @param array $filters
239      * @return array
240      */
241     public function calendarQuery(array $filters)
242     {
243         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
244             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' filters ' . print_r($filters, true));
245         
246         $filterArray = array(array(
247             'field'    => 'container_id',
248             'operator' => 'equals',
249             'value'    => $this->_container->getId()
250         ));
251         
252         $periodFrom = null;
253         $periodUntil = null;
254         
255         if (isset($filters['comp-filters']) && is_array($filters['comp-filters'])) {
256             foreach ($filters['comp-filters'] as $filter) {
257                 if (isset($filter['time-range']) && is_array($filter['time-range'])) {
258                     $timeRange = $filter['time-range'];
259                     if (isset($timeRange['start'])) {
260                         if (! isset($timeRange['end'])) {
261                             // create default time-range end in 4 years from now 
262                             $timeRange['end'] = new DateTime('NOW');
263                             $timeRange['end']->add(new DateInterval('P4Y'));
264                         }
265                         
266                         $periodFrom = new Tinebase_DateTime($timeRange['start']);
267                         $periodUntil = new Tinebase_DateTime($timeRange['end']);
268                     }
269                 }
270                 
271                 if (isset($filter['prop-filters']) && is_array($filter['prop-filters'])) {
272                     $uids = array();
273
274                     foreach ($filter['prop-filters'] as $propertyFilter) {
275                         if ($propertyFilter['name'] === 'UID') {
276                             $uids[] = $this->_getIdFromName($propertyFilter['text-match']['value']);
277                         }
278                     }
279                     
280                     if (!empty($uids)) {
281                         $filterArray[] = array(
282                             'condition' => 'OR', 
283                             'filters' => array(
284                                 array(
285                                     'field'     => 'id',
286                                     'operator'  => 'in',
287                                     'value'     => $uids
288                                 ),
289                                 array(
290                                     'field'     => 'uid',
291                                     'operator'  => 'in',
292                                     'value'     => $uids
293                                 )
294                             )
295                         );
296                     }
297                 }
298             }
299         }
300
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(4);
308         }
309         
310         $filterArray[] = array(
311             'field' => 'period',
312             'operator' => 'within',
313             'value' => array(
314                 'from'  => $periodFrom,
315                 'until' => $periodUntil
316             )
317         );
318         
319         $filterClass = $this->_application->name . '_Model_' . $this->_model . 'Filter';
320         $filter = new $filterClass($filterArray);
321     
322         $this->_calendarQueryCache = $this->getChildren($filter);
323         
324         return array_keys($this->_calendarQueryCache);
325     }
326     
327     /**
328      * get max period (from) in months (default: 2)
329      * 
330      * @return integer
331      */
332     protected function _getMaxPeriodFrom()
333     {
334         return Calendar_Config::getInstance()->get(Calendar_Config::MAX_FILTER_PERIOD_CALDAV, 2);
335     }
336     
337     /**
338      * (non-PHPdoc)
339      * @see \Sabre\CalDAV\IShareableCalendar::getShares()
340      */
341     public function getShares()
342     {
343         $result = array();
344         
345         try {
346             $grants = Tinebase_Container::getInstance()->getGrantsOfContainer($this->_container);
347         } catch (Tinebase_Exception_AccessDenied $e) {
348             // user has no right/grant to see all grants of this container
349             $grants = new Tinebase_Record_RecordSet('Tinebase_Model_Grants');
350             $grants->addRecord(Tinebase_Container::getInstance()->getGrantsOfAccount(Tinebase_Core::getUser(), $this->_container));
351         }
352         
353         foreach ($grants as $grant) {
354             
355             switch ($grant->account_type) {
356                 case 'anyone':
357                     $href       = '/principals/groups/anyone';
358                     $commonName = 'Anyone';
359                     break;
360                 
361                 case 'group':
362                     try {
363                         $list       = Tinebase_Group::getInstance()->getGroupById($grant->account_id);
364                     } catch (Tinebase_Exception_NotFound $tenf) {
365                         continue;
366                     }
367                      
368                     $href       = '/principals/groups/' . $list->list_id;
369                     $commonName = $list->name;
370                     
371                     break;
372                     
373                 case 'user':
374                     try {
375                         $contact = Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend('accountId', $grant->account_id);
376                     } catch (Tinebase_Exception_NotFound $tenf) {
377                         continue;
378                     }
379                      
380                     $href       = '/principals/users/' . $contact->contact_id;
381                     $commonName = $contact->accountDisplayName;
382                     break;
383             }
384             
385             $writeAble = $grant[Tinebase_Model_Grants::GRANT_ADMIN] || 
386                          ( $grant[Tinebase_Model_Grants::GRANT_READ] && 
387                            $grant[Tinebase_Model_Grants::GRANT_ADD]  && 
388                            $grant[Tinebase_Model_Grants::GRANT_EDIT] &&
389                            $grant[Tinebase_Model_Grants::GRANT_DELETE] );
390             
391             $result[] = array(
392                 'href'       => $href,
393                 'commonName' => $commonName,
394                 'status'     => Sabre\CalDAV\SharingPlugin::STATUS_ACCEPTED,
395                 'readOnly'   => !$writeAble, 
396                 'summary'    => null            //optional
397             ); 
398         }
399         
400         return $result;
401     }
402     
403     /**
404      * Returns the list of supported privileges for this node.
405      *
406      * The returned data structure is a list of nested privileges.
407      * See \Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
408      * standard structure.
409      *
410      * If null is returned from this method, the default privilege set is used,
411      * which is fine for most common usecases.
412      *
413      * @return array|null
414      */
415     public function getSupportedPrivilegeSet() 
416     {
417         $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet();
418
419         // We need to inject 'read-free-busy' in the tree, aggregated under
420         // {DAV:}read.
421         foreach($default['aggregates'] as &$agg) {
422
423             if ($agg['privilege'] !== '{DAV:}read') continue;
424
425             $agg['aggregates'][] = array(
426                 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}read-free-busy',
427             );
428
429         }
430         
431         return $default;
432     }
433     
434     /**
435      * (non-PHPdoc)
436      * @see \Sabre\CalDAV\IShareableCalendar::updateShares()
437      */
438     public function updateShares(array $add, array $remove)
439     {
440         
441     }
442 }