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)
18 * class to handle containers in CalDAV tree
21 * @subpackage Frontend
23 class Calendar_Frontend_WebDAV_Container extends Tinebase_WebDav_Container_Abstract implements Sabre\CalDAV\ICalendar, Sabre\CalDAV\IShareableCalendar
25 protected $_applicationName = 'Calendar';
27 protected $_model = 'Event';
29 protected $_suffix = '.ics';
34 protected $_calendarQueryCache = null;
38 * @see Sabre\DAV\Collection::getChild()
40 public function getChild($_name)
42 $eventId = $_name instanceof Tinebase_Record_Interface ? $_name->getId() : $this->_getIdFromName($_name);
44 // check if child exists in calendarQuery cache
45 if ($this->_calendarQueryCache &&
46 isset($this->_calendarQueryCache[$eventId])) {
48 $child = $this->_calendarQueryCache[$eventId];
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;
59 $modelName = $this->_application->name . '_Model_' . $this->_model;
61 if ($_name instanceof $modelName) {
64 $filterClass = $this->_application->name . '_Model_' . $this->_model . 'Filter';
65 $filter = new $filterClass(array(
67 'field' => 'container_id',
68 'operator' => 'equals',
69 'value' => $this->_container->getId()
71 array('condition' => 'OR', 'filters' => array(
74 'operator' => 'equals',
79 'operator' => 'equals',
84 $object = $this->_getController()->search($filter, null, false, false, 'sync')->getFirstRecord();
86 if ($object == null) {
87 throw new Sabre\DAV\Exception\NotFound('Object not found');
91 $httpRequest = new Sabre\HTTP\Request();
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') === '*') {
97 $object->organizer != Tinebase_Core::getUser()->contact_id &&
98 Calendar_Model_Attender::getOwnAttender($object->attendee) !== null
100 throw new Sabre\DAV\Exception\NotFound('Object not found');
104 $objectClass = $this->_application->name . '_Frontend_WebDAV_' . $this->_model;
106 return new $objectClass($this->_container, $object);
110 * Returns an array with all the child nodes
112 * @return Sabre\DAV\INode[]
114 function getChildren($filter = null)
116 if ($filter === null) {
117 $filterClass = $this->_application->name . '_Model_' . $this->_model . 'Filter';
118 $filter = new $filterClass(array(
120 'field' => 'container_id',
121 'operator' => 'equals',
122 'value' => $this->_container->getId()
126 'operator' => 'within',
128 'from' => Tinebase_DateTime::now()->subMonth($this->_getMaxPeriodFrom()),
129 'until' => Tinebase_DateTime::now()->addYear(4)
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
140 $filter->addFilter($skipSharedFilter);
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);
148 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE))
149 Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Event filter: ' . print_r($filter->toArray(), true));
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
158 $objects = $this->_getController()->search($filter, null, false, false, 'sync');
162 foreach ($objects as $object) {
163 $children[$object->getId()] = $this->getChild($object);
170 * Returns the list of properties
172 * @param array $requestedProperties
175 public function getProperties($requestedProperties)
177 $ctags = Tinebase_Container::getInstance()->getContentSequence($this->_container);
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,
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)
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);
198 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE))
199 Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . print_r($properties, true));
203 foreach($requestedProperties as $prop) {
204 if (isset($properties[$prop])) {
205 $response[$prop] = $properties[$prop];
209 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE))
210 Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . print_r($response, true));
215 protected function _getController()
217 if ($this->_controller === null) {
218 $this->_controller = Calendar_Controller_MSEventFacade::getInstance();
221 return $this->_controller;
225 * Performs a calendar-query on the contents of this calendar.
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).
232 * This method should just return a list of (relative) urls that match this
235 * The list of filters are specified as an array. The exact array is
236 * documented by \Sabre\CalDAV\CalendarQueryParser.
238 * @param array $filters
241 public function calendarQuery(array $filters)
243 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
244 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' filters ' . print_r($filters, true));
246 $filterArray = array(array(
247 'field' => 'container_id',
248 'operator' => 'equals',
249 'value' => $this->_container->getId()
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'));
266 $periodFrom = new Tinebase_DateTime($timeRange['start']);
267 $periodUntil = new Tinebase_DateTime($timeRange['end']);
271 if (isset($filter['prop-filters']) && is_array($filter['prop-filters'])) {
274 foreach ($filter['prop-filters'] as $propertyFilter) {
275 if ($propertyFilter['name'] === 'UID') {
276 $uids[] = $this->_getIdFromName($propertyFilter['text-match']['value']);
281 $filterArray[] = array(
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());
306 if ($periodUntil === null) {
307 $periodUntil = Tinebase_DateTime::now()->addYear(4);
310 $filterArray[] = array(
312 'operator' => 'within',
314 'from' => $periodFrom,
315 'until' => $periodUntil
319 $filterClass = $this->_application->name . '_Model_' . $this->_model . 'Filter';
320 $filter = new $filterClass($filterArray);
322 $this->_calendarQueryCache = $this->getChildren($filter);
324 return array_keys($this->_calendarQueryCache);
328 * get max period (from) in months (default: 2)
332 protected function _getMaxPeriodFrom()
334 return Calendar_Config::getInstance()->get(Calendar_Config::MAX_FILTER_PERIOD_CALDAV, 2);
340 * changed href from URI format /path/to/targetid to urn:uuid:id format due to el capitano ical client
342 * https://service.metaways.net/Ticket/Display.html?id=145985
344 * @see \Sabre\CalDAV\IShareableCalendar::getShares()
346 public function getShares()
351 $grants = Tinebase_Container::getInstance()->getGrantsOfContainer($this->_container);
352 } catch (Tinebase_Exception_AccessDenied $e) {
353 // user has no right/grant to see all grants of this container
354 $grants = new Tinebase_Record_RecordSet('Tinebase_Model_Grants');
355 $grants->addRecord(Tinebase_Container::getInstance()->getGrantsOfAccount(Tinebase_Core::getUser(), $this->_container));
358 foreach ($grants as $grant) {
360 switch ($grant->account_type) {
362 // was: '/principals/groups/anyone'
363 $href = 'urn:uuid:anyone';
364 $commonName = 'Anyone';
369 $list = Tinebase_Group::getInstance()->getGroupById($grant->account_id);
370 } catch (Tinebase_Exception_NotFound $tenf) {
374 // was: '/principals/groups/'
375 $href = 'urn:uuid:' . $list->list_id;
376 $commonName = $list->name;
381 if ((string)$this->_container->owner_id === (string)$grant->account_id) {
385 $contact = Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend('accountId', $grant->account_id);
386 } catch (Tinebase_Exception_NotFound $tenf) {
390 // was: '/principals/users/'
391 $href = 'urn:uuid:' . $contact->contact_id;
392 $commonName = $contact->accountDisplayName;
396 $writeAble = $grant[Tinebase_Model_Grants::GRANT_ADMIN] ||
397 ( $grant[Tinebase_Model_Grants::GRANT_READ] &&
398 $grant[Tinebase_Model_Grants::GRANT_ADD] &&
399 $grant[Tinebase_Model_Grants::GRANT_EDIT] &&
400 $grant[Tinebase_Model_Grants::GRANT_DELETE] );
404 'commonName' => $commonName,
405 'status' => Sabre\CalDAV\SharingPlugin::STATUS_ACCEPTED,
406 'readOnly' => !$writeAble,
407 'summary' => null //optional
415 * Returns the list of supported privileges for this node.
417 * The returned data structure is a list of nested privileges.
418 * See \Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
419 * standard structure.
421 * If null is returned from this method, the default privilege set is used,
422 * which is fine for most common usecases.
426 public function getSupportedPrivilegeSet()
428 $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet();
430 // We need to inject 'read-free-busy' in the tree, aggregated under
432 foreach($default['aggregates'] as &$agg) {
434 if ($agg['privilege'] !== '{DAV:}read') continue;
436 $agg['aggregates'][] = array(
437 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}read-free-busy',
447 * @see \Sabre\CalDAV\IShareableCalendar::updateShares()
449 public function updateShares(array $add, array $remove)