make Calendars shareable via CalDAV
[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      * (non-PHPdoc)
33      * @see Sabre\DAV\Collection::getChild()
34      */
35     public function getChild($_name)
36     {
37         $modelName = $this->_application->name . '_Model_' . $this->_model;
38         
39         if ($_name instanceof $modelName) {
40             $object = $_name;
41         } else {
42             $filterClass = $this->_application->name . '_Model_' . $this->_model . 'Filter';
43             $filter = new $filterClass(array(
44                 array(
45                     'field'     => 'container_id',
46                     'operator'  => 'equals',
47                     'value'     => $this->_container->getId()
48                 ),
49                 array('condition' => 'OR', 'filters' => array(
50                     array(
51                         'field'     => 'id',
52                         'operator'  => 'equals',
53                         'value'     => $this->_getIdFromName($_name)
54                     ),
55                     array(
56                         'field'     => 'uid',
57                         'operator'  => 'equals',
58                         'value'     => $this->_getIdFromName($_name)
59                     )
60                 ))
61             ));
62             $object = $this->_getController()->search($filter, null, false, false, 'sync')->getFirstRecord();
63         
64             if ($object == null) {
65                 throw new Sabre\DAV\Exception\NotFound('Object not found');
66             }
67         }
68         
69         $httpRequest = new Sabre\HTTP\Request();
70         
71         // lie about existence of event of request is a PUT request from an ATTENDEE for an already existing event 
72         // to prevent ugly (and not helpful) error messages on the client
73         if (isset($_SERVER['REQUEST_METHOD']) && $httpRequest->getMethod() == 'PUT' && $httpRequest->getHeader('If-None-Match') === '*') {
74             if (
75                 $object->organizer != Tinebase_Core::getUser()->contact_id && 
76                 Calendar_Model_Attender::getOwnAttender($object->attendee) !== null
77             ) {
78                 throw new Sabre\DAV\Exception\NotFound('Object not found');
79             }
80         }
81         
82         $objectClass = $this->_application->name . '_Frontend_WebDAV_' . $this->_model;
83         
84         return new $objectClass($this->_container, $object);
85     }
86     
87     /**
88      * Returns an array with all the child nodes
89      *
90      * @return Sabre\DAV\INode[]
91      */
92     function getChildren()
93     {
94         $filterClass = $this->_application->name . '_Model_' . $this->_model . 'Filter';
95         $filter = new $filterClass(array(
96             array(
97                 'field'     => 'container_id',
98                 'operator'  => 'equals',
99                 'value'     => $this->_container->getId()
100             ),
101             array(
102                 'field'    => 'period', 
103                 'operator'  => 'within', 
104                 'value'     => array(
105                     'from'  => Tinebase_DateTime::now()->subWeek(4),
106                     'until' => Tinebase_DateTime::now()->addYear(4)
107                 )
108             )
109         ));
110     
111         /**
112          * see http://forge.tine20.org/mantisbt/view.php?id=5122
113          * we must use action 'sync' and not 'get' as
114          * otherwise the calendar also return events the user only can see because of freebusy
115          */
116         $objects = $this->_getController()->search($filter, null, false, false, 'sync');
117     
118         $children = array();
119     
120         foreach ($objects as $object) {
121             $children[] = $this->getChild($object);
122         }
123     
124         return $children;
125     }
126     
127     /**
128      * Returns the list of properties
129      *
130      * @param array $requestedProperties
131      * @return array
132      */
133     public function getProperties($requestedProperties) 
134     {
135         $displayName = $this->_container->type == Tinebase_Model_Container::TYPE_SHARED ? $this->_container->name . ' (shared)' : $this->_container->name;
136         
137         $ctags = Tinebase_Container::getInstance()->getContentSequence($this->_container);
138         
139         $properties = array(
140             '{http://calendarserver.org/ns/}getctag' => $ctags,
141             'id'                => $this->_container->getId(),
142             'uri'               => $this->_useIdAsName == true ? $this->_container->getId() : $this->_container->name,
143             '{DAV:}resource-id' => 'urn:uuid:' . $this->_container->getId(),
144             '{DAV:}owner'       => new Sabre\DAVACL\Property\Principal(Sabre\DAVACL\Property\Principal::HREF, 'principals/users/' . Tinebase_Core::getUser()->contact_id),
145             '{DAV:}displayname' => $displayName,
146             '{http://apple.com/ns/ical/}calendar-color' => (empty($this->_container->color)) ? '#000000' : $this->_container->color,
147             
148             '{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new Sabre\CalDAV\Property\SupportedCalendarComponentSet(array('VEVENT')),
149             '{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-data'          => new Sabre\CalDAV\Property\SupportedCalendarData(),
150             '{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}calendar-description'             => 'Calendar ' . $displayName,
151             '{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}calendar-timezone'                => Tinebase_WebDav_Container_Abstract::getCalendarVTimezone($this->_application)
152         );
153         
154         if (!empty(Tinebase_Core::getUser()->accountEmailAddress)) {
155             $properties['{' . Sabre\CalDAV\Plugin::NS_CALDAV . '}calendar-user-address-set'    ] = new Sabre\DAV\Property\HrefList(array('mailto:' . Tinebase_Core::getUser()->accountEmailAddress), false);
156         }
157         
158         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
159             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . print_r($properties, true));
160         
161         $response = array();
162     
163         foreach($requestedProperties as $prop) {
164             if (isset($properties[$prop])) {
165                 $response[$prop] = $properties[$prop];
166             }
167         }
168         
169         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
170             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . print_r($response, true));
171         
172         return $response;
173     }
174     
175     protected function _getController()
176     {
177         if ($this->_controller === null) {
178             $this->_controller = Calendar_Controller_MSEventFacade::getInstance();
179         }
180         
181         return $this->_controller;
182     }
183     
184     /**
185      * Performs a calendar-query on the contents of this calendar.
186      *
187      * The calendar-query is defined in RFC4791 : CalDAV. Using the
188      * calendar-query it is possible for a client to request a specific set of
189      * object, based on contents of iCalendar properties, date-ranges and
190      * iCalendar component types (VTODO, VEVENT).
191      *
192      * This method should just return a list of (relative) urls that match this
193      * query.
194      *
195      * The list of filters are specified as an array. The exact array is
196      * documented by \Sabre\CalDAV\CalendarQueryParser.
197      *
198      * @param array $filters
199      * @return array
200      */
201     public function calendarQuery(array $filters)
202     {
203         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
204             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' filters ' . print_r($filters, true));
205         
206         $filterArray = array(array(
207             'field'    => 'container_id',
208             'operator' => 'equals',
209             'value'    => $this->_container->getId()
210         ));
211         
212         $periodFrom = null;
213         $periodUntil = null;
214         if (isset($filters['comp-filters']) && isset($filters['comp-filters'][0]['time-range'])) {
215             $timeRange = $filters['comp-filters'][0]['time-range'];
216             if (isset($timeRange['start'])) {
217                 if (! isset($timeRange['end'])) {
218                     // create default time-range end in 4 years from now 
219                     $timeRange['end'] = new DateTime('NOW');
220                     $timeRange['end']->add(new DateInterval('P4Y'));
221                 }
222                 
223                 $periodFrom = new Tinebase_DateTime($timeRange['start']);
224                 $periodUntil = new Tinebase_DateTime($timeRange['end']);
225             }
226         }
227
228         // @see 0009162: CalDAV Performance issues for many events
229         // create default time-range end in 4 years from now and 2 months back (configurable) if no filter was set by client
230         if ($periodFrom === null) {
231             $periodFrom = Tinebase_DateTime::now()->subMonth(Calendar_Config::getInstance()->get(Calendar_Config::MAX_FILTER_PERIOD_CALDAV, 2));
232         }
233         if ($periodUntil === null) {
234             $periodUntil = Tinebase_DateTime::now()->addYear(4);
235         }
236         
237         $filterArray[] = array(
238             'field' => 'period',
239             'operator' => 'within',
240             'value' => array(
241                 'from'  => $periodFrom,
242                 'until' => $periodUntil
243             )
244         );
245         
246         $filterClass = $this->_application->name . '_Model_' . $this->_model . 'Filter';
247         $filter = new $filterClass($filterArray);
248     
249         // @see http://forge.tine20.org/mantisbt/view.php?id=5122
250         // we must use action 'sync' and not 'get' as
251         // otherwise the calendar also return events the user only can see because of freebusy
252         $ids = $this->_getController()->search($filter, null, false, true, 'sync');
253     
254         return $ids;
255     }
256     
257     /**
258      * (non-PHPdoc)
259      * @see \Sabre\CalDAV\IShareableCalendar::getShares()
260      */
261     public function getShares()
262     {
263         $result = array();
264         
265         foreach (Tinebase_Container::getInstance()->getGrantsOfContainer($this->_container) as $grant) {
266             
267             switch ($grant->account_type) {
268                 case 'anyone':
269                     $href       = '/principals/groups/anyone';
270                     $commonName = 'Anyone';
271                     break;
272                 
273                 case 'group':
274                     $list       = Tinebase_Group::getInstance()->getGroupById($grant->account_id);
275                      
276                     $href       = '/principals/groups/' . $list->list_id;
277                     $commonName = $list->name;
278                     
279                     break;
280                     
281                 case 'user':
282                     $contact    = Tinebase_User::getInstance()->getUserById($grant->account_id);
283                      
284                     $href       = '/principals/users/' . $contact->contact_id;
285                     $commonName = $contact->accountDisplayName;
286                     break;
287             }
288             
289             $writeAble = $grant[Tinebase_Model_Grants::GRANT_ADMIN] || 
290                          ( $grant[Tinebase_Model_Grants::GRANT_READ] && 
291                            $grant[Tinebase_Model_Grants::GRANT_ADD]  && 
292                            $grant[Tinebase_Model_Grants::GRANT_EDIT] &&
293                            $grant[Tinebase_Model_Grants::GRANT_DELETE] );
294             
295             $result[] = array(
296                 'href'       => $href,
297                 'commonName' => $commonName,
298                 'status'     => Sabre\CalDAV\SharingPlugin::STATUS_ACCEPTED,
299                 'readOnly'   => !$writeAble, 
300                 'summary'    => null            //optional
301             ); 
302         }
303         
304         return $result;
305     }
306     
307     /**
308      * Returns the list of supported privileges for this node.
309      *
310      * The returned data structure is a list of nested privileges.
311      * See \Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
312      * standard structure.
313      *
314      * If null is returned from this method, the default privilege set is used,
315      * which is fine for most common usecases.
316      *
317      * @return array|null
318      */
319     public function getSupportedPrivilegeSet() 
320     {
321         $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet();
322
323         // We need to inject 'read-free-busy' in the tree, aggregated under
324         // {DAV:}read.
325         foreach($default['aggregates'] as &$agg) {
326
327             if ($agg['privilege'] !== '{DAV:}read') continue;
328
329             $agg['aggregates'][] = array(
330                 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}read-free-busy',
331             );
332
333         }
334         
335         return $default;
336     }
337     
338     /**
339      * (non-PHPdoc)
340      * @see \Sabre\CalDAV\IShareableCalendar::updateShares()
341      */
342     public function updateShares(array $add, array $remove)
343     {
344         
345     }
346 }