throw sabredav exception when application is unavailable
[tine20] / tine20 / Tinebase / WebDav / Collection / AbstractContainerTree.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) 2014-2014 Metaways Infosystems GmbH (http://www.metaways.de)
10  */
11
12 /**
13  * class to handle top level folders for an application
14  *
15  * @package     Tinebase
16  * @subpackage  WebDAV
17  */
18 abstract class Tinebase_WebDav_Collection_AbstractContainerTree extends \Sabre\DAV\Collection implements \Sabre\DAV\IProperties, \Sabre\DAVACL\IACL, \Sabre\DAV\IExtendedCollection
19 {
20     /**
21      * the current application object
22      * 
23      * @var Tinebase_Model_Application
24      */
25     protected $_application;
26     
27     /**
28      * application name
29      *
30      * @var string
31      */
32     protected $_applicationName;
33     
34     /**
35      * app has personal folders
36      *
37      * @var string
38      */
39     protected $_hasPersonalFolders = true;
40     
41     /**
42      * app has records folder
43      *
44      * @var string
45      */
46     protected $_hasRecordFolder = true;
47     
48     /**
49      * the current path
50      * 
51      * @var string
52      */
53     protected $_path;
54     
55     /**
56      * @var array
57      */
58     protected $_pathParts;
59     
60     /**
61      * 
62      * @var boolean
63      */
64     protected $_useIdAsName;
65     
66     /**
67      * contructor
68      * 
69      * @param string $path         the current path
70      * @param bool   $useIdAsName  use name or id as node name
71      */
72     public function __construct($path, $useIdAsName = false)
73     {
74         $this->_path        = $path;
75         $this->_useIdAsName = $useIdAsName;
76     }
77     
78     /**
79      * (non-PHPdoc)
80      * @see \Sabre\DAV\Collection::createDirectory()
81      */
82     public function createDirectory($name) 
83     {
84         return $this->_createContainer(array(
85             'name' => $name
86         ));
87     }
88     
89     /**
90      * (non-PHPdoc)
91      * @see \Sabre\DAV\IExtendedCollection::createExtendedCollection()
92      */
93     function createExtendedCollection($name, array $resourceType, array $properties)
94     {
95         return $this->_createContainer(array(
96             'name'  => isset($properties['{DAV:}displayname']) ? $properties['{DAV:}displayname'] : $name,
97             'uuid'  => $name,
98             'color' => isset($properties['{http://apple.com/ns/ical/}calendar-color']) ? substr($properties['{http://apple.com/ns/ical/}calendar-color'], 0, 7) : null
99         ));
100     }
101     
102     /**
103      * (non-PHPdoc)
104      * @see Sabre\DAV\Collection::getChild()
105      */
106     public function getChild($name)
107     {
108         switch (count($this->_getPathParts())) {
109             # path == /<applicationPrefix> (for example calendars)
110             # return folders for currentuser, other users and 'shared' folder
111             # $name can be
112             # * contact_id of user
113             # * 'shared'
114             case 1:
115                 if ($name === Tinebase_Model_Container::TYPE_SHARED ||
116                     ($this->_hasRecordFolder && $name === Tinebase_FileSystem::FOLDER_TYPE_RECORDS)) {
117                     $path = $this->_path . '/' . $name;
118                     
119                 } elseif ($this->_hasPersonalFolders) {
120                     if ($name === '__currentuser__') {
121                         $path = $this->_path . '/__currentuser__';
122                         
123                     } else {
124                         try {
125                             $contact = $this->_getContact($name);
126                             
127                         } catch (Tinebase_Exception_NotFound $tenf) {
128                             throw new \Sabre\DAV\Exception\NotFound("Directory $this->_path/$name not found");
129                         }
130                         
131                         $path = $this->_path . '/' . ($this->_useIdAsName ? $contact->getId() : $contact->n_fileas);
132                     }
133                     
134                 } else {
135                     throw new \Sabre\DAV\Exception\NotFound("Directory $this->_path/$name not found");
136                 }
137                 
138                 $className = $this->_getAppliationName() . '_Frontend_WebDAV';
139                 
140                 return new $className($path, $this->_useIdAsName);
141                 
142                 break;
143                 
144             # path == /<applicationPrefix>/<contactid>|'shared'
145             # list container
146             case 2:
147                 if (array_value(1, $this->_getPathParts()) == Tinebase_Model_Container::TYPE_SHARED) {
148                     try { 
149                         if ($name instanceof Tinebase_Model_Container) {
150                             $container = $name;
151                         } elseif ($this->_useIdAsName) {
152                             // first try to fetch by uuid ...
153                             try {
154                                 $container = Tinebase_Container::getInstance()->getByProperty($name, 'uuid');
155                             } catch (Tinebase_Exception_NotFound $tenf) {
156                                 // ... if that fails by id
157                                 $container = Tinebase_Container::getInstance()->getContainerById($name);
158                             }
159                         } else {
160                             $container = Tinebase_Container::getInstance()->getContainerByName(
161                                 $this->_getAppliationName(), 
162                                 $name, 
163                                 Tinebase_Model_Container::TYPE_SHARED
164                             );
165                         }
166                         
167                     } catch (Tinebase_Exception_NotFound $tenf) {
168                         throw new \Sabre\DAV\Exception\NotFound("Directory $this->_path/$name not found");
169                         
170                     } catch (Tinebase_Exception_InvalidArgument $teia) {
171                         // invalid container id provided
172                         throw new \Sabre\DAV\Exception\NotFound("Directory $this->_path/$name not found");
173                     }
174                 } elseif ($this->_hasRecordFolder && array_value(1, $this->_getPathParts()) == Tinebase_FileSystem::FOLDER_TYPE_RECORDS) {
175                     
176                     return new Tinebase_Frontend_WebDAV_RecordCollection($this->_path . '/' . $name);
177                     
178                 } elseif ($this->_hasPersonalFolders) {
179                     if (array_value(1, $this->_getPathParts()) === '__currentuser__') {
180                         $accountId = Tinebase_Core::getUser()->accountId;
181                         
182                     } else {
183                         try {
184                             $accountId = $this->_getContact(array_value(1, $this->_getPathParts()))->account_id;
185                             
186                         } catch (Tinebase_Exception_NotFound $tenf) {
187                             throw new \Sabre\DAV\Exception\NotFound("Directory $this->_path/$name not found");
188                         }
189                     }
190                     
191                     try {
192                         if ($name instanceof Tinebase_Model_Container) {
193                             $container = $name;
194                         } elseif ($this->_useIdAsName) {
195                             // first try to fetch by uuid ...
196                             try {
197                                 $container = Tinebase_Container::getInstance()->getByProperty((string) $name, 'uuid');
198                             } catch (Tinebase_Exception_NotFound $tenf) {
199                                 // ... if that fails by id
200                                 $container = Tinebase_Container::getInstance()->getContainerById($name);
201                             }
202                             
203                         } else { 
204                             $container = Tinebase_Container::getInstance()->getContainerByName(
205                                 $this->_getAppliationName(), 
206                                 $name,
207                                 Tinebase_Model_Container::TYPE_PERSONAL, 
208                                 $accountId
209                             );
210                         }
211                         
212                     } catch (Tinebase_Exception_NotFound $tenf) {
213                         throw new \Sabre\DAV\Exception\NotFound("Directory $this->_path/$name not found");
214                         
215                     } catch (Tinebase_Exception_InvalidArgument $teia) {
216                         // invalid container id provided
217                         throw new \Sabre\DAV\Exception\NotFound("Directory $this->_path/$name not found");
218                     }
219                     
220                 } else {
221                     throw new \Sabre\DAV\Exception\NotFound("Directory $this->_path/$name not found");
222                 }
223                 
224                 if (!Tinebase_Core::getUser()->hasGrant($container, Tinebase_Model_Grants::GRANT_READ) ||
225                     !Tinebase_Core::getUser()->hasGrant($container, Tinebase_Model_Grants::GRANT_SYNC)) {
226                     throw new \Sabre\DAV\Exception\NotFound("Directory $this->_path/$name not found");
227                 }
228                 
229                 $objectClass = Tinebase_Application::getInstance()->getApplicationById($container->application_id)->name . '_Frontend_WebDAV_Container';
230                 
231                 return new $objectClass($container, $this->_useIdAsName);
232                 
233                 break;
234                 
235             default:
236                 throw new Sabre\DAV\Exception\NotFound("Directory $this->_path/$name not found");
237             
238                 break;
239         }
240     
241         
242     }
243     
244     /**
245      * Returns an array with all the child nodes
246      * 
247      * the records subtree is not returned as child here. It's only available via getChild().
248      *
249      * @return \Sabre\DAV\INode[]
250      */
251     function getChildren()
252     {
253         $children = array();
254         
255         switch (count($this->_getPathParts())) {
256             # path == /<applicationPrefix> (for example calendars)
257             # return folders for currentuser, other users and 'shared' folder
258             case 1:
259                 $children[] = $this->getChild(Tinebase_Model_Container::TYPE_SHARED);
260                 
261                 if ($this->_hasPersonalFolders) {
262                     $children[] = $this->getChild($this->_useIdAsName ? Tinebase_Core::getUser()->contact_id : Tinebase_Core::getUser()->accountDisplayName);
263                     
264                     $otherUsers = Tinebase_Container::getInstance()->getOtherUsers(Tinebase_Core::getUser(), $this->_getAppliationName(), array(
265                         Tinebase_Model_Grants::GRANT_READ,
266                         Tinebase_Model_Grants::GRANT_SYNC
267                     ));
268                     
269                     foreach ($otherUsers as $user) {
270                         if ($user->contact_id && $user->visibility === Tinebase_Model_User::VISIBILITY_DISPLAYED) {
271                             try {
272                                 $children[] = $this->getChild($this->_useIdAsName ? $user->contact_id : $user->accountDisplayName);
273                             } catch (\Sabre\DAV\Exception\NotFound $sdavenf) {
274                                 // ignore contacts not found
275                             }
276                         }
277                     }
278                 }
279         
280                 break;
281             
282             # path == /<applicationPrefix>/<contactid>|'shared'
283             # list container
284             case 2:
285                 if (array_value(1, $this->_getPathParts()) == Tinebase_Model_Container::TYPE_SHARED) {
286                     $containers = Tinebase_Container::getInstance()->getSharedContainer(
287                         Tinebase_Core::getUser(),
288                         $this->_getAppliationName(),
289                         array(
290                             Tinebase_Model_Grants::GRANT_READ,
291                             Tinebase_Model_Grants::GRANT_SYNC
292                         )
293                     );
294                     
295                 } elseif ($this->_hasPersonalFolders) {
296                     if (array_value(1, $this->_getPathParts()) === '__currentuser__') {
297                         $accountId = Tinebase_Core::getUser()->accountId;
298                         
299                     } else {
300                         try {
301                             $accountId = $this->_getContact(array_value(1, $this->_getPathParts()))->account_id;
302                         } catch (Tinebase_Exception_NotFound $tenf) {
303                             throw new \Sabre\DAV\Exception\NotFound("Path $this->_path not found");
304                         }
305                     }
306                     
307                     try {
308                         $containers = Tinebase_Container::getInstance()->getPersonalContainer(
309                             Tinebase_Core::getUser(),
310                             $this->_getAppliationName(),
311                             $accountId,
312                             array(
313                                 Tinebase_Model_Grants::GRANT_READ, 
314                                 Tinebase_Model_Grants::GRANT_SYNC
315                             )
316                         );
317                     } catch (Tinebase_Exception_AccessDenied $tead) {
318                         throw new Sabre\DAV\Exception\NotFound("Could not find path (" . $tead->getMessage() . ")");
319                     }
320                     
321                 } else {
322                     throw new Sabre\DAV\Exception\NotFound("Path $this->_path not found");
323                 }
324                 
325                 foreach ($containers as $container) {
326                     try {
327                         $children[] = $this->getChild($container);
328                     } catch (\Sabre\DAV\Exception\NotFound $sdavenf) {
329                         // ignore containers not found
330                     }
331                 }
332                 
333                 break;
334                 
335             default:
336                 throw new Sabre\DAV\Exception\NotFound("Path $this->_path not found");
337                 
338                 break;
339         }
340         
341         return $children;
342     }
343     
344     /**
345      * return etag
346      * 
347      * @return string
348      */
349     public function getETag()
350     {
351         $etags = array();
352         
353         foreach ($this->getChildren() as $child) {
354             $etags[] = $child->getETag();
355         }
356         
357         return '"' . sha1(implode(null, $etags)) . '"';
358     }
359     
360     /**
361      * Returns a group principal
362      *
363      * This must be a url to a principal, or null if there's no owner
364      *
365      * @return string|null
366      */
367     public function getGroup()
368     {
369         return null;
370     }
371     
372     /**
373      * (non-PHPdoc)
374      * @see \Sabre\DAV\Node::getLastModified()
375      */
376     public function getLastModified()
377     {
378         $lastModified = 1;
379         
380         foreach ($this->getChildren() as $child) {
381             $lastModified = $child->getLastModified() > $lastModified ? $child->getLastModified() : $lastModified;
382         }
383         
384         return $lastModified;
385     }
386     
387     /**
388      * Returns a list of ACE's for this node.
389      *
390      * Each ACE has the following properties:
391      *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
392      *     currently the only supported privileges
393      *   * 'principal', a url to the principal who owns the node
394      *   * 'protected' (optional), indicating that this ACE is not allowed to
395      *      be updated.
396      *      
397      * @todo implement real logic
398      * @return array
399      */
400     public function getACL() 
401     {
402         $principal = 'principals/users/' . Tinebase_Core::getUser()->contact_id;
403         
404         return array(
405             array(
406                 'privilege' => '{DAV:}read',
407                 'principal' => $principal,
408                 'protected' => true,
409             ),
410             array(
411                 'privilege' => '{DAV:}write',
412                 'principal' => $principal,
413                 'protected' => true,
414             )
415         );
416     }
417     
418     /**
419      * Returns the name of the node
420      *
421      * @return string
422      */
423     public function getName()
424     {
425         if (count($this->_getPathParts()) === 2 && 
426             array_value(1, $this->_getPathParts()) !== Tinebase_Model_Container::TYPE_SHARED &&
427             !$this->_useIdAsName
428         ) {
429             try {
430                 $contact = $this->_getContact(array_value(1, $this->_getPathParts()));
431                 
432                 $name = $contact->n_fileas;
433                 
434             } catch (Tinebase_Exception_NotFound $tenf) {
435                 list(,$name) = Sabre\DAV\URLUtil::splitPath($this->_path);
436             }
437             
438         } else {
439             list(,$name) = Sabre\DAV\URLUtil::splitPath($this->_path);
440         }
441         
442         return $name;
443     }
444     
445     /**
446      * Returns the owner principal
447      *
448      * This must be a url to a principal, or null if there's no owner
449      * 
450      * @return string|null
451      */
452     public function getOwner()
453     {
454         if (count($this->_getPathParts()) === 2 && $this->getName() !== Tinebase_Model_Container::TYPE_SHARED) {
455             try {
456                 $contact = $this->_getContact(array_value(1, $this->_getPathParts()));
457             } catch (Tinebase_Exception_NotFound $tenf) {
458                 return null;
459             }
460             
461             return 'principals/users/' . $contact->getId();
462         }
463         
464         return null;
465     }
466     
467     /**
468      * Returns the list of properties
469      *
470      * @param array $requestedProperties
471      * @return array
472      */
473     public function getProperties($requestedProperties) 
474     {
475         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
476             __METHOD__ . '::' . __LINE__ . ' path: ' . $this->_path . ' ' . print_r($requestedProperties, true));
477         
478         $response = array();
479     
480         foreach ($requestedProperties as $property) {
481             switch ($property) {
482                 case '{DAV:}displayname':
483                     if (count($this->_getPathParts()) === 2 && $this->getName() !== Tinebase_Model_Container::TYPE_SHARED) {
484                         try {
485                             $contact = $this->_getContact(array_value(1, $this->_getPathParts()));
486                         } catch (Tinebase_Exception_NotFound $tenf) {
487                             continue;
488                         }
489                         
490                         $response[$property] = $contact->n_fileas;
491                     }
492                     
493                     break;
494                     
495                 case '{DAV:}owner':
496                     if ($this->getOwner()) {
497                         $response[$property] = new \Sabre\DAVACL\Property\Principal(
498                             \Sabre\DAVACL\Property\Principal::HREF, $this->getOwner()
499                         );
500                     }
501                     
502                     break;
503                     
504                 case '{DAV:}getetag':
505                     $response[$property] = $this->getETag();
506                     
507                     break;
508             }
509         }
510         
511         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
512             __METHOD__ . '::' . __LINE__ . ' path: ' . $this->_path . ' ' . print_r($response, true));
513         
514         return $response;
515     }
516     
517     /**
518      * Updates the ACL
519      *
520      * This method will receive a list of new ACE's.
521      *
522      * @param array $acl
523      * @return void
524      */
525     public function setACL(array $acl)
526     {
527         throw new Sabre\DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
528     }
529     
530     /**
531      * Updates properties on this node,
532      *
533      * The properties array uses the propertyName in clark-notation as key,
534      * and the array value for the property value. In the case a property
535      * should be deleted, the property value will be null.
536      *
537      * This method must be atomic. If one property cannot be changed, the
538      * entire operation must fail.
539      *
540      * If the operation was successful, true can be returned.
541      * If the operation failed, false can be returned.
542      *
543      * Deletion of a non-existant property is always succesful.
544      *
545      * Lastly, it is optional to return detailed information about any
546      * failures. In this case an array should be returned with the following
547      * structure:
548      *
549      * array(
550      *   403 => array(
551      *      '{DAV:}displayname' => null,
552      *   ),
553      *   424 => array(
554      *      '{DAV:}owner' => null,
555      *   )
556      * )
557      *
558      * In this example it was forbidden to update {DAV:}displayname. 
559      * (403 Forbidden), which in turn also caused {DAV:}owner to fail
560      * (424 Failed Dependency) because the request needs to be atomic.
561      *
562      * @param array $mutations 
563      * @return bool|array 
564      */
565     public function updateProperties($mutations) 
566     {
567         throw new Sabre\DAV\Exception\MethodNotAllowed('Changing properties is not yet supported');
568     }
569     
570     /**
571      * 
572      */
573     public function getSupportedPrivilegeSet()
574     {
575         return null;
576     }
577     
578     /**
579      * return application object
580      * 
581      * @return Tinebase_Model_Application
582      */
583     protected function _getApplication()
584     {
585         if (!$this->_application) {
586             $this->_application = Tinebase_Application::getInstance()->getApplicationByName($this->_getAppliationName());
587         }
588         
589         return $this->_application;
590     }
591     
592     /**
593      * creates a new container
594      * 
595      * @todo allow to create personal folders only when in currents users own path
596      * 
597      * @param  array  $properties  properties for new container
598      * @throws \Sabre\DAV\Exception\Forbidden
599      * @return Tinebase_Model_Container
600      */
601     protected function _createContainer(array $properties) 
602     {
603         if (count($this->_getPathParts()) !== 2) {
604             throw new \Sabre\DAV\Exception\Forbidden('Permission denied to create directory ' . $properties['name']);
605         }
606         
607         $containerType = array_value(1, $this->_getPathParts()) == Tinebase_Model_Container::TYPE_SHARED ?
608             Tinebase_Model_Container::TYPE_SHARED :
609             Tinebase_Model_Container::TYPE_PERSONAL;
610         
611         $newContainer = new Tinebase_Model_Container(array_merge($properties, array(
612             'type'              => $containerType,
613             'backend'           => 'Sql',
614             'application_id'    => $this->_getApplication()->getId(),
615             'model'             => Tinebase_Core::getApplicationInstance($this->_applicationName)->getDefaultModel()
616         )));
617
618         try {
619             $container = Tinebase_Container::getInstance()->addContainer($newContainer);
620         } catch (Tinebase_Exception_AccessDenied $tead) {
621             throw new \Sabre\DAV\Exception\Forbidden('Permission denied to create directory ' . $name);
622         }
623         
624         return $container;
625     }
626     
627     /**
628      * return application name
629      * 
630      * @return string
631      */
632     protected function _getAppliationName()
633     {
634         if (!$this->_applicationName) {
635             $this->_applicationName = array_value(0, explode('_', get_class($this)));
636         }
637         
638         return $this->_applicationName;
639     }
640     
641     /**
642      * get path parts
643      * 
644      * @return array
645      */
646     protected function _getPathParts()
647     {
648         if (!$this->_pathParts) {
649             $this->_pathParts = $this->_parsePath($this->_path);
650         }
651         
652         return $this->_pathParts;
653     }
654     
655     /**
656      * split path into parts
657      * 
658      * @param  string  $_path
659      * @return array
660      */
661     protected function _parsePath($_path)
662     {
663         $pathParts = explode('/', trim($this->_path, '/'));
664         
665         return $pathParts;
666     }
667     
668     /**
669      * resolve contact_id to Addressbook_Model_Contact
670      * 
671      * @return Addressbook_Model_Contact
672      */
673     protected function _getContact($contactId)
674     {
675         $filter = new Addressbook_Model_ContactFilter(array(
676             array(
677                 'field'     => 'type',
678                 'operator'  => 'equals',
679                 'value'     => Addressbook_Model_Contact::CONTACTTYPE_USER
680             ),
681             array(
682                 'field'     => $this->_useIdAsName ? 'id' : 'n_fileas',
683                 'operator'  => 'equals',
684                 'value'     => $contactId
685             ),
686         ));
687         
688         $contact = Addressbook_Controller_Contact::getInstance()->search($filter)->getFirstRecord();
689         
690         if (!$contact) {
691             throw new Tinebase_Exception_NotFound('contact not found');
692         }
693         
694         return $contact;
695     }
696 }