0011428: support caldav sync token
[tine20] / tine20 / Tinebase / WebDav / Container / Abstract.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Tinebase
6  * @subpackage  WebDAV
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-2015 Metaways Infosystems GmbH (http://www.metaways.de)
10  *
11  */
12
13 /**
14  * abstract class to handle containers in Cal/CardDAV tree
15  *
16  * @package     Tinebase
17  * @subpackage  WebDAV
18  */
19 abstract class Tinebase_WebDav_Container_Abstract extends \Sabre\DAV\Collection implements \Sabre\DAV\IProperties, \Sabre\DAVACL\IACL
20 {
21     /**
22      * the current application object
23      * 
24      * @var Tinebase_Model_Application
25      */
26     protected $_application;
27     
28     protected $_applicationName;
29     
30     protected $_container;
31     
32     protected $_controller;
33     
34     protected $_model;
35     
36     protected $_suffix;
37     
38     protected $_useIdAsName;
39     
40     /**
41      * contructor
42      * 
43      * @param  string|Tinebase_Model_Application  $_application  the current application
44      * @param  string                             $_container    the current path
45      */
46     public function __construct(Tinebase_Model_Container $_container, $_useIdAsName = false)
47     {
48         $this->_application = Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName);
49         $this->_container   = $_container;
50         $this->_useIdAsName = (boolean)$_useIdAsName;
51     }
52     
53     /**
54      * Creates a new file
55      *
56      * The contents of the new file must be a valid VCARD
57      *
58      * @param  string    $name
59      * @param  resource  $vcardData
60      * @return string    the etag of the record
61      */
62     public function createFile($name, $vobjectData = null) 
63     {
64         $objectClass = $this->_application->name . '_Frontend_WebDAV_' . $this->_model;
65
66         $object = $objectClass::create($this->_container, $name, $vobjectData);
67         
68         return $object->getETag();
69     }
70     
71     /**
72      * (non-PHPdoc)
73      * @see \Sabre\DAV\Node::delete()
74      */
75     public function delete()
76     {
77         try {
78             Tinebase_Container::getInstance()->deleteContainer($this->_container);
79         } catch (Tinebase_Exception_AccessDenied $tead) {
80             throw new Sabre\DAV\Exception\Forbidden('Permission denied to delete node');
81         } catch (Tinebase_Exception_Record_SystemContainer $ters) {
82             throw new Sabre\DAV\Exception\Forbidden('Permission denied to delete system container');
83         } catch (Exception $e) {
84             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE))
85                 Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' failed to delete container ' .$this->_container->getId() . "\n$e" );
86
87             throw new \Sabre\DAV\Exception($e->getMessage());
88         }
89     }
90     
91     /**
92      * (non-PHPdoc)
93      * @see Sabre\DAV\Collection::getChild()
94      */
95     public function getChild($_name)
96     {
97         $modelName = $this->_application->name . '_Model_' . $this->_model;
98         
99         if ($_name instanceof $modelName) {
100             $object = $_name;
101         } else {
102             $filterClass = $this->_application->name . '_Model_' . $this->_model . 'Filter';
103             $filter = new $filterClass(array(
104                 array(
105                     'field'     => 'container_id',
106                     'operator'  => 'equals',
107                     'value'     => $this->_container->getId()
108                 ),
109                 array(
110                     'field'     => 'id',
111                     'operator'  => 'equals',
112                     'value'     => $this->_getIdFromName($_name)
113                 )
114             ));
115             $object = $this->_getController()->search($filter, null, false, false, 'sync')->getFirstRecord();
116             
117             if ($object == null) {
118                 throw new Sabre\DAV\Exception\NotFound('Object not found');
119             }
120         }
121         
122         if ($object->has('tags') && !isset($object->tags)) {
123             Tinebase_Tags::getInstance()->getTagsOfRecord($object);
124         }
125         
126         $objectClass = $this->_application->name . '_Frontend_WebDAV_' . $this->_model;
127         
128         return new $objectClass($this->_container, $object);
129     }
130     
131     /**
132      * Returns an array with all the child nodes
133      *
134      * @return Sabre\DAV\INode[]
135      */
136     function getChildren()
137     {
138         $filterClass = $this->_application->name . '_Model_' . $this->_model . 'Filter';
139         $filter = new $filterClass(array(
140             array(
141                 'field'     => 'container_id',
142                 'operator'  => 'equals',
143                 'value'     => $this->_container->getId()
144             )
145         ));
146
147         /*
148          * see http://forge.tine20.org/mantisbt/view.php?id=5122
149          * we must use action 'sync' and not 'get' as 
150          * otherwise the calendar also return events the user only can see because of freebusy
151          */        
152         $objects = $this->_getController()->search($filter, null, false, false, 'sync');
153         
154         $children = array();
155         
156         foreach ($objects as $object) {
157             $children[] = $this->getChild($object);
158         }
159
160         return $children;
161     }
162     
163     /**
164      * return etag
165      * 
166      * @return string
167      */
168     public function getETag()
169     {
170         return '"' . $this->_container->seq . '"';
171     }
172     
173     /**
174      * Returns a group principal
175      *
176      * This must be a url to a principal, or null if there's no owner
177      *
178      * @return string|null
179      */
180     public function getGroup()
181     {
182         return null;
183     }
184     
185     /**
186      * Returns a list of ACE's for this node.
187      *
188      * Each ACE has the following properties:
189      *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
190      *     currently the only supported privileges
191      *   * 'principal', a url to the principal who owns the node
192      *   * 'protected' (optional), indicating that this ACE is not allowed to
193      *      be updated.
194      *      
195      * @todo implement real logic
196      * @return array
197      */
198     public function getACL() 
199     {
200         $acl    = array();
201         
202         $grants = Tinebase_Container::getInstance()->getGrantsOfContainer($this->_container, true);
203         
204         foreach ($grants as $grant) {
205             switch ($grant->account_type) {
206                 case Tinebase_Acl_Rights::ACCOUNT_TYPE_ANYONE:
207                     $principal = 'principals/users/' . Tinebase_Core::getUser()->contact_id;
208                     break;
209                     
210                 case Tinebase_Acl_Rights::ACCOUNT_TYPE_GROUP:
211                     try {
212                         $group = Tinebase_Group::getInstance()->getGroupById($grant->account_id);
213                     } catch (Tinebase_Exception_Record_NotDefined $ternd) {
214                         // skip group
215                         continue 2;
216                     } catch (Tinebase_Exception_NotFound $tenf) {
217                         // skip group
218                         continue 2;
219                     }
220                     
221                     $principal = 'principals/groups/' . $group->list_id;
222                     
223                     break;
224                     
225                 case Tinebase_Acl_Rights::ACCOUNT_TYPE_USER:
226                     try {
227                         $fulluser = Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend('accountId', $grant->account_id, 'Tinebase_Model_FullUser');
228                     } catch (Tinebase_Exception_Record_NotDefined $ternd) {
229                         // skip group
230                         continue 2;
231                     } catch (Tinebase_Exception_NotFound $tenf) {
232                         // skip user
233                         continue 2;
234                     }
235                     
236                     $principal = 'principals/users/' . $fulluser->contact_id;
237                     
238                     break;
239                     
240                 default:
241                     throw new Tinebase_Exception_UnexpectedValue('unsupported account type');
242             }
243             
244             if($grant[Tinebase_Model_Grants::GRANT_READ] == true) {
245                 $acl[] = array(
246                     'privilege' => '{DAV:}read',
247                     'principal' => $principal,
248                     'protected' => true,
249                 );
250             }
251             if($grant[Tinebase_Model_Grants::GRANT_EDIT] == true) {
252                 $acl[] = array(
253                     'privilege' => '{DAV:}write-content',
254                     'principal' => $principal,
255                     'protected' => true,
256                 );
257             }
258             if($grant[Tinebase_Model_Grants::GRANT_ADD] == true) {
259                 $acl[] = array(
260                     'privilege' => '{DAV:}bind',
261                     'principal' => $principal,
262                     'protected' => true,
263                 );
264             }
265             if($grant[Tinebase_Model_Grants::GRANT_DELETE] == true) {
266                 $acl[] = array(
267                     'privilege' => '{DAV:}unbind',
268                     'principal' => $principal,
269                     'protected' => true,
270                 );
271             }
272             if($grant[Tinebase_Model_Grants::GRANT_ADMIN] == true) {
273                 $acl[] = array(
274                     'privilege' => '{DAV:}write-properties',
275                     'principal' => $principal,
276                     'protected' => true,
277                 );
278             }
279         }
280
281         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
282             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' webdav acl ' . print_r($acl, true));
283         
284         return $acl;
285     }
286     
287     /**
288      * Returns the last modification date as a unix timestamp
289      *
290      * @return time
291      */
292     public function getLastModified() 
293     {
294         if ($this->_container->last_modified_time instanceof Tinebase_DateTime) {
295             return $this->_container->last_modified_time->getTimestamp();
296         }
297         
298         if ($this->_container->creation_time instanceof Tinebase_DateTime) {
299             return $this->_container->creation_time->getTimestamp();
300         }
301         
302         return Tinebase_DateTime::now()->getTimestamp();
303     }
304     
305     /**
306      * Returns the name of the node
307      *
308      * @return string
309      */
310     public function getName()
311     {
312         if ($this->_useIdAsName == true) {
313             if ($this->_container->uuid) {
314                 return $this->_container->uuid;
315             } else {
316                 return $this->_container->getId();
317             }
318         } 
319         
320         return $this->_container->name;
321     }
322     
323     /**
324      * Returns the owner principal
325      *
326      * This must be a url to a principal, or null if there's no owner
327      * 
328      * @todo implement real logic
329      * @return string|null
330      */
331     public function getOwner()
332     {
333         if (! Tinebase_Container::getInstance()->hasGrant(
334             Tinebase_Core::getUser(), 
335             $this->_container, 
336             Tinebase_Model_Grants::GRANT_ADMIN)
337         ) {
338             return null;
339         }
340         
341         return 'principals/users/' . Tinebase_Core::getUser()->contact_id;
342     }
343     
344     /**
345      * Returns the list of properties
346      *
347      * @param array $requestedProperties
348      * @return array
349      */
350     public function getProperties($requestedProperties) 
351     {
352         $properties = array();
353         
354         $response = array();
355         
356         foreach ($requestedProperties as $prop) {
357             switch($prop) {
358                 case '{DAV:}getetag':
359                     $response[$prop] = $this->getETag();
360                     break;
361
362                 case '{DAV:}sync-token':
363                     $response[$prop] = $this->getSyncToken();
364                     break;
365                     
366                 default:
367                     if (isset($properties[$prop])) $response[$prop] = $properties[$prop];
368                     break;
369             }
370         }
371         
372         return $response;
373     }
374     
375     /**
376      * Updates the ACL
377      *
378      * This method will receive a list of new ACE's.
379      *
380      * @param array $acl
381      * @return void
382      */
383     public function setACL(array $acl)
384     {
385         throw new Sabre\DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
386     }
387     
388     /**
389      * Updates properties on this node,
390      *
391      * The properties array uses the propertyName in clark-notation as key,
392      * and the array value for the property value. In the case a property
393      * should be deleted, the property value will be null.
394      *
395      * This method must be atomic. If one property cannot be changed, the
396      * entire operation must fail.
397      *
398      * If the operation was successful, true can be returned.
399      * If the operation failed, false can be returned.
400      *
401      * Deletion of a non-existant property is always succesful.
402      *
403      * Lastly, it is optional to return detailed information about any
404      * failures. In this case an array should be returned with the following
405      * structure:
406      *
407      * array(
408      *   403 => array(
409      *      '{DAV:}displayname' => null,
410      *   ),
411      *   424 => array(
412      *      '{DAV:}owner' => null,
413      *   )
414      * )
415      *
416      * In this example it was forbidden to update {DAV:}displayname. 
417      * (403 Forbidden), which in turn also caused {DAV:}owner to fail
418      * (424 Failed Dependency) because the request needs to be atomic.
419      *
420      * @param array $mutations 
421      * @return bool|array 
422      */
423     public function updateProperties($mutations) 
424     {
425         if (!Tinebase_Core::getUser()->hasGrant($this->_container, Tinebase_Model_Grants::GRANT_ADMIN)) {
426             throw new \Sabre\DAV\Exception\Forbidden('permission to update container denied');
427         }
428         
429         $result = array(
430             200 => array(),
431             403 => array()
432         );
433         
434         foreach ($mutations as $key => $value) {
435             switch ($key) {
436                 case '{DAV:}displayname':
437                     if ($value === $this->_container->uuid || $value === $this->_container->getId()) {
438                         if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ 
439                             . ' It is not allowed to overwrite the name with the uuid/id');
440                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
441                             . ' ' . print_r(array(
442                                 'useIdAsName' => $this->_useIdAsName,
443                                 'container'   => $this->_container->toArray(),
444                                 'new value'   => $value
445                             ), true));
446                     } else {
447                         $this->_container->name = $value;
448                     }
449                     $result['200'][$key] = null;
450                     break;
451                     
452                 case '{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}calendar-description':
453                 case '{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}calendar-timezone':
454                     // fake success
455                     $result['200'][$key] = null;
456                     break;
457                     
458                 case '{http://apple.com/ns/ical/}calendar-color':
459                     $this->_container->color = substr($value, 0, 7);
460                     $result['200'][$key] = null;
461                     break;
462                 
463                 default:
464                     $result['403'][$key] = null;
465             }
466         }
467         
468         Tinebase_Container::getInstance()->update($this->_container);
469         
470         return $result;
471     }
472     
473     /**
474      * 
475      * @return Tinebase_Controller_Record_Interface
476      */
477     protected function _getController()
478     {
479         if ($this->_controller === null) {
480             $this->_controller = Tinebase_Core::getApplicationInstance($this->_application->name, $this->_model);
481         }
482         
483         return $this->_controller;
484     }
485     
486     /**
487      * get id from name => strip of everything after last dot
488      * 
489      * @param  string  $_name  the name for example vcard.vcf
490      * @return string
491      */
492     protected function _getIdFromName($_name)
493     {
494         $id = ($pos = strrpos($_name, '.')) === false ? $_name : substr($_name, 0, $pos);
495         $id = strlen($id) > 40 ? sha1($id) : $id;
496         
497         return $id;
498     }
499     
500     /**
501      * generate VTimezone for given folder
502      * 
503      * @param  string|Tinebase_Model_Application  $applicationName
504      * @return string
505      */
506     public static function getCalendarVTimezone($applicationName)
507     {
508         $timezone = Tinebase_Core::getPreference()->getValueForUser(Tinebase_Preference::TIMEZONE, Tinebase_Core::getUser()->getId());
509         
510         $application = $applicationName instanceof Tinebase_Model_Application 
511             ? $applicationName 
512             : Tinebase_Application::getInstance()->getApplicationByName($applicationName); 
513         
514         // create vcalendar object with timezone information
515         $vcalendar = new \Sabre\VObject\Component\VCalendar(array(
516             'PRODID'   => "-//tine20.org//Tine 2.0 {$application->name} V{$application->version}//EN",
517             'VERSION'  => '2.0',
518             'CALSCALE' => 'GREGORIAN'
519         ));
520         $vcalendar->add(new Sabre_VObject_Component_VTimezone($timezone));
521         
522         // Taking out \r to not screw up the xml output
523         return str_replace("\r","", $vcalendar->serialize());
524     }
525     
526     /**
527      * 
528      */
529     public function getSupportedPrivilegeSet()
530     {
531         return null;
532     }
533
534     /**
535      * indicates whether the concrete class supports sync-token
536      *
537      * @return bool
538      */
539     public function supportsSyncToken()
540     {
541         return false;
542     }
543
544     /**
545      * Returns the content sequence for this container
546      *
547      * @return string
548      */
549     public function getSyncToken()
550     {
551        return Tinebase_Container::getInstance()->getContentSequence($this->_container);
552     }
553
554     /**
555      * returns the changes happened since the provided syncToken which is the content sequence
556      *
557      * @param string $syncToken
558      * @return array
559      */
560     public function getChanges($syncToken)
561     {
562         $result = array(
563             'syncToken' => $this->getSyncToken(),
564             Tinebase_Model_ContainerContent::ACTION_CREATE => array(),
565             Tinebase_Model_ContainerContent::ACTION_UPDATE => array(),
566             Tinebase_Model_ContainerContent::ACTION_DELETE => array(),
567         );
568
569         $resultSet = Tinebase_Container::getInstance()->getContentHistory($this->_container, $syncToken);
570         foreach($resultSet as $contentModel) {
571             switch($contentModel->action) {
572                 case Tinebase_Model_ContainerContent::ACTION_DELETE:
573                     unset($result[Tinebase_Model_ContainerContent::ACTION_CREATE][$contentModel->record_id]);
574                     unset($result[Tinebase_Model_ContainerContent::ACTION_UPDATE][$contentModel->record_id]);
575                 case Tinebase_Model_ContainerContent::ACTION_CREATE:
576                 case Tinebase_Model_ContainerContent::ACTION_UPDATE:
577                     $result[$contentModel->action][$contentModel->record_id] = $contentModel->record_id . $this->_suffix;
578                     break;
579
580                 default:
581                     Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' unknown Tinebase_Model_ContainerContent::ACTION_* found: ' . $contentModel->action . ' ... ignoring');
582                     break;
583             }
584         }
585
586         return $result;
587     }
588 }