0916703b10a9c2e96d19d7d87533d333708e478d
[tine20] / tine20 / Filemanager / Controller / Node.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Filemanager
6  * @subpackage  Controller
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Philipp Schüle <p.schuele@metaways.de>
9  * @copyright   Copyright (c) 2011-2012 Metaways Infosystems GmbH (http://www.metaways.de)
10  * 
11  * @todo        add transactions to move/create/delete/copy 
12  */
13
14 /**
15  * Node controller for Filemanager
16  *
17  * @package     Filemanager
18  * @subpackage  Controller
19  */
20 class Filemanager_Controller_Node extends Tinebase_Controller_Record_Abstract
21 {
22     /**
23      * application name (is needed in checkRight())
24      *
25      * @var string
26      */
27     protected $_applicationName = 'Filemanager';
28     
29     /**
30      * Filesystem backend
31      *
32      * @var Tinebase_FileSystem
33      */
34     protected $_backend = NULL;
35     
36     /**
37      * the model handled by this controller
38      * @var string
39      */
40     protected $_modelName = 'Filemanager_Model_Node';
41     
42     /**
43      * TODO handle modlog
44      * @var boolean
45      */
46     protected $_omitModLog = TRUE;
47     
48     /**
49      * holds the total count of the last recursive search
50      * @var integer
51      */
52     protected $_recursiveSearchTotalCount = 0;
53     
54     /**
55      * holds the instance of the singleton
56      *
57      * @var Filemanager_Controller_Node
58      */
59     private static $_instance = NULL;
60     
61     /**
62      * the constructor
63      *
64      * don't use the constructor. use the singleton
65      */
66     private function __construct() 
67     {
68         $this->_backend = Tinebase_FileSystem::getInstance();
69     }
70     
71     /**
72      * don't clone. Use the singleton.
73      *
74      */
75     private function __clone() 
76     {
77     }
78     
79     /**
80      * the singleton pattern
81      *
82      * @return Filemanager_Controller_Node
83      */
84     public static function getInstance() 
85     {
86         if (self::$_instance === NULL) {
87             self::$_instance = new Filemanager_Controller_Node();
88         }
89         
90         return self::$_instance;
91     }
92     
93     /**
94      * (non-PHPdoc)
95      * @see Tinebase_Controller_Record_Abstract::update()
96      */
97     public function update(Tinebase_Record_Interface $_record)
98     {
99         if (! $this->_checkACLContainer($this->_backend->getNodeContainer($_record->getId()), 'update')) {
100             throw new Tinebase_Exception_AccessDenied('No permission to update nodes.');
101         }
102
103         return parent::update($_record);
104     }
105     
106     /**
107      * inspect update of one record (before update)
108      *
109      * @param   Tinebase_Record_Interface $_record      the update record
110      * @param   Tinebase_Record_Interface $_oldRecord   the current persistent record
111      * @return  void
112      */
113     protected function _inspectBeforeUpdate($_record, $_oldRecord)
114     {
115         foreach (array_keys($_record->toArray()) as $property) {
116             if (! in_array($property, array('id', 'name', 'description', 'relations', 'customfields', 'tags', 'notes', 'object_id'))) {
117                 unset($_record->{$property});
118             }
119         }
120     }
121     
122     /**
123      * (non-PHPdoc)
124      * @see Tinebase_Controller_Record_Abstract::getMultiple()
125      * 
126      * @return  Tinebase_Record_RecordSet
127      */
128     public function getMultiple($_ids)
129     {
130         $results = $this->_backend->getMultipleTreeNodes($_ids);
131         $this->resolveMultipleTreeNodesPath($results);
132         
133         return $results;
134     }
135     
136     /**
137      * Resolve path of multiple tree nodes
138      * 
139      * @param Tinebase_Record_RecordSet|Tinebase_Model_Tree_Node $_records
140      */
141     public function resolveMultipleTreeNodesPath($_records)
142     {
143         $records = ($_records instanceof Tinebase_Model_Tree_Node)
144             ? new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array($_records)) : $_records;
145             
146         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
147             . ' Resolving paths for ' . count($records) .  ' records.');
148             
149         foreach ($records as $record) {
150             $path = $this->_backend->getPathOfNode($record, TRUE);
151             $record->path = Tinebase_Model_Tree_Node_Path::removeAppIdFromPath($path, $this->_applicationName);
152
153             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
154                 . ' Got path ' . $record->path .  ' for node ' . $record->name);
155         }
156     }
157     
158     /**
159      * (non-PHPdoc)
160      * @see Tinebase_Controller_Record_Abstract::get()
161      */
162     public function get($_id, $_containerId = NULL)
163     {
164         if (! $this->_checkACLContainer($this->_backend->getNodeContainer($_id), 'get')) {
165             throw new Tinebase_Exception_AccessDenied('No permission to get node');
166         }
167         $record = parent::get($_id);
168         if($record) {
169             $record->notes = Tinebase_Notes::getInstance()->getNotesOfRecord('Tinebase_Model_Tree_Node', $record->getId());
170         }
171         return $record;
172     }
173     
174     /**
175      * search tree nodes
176      * 
177      * @param Tinebase_Model_Filter_FilterGroup|optional $_filter
178      * @param Tinebase_Model_Pagination|optional $_pagination
179      * @param bool $_getRelations
180      * @param bool $_onlyIds
181      * @param string|optional $_action
182      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
183      */
184     public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL, $_getRelations = FALSE, $_onlyIds = FALSE, $_action = 'get')
185     {
186         // perform recursive search on recursive filter set
187         if($_filter->getFilter('recursive')) {
188             return $this->_searchNodesRecursive($_filter, $_pagination);
189         } else {
190             $path = $this->_checkFilterACL($_filter, $_action);
191         }
192         
193         if ($path->containerType === Tinebase_Model_Tree_Node_Path::TYPE_ROOT) {
194             $result = $this->_getRootNodes();
195         } else if ($path->containerType === Tinebase_Model_Container::TYPE_PERSONAL && ! $path->containerOwner) {
196             if (! file_exists($path->statpath)) {
197                 $this->_backend->mkdir($path->statpath);
198             }
199             $result = $this->_getOtherUserNodes();
200         } else {
201             try {
202                 $result = $this->_backend->searchNodes($_filter, $_pagination);
203             } catch (Tinebase_Exception_NotFound $tenf) {
204                 // create basic nodes like personal|shared|user root
205                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . 
206                         ' ' . $path->statpath);
207                 if ($path->name === Tinebase_Model_Container::TYPE_SHARED || 
208                     $path->statpath === $this->_backend->getApplicationBasePath(
209                         Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName), 
210                         Tinebase_Model_Container::TYPE_PERSONAL
211                     ) . '/' . Tinebase_Core::getUser()->getId()
212                 ) {
213                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . 
214                         ' Creating new path ' . $path->statpath);
215                     $this->_backend->mkdir($path->statpath);
216                     $result = $this->_backend->searchNodes($_filter, $_pagination);
217                 } else {
218                     throw $tenf;
219                 }
220             }
221             $this->resolveContainerAndAddPath($result, $path);
222             $this->_sortContainerNodes($result, $path, $_pagination);
223         }
224         return $result;
225     }
226     
227     /**
228      * search tree nodes for search combo
229      * 
230      * @param Tinebase_Model_Tree_Node_Filter $_filter
231      * @param Tinebase_Record_Interface $_pagination
232      * 
233      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
234      */
235     
236     protected function _searchNodesRecursive($_filter, $_pagination)
237     {
238         $files = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
239         $ret = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
240         $folders = $this->_getRootNodes();
241         $folders->merge($this->_getOtherUserNodes());
242
243         while($folders->count()) {
244             $node = $folders->getFirstRecord();
245             $filter = new Tinebase_Model_Tree_Node_Filter(array(
246                 array('field' => 'path', 'operator' => 'equals', 'value' => $node->path),
247                 ), 'AND');
248             $result = $this->search($filter);
249             $folders->merge($result->filter('type', Tinebase_Model_Tree_Node::TYPE_FOLDER));
250             
251             if($_filter->getFilter('query') && $_filter->getFilter('query')->getValue()) {
252                 $files->merge($result->filter('type', Tinebase_Model_Tree_Node::TYPE_FILE)->filter('name', '/^'.$_filter->getFilter('query')->getValue().'./i', true));
253             } else {
254                 $files->merge($result->filter('type', Tinebase_Model_Tree_Node::TYPE_FILE));
255             }
256             
257             $folders->removeRecord($node);
258         }
259         $this->_recursiveSearchTotalCount = $files->count();
260         $ret = $files->sortByPagination($_pagination)->limitByPagination($_pagination);
261         return $ret;
262     }
263     
264     /**
265      * checks filter acl and adds base path
266      * 
267      * @param Tinebase_Model_Filter_FilterGroup $_filter
268      * @param string $_action get|update
269      * @return Tinebase_Model_Tree_Node_Path
270      * @throws Tinebase_Exception_AccessDenied
271      */
272     protected function _checkFilterACL(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
273     {
274         if ($_filter === NULL) {
275             $_filter = new Tinebase_Model_Tree_Node_Filter();
276         }
277         
278         $pathFilters = $_filter->getFilter('path', TRUE);
279         if (count($pathFilters) !== 1) {
280             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
281                 . 'Exactly one path filter required.');
282             $pathFilter = (count($pathFilters) > 1) ? $pathFilters[0] : new Tinebase_Model_Tree_Node_PathFilter(array(
283                 'field'     => 'path',
284                 'operator'  => 'equals',
285                 'value'     => '/',)
286             );
287             $_filter->removeFilter('path');
288             $_filter->addFilter($pathFilter);
289         } else {
290             $pathFilter = $pathFilters[0];
291         }
292         
293         // add base path and check grants
294         try {
295             $path = Tinebase_Model_Tree_Node_Path::createFromPath($this->addBasePath($pathFilter->getValue()));
296         } catch (Exception $e) {
297             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
298                 . ' Could not determine path, setting root path (' . $e->getMessage() . ')');
299             $path = Tinebase_Model_Tree_Node_Path::createFromPath($this->addBasePath('/'));
300         }
301         $pathFilter->setValue($path);
302         
303         $this->_checkPathACL($path, $_action);
304         
305         return $path;
306     }
307     
308     /**
309      * get the three root nodes
310      * 
311      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
312      */
313     protected function _getRootNodes()
314     {
315         $translate = Tinebase_Translation::getTranslation($this->_applicationName);
316         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array(
317             array(
318                 'name' => $translate->_('My folders'),
319                 'path' => '/' . Tinebase_Model_Container::TYPE_PERSONAL . '/' . Tinebase_Core::getUser()->accountLoginName,
320                 'type' => Tinebase_Model_Tree_Node::TYPE_FOLDER,
321                 'id' => Tinebase_Model_Container::TYPE_PERSONAL,
322
323             ),
324             array(
325                 'name' => $translate->_('Shared folders'),
326                 'path' => '/' . Tinebase_Model_Container::TYPE_SHARED,
327                 'type' => Tinebase_Model_Tree_Node::TYPE_FOLDER,
328                 'id' => Tinebase_Model_Container::TYPE_SHARED,
329             
330             ),
331             array(
332                 'name' => $translate->_('Other users folders'),
333                 'path' => '/' . Tinebase_Model_Container::TYPE_PERSONAL,
334                 'type' => Tinebase_Model_Tree_Node::TYPE_FOLDER,
335                 'id' => Tinebase_Model_Container::TYPE_OTHERUSERS,
336             ),
337         ), TRUE); // bypass validation
338         
339         return $result;
340     }
341
342     /**
343      * get other users nodes
344      * 
345      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
346      */
347     protected function _getOtherUserNodes()
348     {
349         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
350         $users = Tinebase_Container::getInstance()->getOtherUsers(Tinebase_Core::getUser(), $this->_applicationName, Tinebase_Model_Grants::GRANT_READ);
351         foreach ($users as $user) {
352             $fullUser = Tinebase_User::getInstance()->getFullUserById($user);
353             $record = new Tinebase_Model_Tree_Node(array(
354                 'name' => $fullUser->accountDisplayName,
355                 'path' => '/' . Tinebase_Model_Container::TYPE_PERSONAL . '/' . $fullUser->accountLoginName,
356                 'type' => Tinebase_Model_Tree_Node::TYPE_FOLDER,
357             ), TRUE);
358             $result->addRecord($record);
359         }
360         
361         return $result;
362     }
363     
364     /**
365      * sort nodes (only checks if we are on the container level and sort by container_name then)
366      * 
367      * @param Tinebase_Record_RecordSet $nodes
368      * @param Tinebase_Model_Tree_Node_Path $path
369      * @param Tinebase_Model_Pagination $pagination
370      */
371     protected function _sortContainerNodes(Tinebase_Record_RecordSet $nodes, Tinebase_Model_Tree_Node_Path $path, Tinebase_Model_Pagination $pagination = NULL)
372     {
373         if ($path->container || ($pagination !== NULL && $pagination->sort && $pagination->sort !== 'name')) {
374             // no toplevel path or no sorting by name -> sorting should be already handled by search()
375             return;
376         }
377         
378         $dir = ($pagination !== NULL && $pagination->dir) ? $pagination->dir : 'ASC';
379         
380         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
381             . ' Sorting container nodes by name (path: ' . $path->flatpath . ') / dir: ' . $dir);
382         
383         $nodes->sort('container_name', $dir);
384     }
385     
386     /**
387      * get file node
388      * 
389      * @param Tinebase_Model_Tree_Node_Path $_path
390      * @return Tinebase_Model_Tree_Node
391      */
392     public function getFileNode(Tinebase_Model_Tree_Node_Path $_path)
393     {
394         $this->_checkPathACL($_path, 'get');
395         
396         if (! $this->_backend->fileExists($_path->statpath)) {
397             throw new Filemanager_Exception('File does not exist,');
398         }
399         
400         if (! $this->_backend->isFile($_path->statpath)) {
401             throw new Filemanager_Exception('Is a directory');
402         }
403         
404         return $this->_backend->stat($_path->statpath);
405     }
406     
407     /**
408      * add base path
409      * 
410      * @param Tinebase_Model_Tree_Node_PathFilter $_pathFilter
411      * @return string
412      */
413     public function addBasePath($_path)
414     {
415         $basePath = $this->_backend->getApplicationBasePath(Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName));
416         $basePath .= '/folders';
417         
418         $path = (strpos($_path, '/') === 0) ? $_path : '/' . $_path;
419         // only add base path once
420         $result = (! preg_match('@^' . preg_quote($basePath) . '@', $path)) ? $basePath . $path : $path;
421         
422         return $result;
423     }
424     
425     /**
426      * check if user has the permissions for the container
427      * 
428      * @param Tinebase_Model_Container $_container
429      * @param string $_action get|update|...
430      * @return boolean
431      */
432     protected function _checkACLContainer($_container, $_action = 'get')
433     {
434         if (Tinebase_Container::getInstance()->hasGrant(Tinebase_Core::getUser(), $_container, Tinebase_Model_Grants::GRANT_ADMIN)) {
435             return TRUE;
436         }
437         
438         switch ($_action) {
439             case 'get':
440                 $requiredGrant = Tinebase_Model_Grants::GRANT_READ;
441                 break;
442             case 'add':
443                 $requiredGrant = Tinebase_Model_Grants::GRANT_ADD;
444                 break;
445             case 'update':
446                 $requiredGrant = Tinebase_Model_Grants::GRANT_EDIT;
447                 break;
448             case 'delete':
449                 $requiredGrant = Tinebase_Model_Grants::GRANT_DELETE;
450                 break;
451             default:
452                 throw new Tinebase_Exception_UnexpectedValue('Unknown action: ' . $_action);
453         }
454         
455         return Tinebase_Container::getInstance()->hasGrant(Tinebase_Core::getUser(), $_container, $requiredGrant);
456     }
457     
458     /**
459      * Gets total count of search with $_filter
460      * 
461      * @param Tinebase_Model_Filter_FilterGroup $_filter
462      * @param string|optional $_action
463      * @return int
464      */
465     public function searchCount(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
466     {
467         $path = $this->_checkFilterACL($_filter, $_action);
468         
469         if($_filter->getFilter('recursive') && $_filter->getFilter('recursive')->getValue()) {
470             $result = $this->_recursiveSearchTotalCount;
471         } else if ($path->containerType === Tinebase_Model_Tree_Node_Path::TYPE_ROOT) {
472             $result = count($this->_getRootNodes());
473         } else if ($path->containerType === Tinebase_Model_Container::TYPE_PERSONAL && ! $path->containerOwner) {
474             $result = count($this->_getOtherUserNodes());
475         } else {
476             $result = $this->_backend->searchNodesCount($_filter);
477         }
478         
479         return $result;
480     }
481
482     /**
483      * create node(s)
484      * 
485      * @param array $_filenames
486      * @param string $_type directory or file
487      * @param array $_tempFileIds
488      * @param boolean $_forceOverwrite
489      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
490      */
491     public function createNodes($_filenames, $_type, $_tempFileIds = array(), $_forceOverwrite = FALSE)
492     {
493         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
494         $nodeExistsException = NULL;
495         
496         foreach ($_filenames as $idx => $filename) {
497             $tempFileId = (isset($_tempFileIds[$idx])) ? $_tempFileIds[$idx] : NULL;
498
499             try {
500                 $node = $this->_createNode($filename, $_type, $tempFileId, $_forceOverwrite);
501                 $result->addRecord($node);
502             } catch (Filemanager_Exception_NodeExists $fene) {
503                 $nodeExistsException = $this->_handleNodeExistsException($fene, $nodeExistsException);
504             }
505         }
506
507         if ($nodeExistsException) {
508             throw $nodeExistsException;
509         }
510         
511         return $result;
512     }
513     
514     /**
515      * collect information of a Filemanager_Exception_NodeExists in a "parent" exception
516      * 
517      * @param Filemanager_Exception_NodeExists $_fene
518      * @param Filemanager_Exception_NodeExists|NULL $_parentNodeExistsException
519      */
520     protected function _handleNodeExistsException($_fene, $_parentNodeExistsException = NULL)
521     {
522         // collect all nodes that already exist and add them to exception info
523         if (! $_parentNodeExistsException) {
524             $_parentNodeExistsException = new Filemanager_Exception_NodeExists();
525         }
526         
527         $nodesInfo = $_fene->getExistingNodesInfo();
528         if (count($nodesInfo) > 0) {
529             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
530                 . ' Adding node info to exception.');
531             $_parentNodeExistsException->addExistingNodeInfo($nodesInfo->getFirstRecord());
532         } else {
533             return $_fene;
534         }
535         
536         return $_parentNodeExistsException;
537     }
538     
539     /**
540      * create new node
541      * 
542      * @param string|Tinebase_Model_Tree_Node_Path $_path
543      * @param string $_type
544      * @param string $_tempFileId
545      * @param boolean $_forceOverwrite
546      * @return Tinebase_Model_Tree_Node
547      * @throws Tinebase_Exception_InvalidArgument
548      */
549     protected function _createNode($_path, $_type, $_tempFileId = NULL, $_forceOverwrite = FALSE)
550     {
551         if (! in_array($_type, array(Tinebase_Model_Tree_Node::TYPE_FILE, Tinebase_Model_Tree_Node::TYPE_FOLDER))) {
552             throw new Tinebase_Exception_InvalidArgument('Type ' . $_type . 'not supported.');
553         } 
554
555         $path = ($_path instanceof Tinebase_Model_Tree_Node_Path) 
556             ? $_path : Tinebase_Model_Tree_Node_Path::createFromPath($this->addBasePath($_path));
557         $parentPathRecord = $path->getParent();
558         
559         // we need to check the parent record existance before commencing node creation
560         $parentPathRecord->validateExistance();
561         
562         try {
563             $this->_checkIfExists($path);
564             $this->_checkPathACL($parentPathRecord, 'add');
565         } catch (Filemanager_Exception_NodeExists $fene) {
566             if ($_forceOverwrite) {
567                 $existingNode = $this->_backend->stat($path->statpath);
568                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
569                     . ' Existing node: ' . print_r($existingNode->toArray(), TRUE));
570                 
571                 if (! $_tempFileId) {
572                     // just return the exisiting node and do not overwrite existing file if no tempfile id was given
573                     $this->_checkPathACL($path, 'get');
574                     $this->resolveContainerAndAddPath($existingNode, $parentPathRecord);
575                     return $existingNode;
576                 } else {
577                     // check if a new (size 0) file is overwritten
578                     // @todo check revision here?
579                     if ($existingNode->size == 0) {
580                         $this->_checkPathACL($parentPathRecord, 'add');
581                     } else {
582                         $this->_checkPathACL($parentPathRecord, 'update');
583                     }
584                 }
585             } else if (! $_forceOverwrite) {
586                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
587                     . ' ' . $fene);
588                 throw $fene;
589             }
590         }
591
592         if (! $parentPathRecord->container && $_type === Tinebase_Model_Tree_Node::TYPE_FOLDER) {
593             $container = $this->_createContainer($path->name, $parentPathRecord->containerType);
594             $newNodePath = $parentPathRecord->statpath . '/' . $container->getId();
595         } else {
596             $container = NULL;
597             $newNodePath = $parentPathRecord->statpath . '/' . $path->name;
598         }
599         
600         $newNode = $this->_createNodeInBackend($newNodePath, $_type, $_tempFileId);
601         $this->resolveContainerAndAddPath($newNode, $parentPathRecord, $container);
602         return $newNode;
603     }
604     
605     /**
606      * create node in backend
607      * 
608      * @param string $_statpath
609      * @param type
610      * @param string $_tempFileId
611      * @return Tinebase_Model_Tree_Node
612      */
613     protected function _createNodeInBackend($_statpath, $_type, $_tempFileId = NULL)
614     {
615         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . 
616             ' Creating new path ' . $_statpath . ' of type ' . $_type);
617         
618         switch ($_type) {
619             case Tinebase_Model_Tree_Node::TYPE_FILE:
620                 $this->_backend->copyTempfile($_tempFileId, $_statpath);
621                 break;
622             case Tinebase_Model_Tree_Node::TYPE_FOLDER:
623                 $this->_backend->mkdir($_statpath);
624                 break;
625         }
626         
627         return $this->_backend->stat($_statpath);
628     }
629     
630     /**
631      * check file existance
632      * 
633      * @param Tinebase_Model_Tree_Node_Path $_path
634      * @param Tinebase_Model_Tree_Node $_node
635      * @throws Filemanager_Exception_NodeExists
636      */
637     protected function _checkIfExists(Tinebase_Model_Tree_Node_Path $_path, $_node = NULL)
638     {
639         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
640             . ' Check existance of ' . $_path->statpath);
641         
642         if ($this->_backend->fileExists($_path->statpath)) {
643             $existsException = new Filemanager_Exception_NodeExists();
644             $existsException->addExistingNodeInfo(($_node !== NULL) ? $_node : $this->_backend->stat($_path->statpath));
645             throw $existsException;
646         }
647     }
648         
649     /**
650      * check acl of path
651      * 
652      * @param Tinebase_Model_Tree_Node_Path $_path
653      * @param string $_action
654      * @param boolean $_topLevelAllowed
655      * @throws Tinebase_Exception_AccessDenied
656      */
657     protected function _checkPathACL(Tinebase_Model_Tree_Node_Path $_path, $_action = 'get', $_topLevelAllowed = TRUE)
658     {
659         $hasPermission = FALSE;
660         
661         if ($_path->container) {
662             $hasPermission = $this->_checkACLContainer($_path->container, $_action);
663         } else if ($_topLevelAllowed) {
664             switch ($_path->containerType) {
665                 case Tinebase_Model_Container::TYPE_PERSONAL:
666                     if ($_path->containerOwner) {
667                         $hasPermission = ($_path->containerOwner === Tinebase_Core::getUser()->accountLoginName || $_action === 'get');
668                     } else {
669                         $hasPermission = ($_action === 'get');
670                     }
671                     break;
672                 case Tinebase_Model_Container::TYPE_SHARED:
673                     $hasPermission = ($_action !== 'get') ? $this->checkRight(Tinebase_Acl_Rights::MANAGE_SHARED_FOLDERS, FALSE) : TRUE;
674                     break;
675                 case Tinebase_Model_Tree_Node_Path::TYPE_ROOT:
676                     $hasPermission = ($_action === 'get');
677                     break;
678             }
679         }
680         
681         if (! $hasPermission) {
682             throw new Tinebase_Exception_AccessDenied('No permission to ' . $_action . ' nodes in path ' . $_path->flatpath);
683         }
684     }
685     
686     /**
687      * create new container
688      * 
689      * @param string $_name
690      * @param string $_type
691      * @return Tinebase_Model_Container
692      * @throws Tinebase_Exception_Record_NotAllowed
693      */
694     protected function _createContainer($_name, $_type)
695     {
696         $ownerId = ($_type === Tinebase_Model_Container::TYPE_PERSONAL) ? Tinebase_Core::getUser()->getId() : NULL;
697         try {
698             $existingContainer = Tinebase_Container::getInstance()->getContainerByName(
699                 $this->_applicationName, $_name, $_type, $ownerId);
700             throw new Filemanager_Exception_NodeExists('Container ' . $_name . ' of type ' . $_type . ' already exists.');
701         } catch (Tinebase_Exception_NotFound $tenf) {
702             // go on
703         }
704         
705         $app = Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName);
706         $container = Tinebase_Container::getInstance()->addContainer(new Tinebase_Model_Container(array(
707             'name'           => $_name,
708             'type'           => $_type,
709             'backend'        => 'sql',
710             'application_id' => $app->getId(),
711             'model'          => 'Filemanager_Model_Node'
712         )));
713         
714         return $container;
715     }
716
717     /**
718      * resolve node container and path
719      * 
720      * (1) add path to records 
721      * (2) replace name with container record, if node name is a container id 
722      *     / path is toplevel (shared/personal with useraccount
723      * (3) add account grants of acl container to node
724      * 
725      * @param Tinebase_Record_RecordSet|Tinebase_Model_Tree_Node $_records
726      * @param Tinebase_Model_Tree_Node_Path $_path
727      * @param Tinebase_Model_Container $_container
728      */
729     public function resolveContainerAndAddPath($_records, Tinebase_Model_Tree_Node_Path $_path, Tinebase_Model_Container $_container = NULL)
730     {
731         $records = ($_records instanceof Tinebase_Model_Tree_Node) 
732             ? new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array($_records)) : $_records;
733         
734         if (! $_path->container) {
735             // fetch top level container nodes
736             if ($_container === NULL) {
737                 $containerIds = $_records->name;
738                 $containers = Tinebase_Container::getInstance()->getMultiple($containerIds);
739             } else {
740                 $containers = new Tinebase_Record_RecordSet('Tinebase_Model_Container', array($_container));
741             }
742         }
743         
744         $app = Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName);
745         $flatpathWithoutBasepath = Tinebase_Model_Tree_Node_Path::removeAppIdFromPath($_path->flatpath, $app);
746         
747         foreach ($records as $record) {
748             $record->path = $flatpathWithoutBasepath . '/' . $record->name;
749             
750             $aclContainer = NULL;
751             if (! $_path->container) {
752                 // resolve container
753                 if (! $record->name instanceof Tinebase_Model_Container) {
754                     $idx = $containers->getIndexById($record->name);
755                     if ($idx !== FALSE) {
756                         $aclContainer = $containers[$idx];
757                         $record->name = $aclContainer;
758                         $record->path = $flatpathWithoutBasepath . '/' . $record->name->name;
759                     }
760                 }
761             } else {
762                 $aclContainer = $_path->container;
763             }
764             
765             if ($aclContainer) {
766                 $record->account_grants = Tinebase_Container::getInstance()->getGrantsOfAccount(
767                     Tinebase_Core::getUser(), 
768                     $aclContainer
769                 )->toArray();
770                 $aclContainer->account_grants = $record->account_grants;
771                 
772                 // needed for sorting
773                 $record->container_name = $aclContainer->name;
774             }
775         }
776     }
777     
778     /**
779      * copy nodes
780      * 
781      * @param array $_sourceFilenames array->multiple
782      * @param string|array $_destinationFilenames string->singlefile OR directory, array->multiple files
783      * @param boolean $_forceOverwrite
784      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
785      */
786     public function copyNodes($_sourceFilenames, $_destinationFilenames, $_forceOverwrite = FALSE)
787     {
788         return $this->_copyOrMoveNodes($_sourceFilenames, $_destinationFilenames, 'copy', $_forceOverwrite);
789     }
790     
791     /**
792      * copy or move an array of nodes identified by their path
793      * 
794      * @param array $_sourceFilenames array->multiple
795      * @param string|array $_destinationFilenames string->singlefile OR directory, array->multiple files
796      * @param string $_action copy|move
797      * @param boolean $_forceOverwrite
798      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
799      */
800     protected function _copyOrMoveNodes($_sourceFilenames, $_destinationFilenames, $_action, $_forceOverwrite = FALSE)
801     {
802         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
803         $nodeExistsException = NULL;
804         
805         foreach ($_sourceFilenames as $idx => $source) {
806             $sourcePathRecord = Tinebase_Model_Tree_Node_Path::createFromPath($this->addBasePath($source));
807             $destinationPathRecord = $this->_getDestinationPath($_destinationFilenames, $idx, $sourcePathRecord);
808
809             try {
810                 if ($_action === 'move') {
811                     $node = $this->_moveNode($sourcePathRecord, $destinationPathRecord, $_forceOverwrite);
812                 } else if ($_action === 'copy') {
813                     $node = $this->_copyNode($sourcePathRecord, $destinationPathRecord, $_forceOverwrite);
814                 }
815                 $result->addRecord($node);
816             } catch (Filemanager_Exception_NodeExists $fene) {
817                 $nodeExistsException = $this->_handleNodeExistsException($fene, $nodeExistsException);
818             }
819         }
820         
821         $this->resolveContainerAndAddPath($result, $destinationPathRecord->getParent());
822         
823         if ($nodeExistsException) {
824             // @todo add correctly moved/copied files here?
825             throw $nodeExistsException;
826         }
827         
828         return $result;
829     }
830     
831     /**
832      * get single destination from an array of destinations and an index + $_sourcePathRecord
833      * 
834      * @param string|array $_destinationFilenames
835      * @param int $_idx
836      * @param Tinebase_Model_Tree_Node_Path $_sourcePathRecord
837      * @return Tinebase_Model_Tree_Node_Path
838      * @throws Filemanager_Exception
839      * 
840      * @todo add Tinebase_FileSystem::isDir() check?
841      */
842     protected function _getDestinationPath($_destinationFilenames, $_idx, $_sourcePathRecord)
843     {
844         if (is_array($_destinationFilenames)) {
845             $isdir = FALSE;
846             if (isset($_destinationFilenames[$_idx])) {
847                 $destination = $_destinationFilenames[$_idx];
848             } else {
849                 throw new Filemanager_Exception('No destination path found.');
850             }
851         } else {
852             $isdir = TRUE;
853             $destination = $_destinationFilenames;
854         }
855         
856         if ($isdir) {
857             $destination = $destination . '/' . $_sourcePathRecord->name;
858         }
859         
860         return Tinebase_Model_Tree_Node_Path::createFromPath($this->addBasePath($destination));
861     }
862     
863     /**
864      * copy single node
865      * 
866      * @param Tinebase_Model_Tree_Node_Path $_source
867      * @param Tinebase_Model_Tree_Node_Path $_destination
868      * @param boolean $_forceOverwrite
869      * @return Tinebase_Model_Tree_Node
870      */
871     protected function _copyNode(Tinebase_Model_Tree_Node_Path $_source, Tinebase_Model_Tree_Node_Path $_destination, $_forceOverwrite = FALSE)
872     {
873         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
874             . ' Copy Node ' . $_source->flatpath . ' to ' . $_destination->flatpath);
875                 
876         $newNode = NULL;
877         
878         $this->_checkPathACL($_source, 'get', FALSE);
879         
880         $sourceNode = $this->_backend->stat($_source->statpath);
881         
882         switch ($sourceNode->type) {
883             case Tinebase_Model_Tree_Node::TYPE_FILE:
884                 $newNode = $this->_copyOrMoveFileNode($_source, $_destination, 'copy', $_forceOverwrite);
885                 break;
886             case Tinebase_Model_Tree_Node::TYPE_FOLDER:
887                 $newNode = $this->_copyFolderNode($_source, $_destination);
888                 break;
889         }
890         
891         return $newNode;
892     }
893     
894     /**
895      * copy file node
896      * 
897      * @param Tinebase_Model_Tree_Node_Path $_source
898      * @param Tinebase_Model_Tree_Node_Path $_destination
899      * @param string $_action
900      * @param boolean $_forceOverwrite
901      * @return Tinebase_Model_Tree_Node
902      */
903     protected function _copyOrMoveFileNode(Tinebase_Model_Tree_Node_Path $_source, Tinebase_Model_Tree_Node_Path $_destination, $_action, $_forceOverwrite = FALSE)
904     {
905         $this->_checkPathACL($_destination->getParent(), 'update', FALSE);
906         
907         try {
908             $this->_checkIfExists($_destination);
909         } catch (Filemanager_Exception_NodeExists $fene) {
910             if ($_forceOverwrite && $_source->statpath !== $_destination->statpath) {
911                 // delete old node
912                 $this->_backend->unlink($_destination->statpath);
913             } elseif (! $_forceOverwrite) {
914                 throw $fene;
915             }
916         }
917         
918         switch ($_action) {
919             case 'copy':
920                 $newNode = $this->_backend->copy($_source->statpath, $_destination->statpath);
921                 break;
922             case 'move':
923                 $newNode = $this->_backend->rename($_source->statpath, $_destination->statpath);
924                 break;
925         }
926
927         return $newNode;
928     }
929     
930     /**
931      * copy folder node
932      * 
933      * @param Tinebase_Model_Tree_Node_Path $_source
934      * @param Tinebase_Model_Tree_Node_Path $_destination
935      * @return Tinebase_Model_Tree_Node
936      * @throws Filemanager_Exception_NodeExists
937      * 
938      * @todo add $_forceOverwrite?
939      */
940     protected function _copyFolderNode(Tinebase_Model_Tree_Node_Path $_source, Tinebase_Model_Tree_Node_Path $_destination)
941     {
942         $newNode = $this->_createNode($_destination, Tinebase_Model_Tree_Node::TYPE_FOLDER);
943         
944         // recursive copy for (sub-)folders/files
945         $filter = new Tinebase_Model_Tree_Node_Filter(array(array(
946             'field'    => 'path', 
947             'operator' => 'equals', 
948             'value'    => Tinebase_Model_Tree_Node_Path::removeAppIdFromPath(
949                 $_source->flatpath, 
950                 Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName)
951             ),
952         )));
953         $result = $this->search($filter);
954         if (count($result) > 0) {
955             $this->copyNodes($result->path, $newNode->path);
956         }
957         
958         return $newNode;
959     }
960     
961     /**
962      * move nodes
963      * 
964      * @param array $_sourceFilenames array->multiple
965      * @param string|array $_destinationFilenames string->singlefile OR directory, array->multiple files
966      * @param boolean $_forceOverwrite
967      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
968      */
969     public function moveNodes($_sourceFilenames, $_destinationFilenames, $_forceOverwrite = FALSE)
970     {
971         return $this->_copyOrMoveNodes($_sourceFilenames, $_destinationFilenames, 'move', $_forceOverwrite);
972     }
973     
974     /**
975      * move single node
976      * 
977      * @param Tinebase_Model_Tree_Node_Path $_source
978      * @param Tinebase_Model_Tree_Node_Path $_destination
979      * @param boolean $_forceOverwrite
980      * @return Tinebase_Model_Tree_Node
981      */
982     protected function _moveNode(Tinebase_Model_Tree_Node_Path $_source, Tinebase_Model_Tree_Node_Path $_destination, $_forceOverwrite = FALSE)
983     {
984         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
985             . ' Move Node ' . $_source->flatpath . ' to ' . $_destination->flatpath);
986         
987         $sourceNode = $this->_backend->stat($_source->statpath);
988         
989         switch ($sourceNode->type) {
990             case Tinebase_Model_Tree_Node::TYPE_FILE:
991                 $movedNode = $this->_copyOrMoveFileNode($_source, $_destination, 'move', $_forceOverwrite);
992                 break;
993             case Tinebase_Model_Tree_Node::TYPE_FOLDER:
994                 $movedNode = $this->_moveFolderNode($_source, $sourceNode, $_destination, $_forceOverwrite);
995                 break;
996         }
997         
998         return $movedNode;
999     }
1000     
1001     /**
1002      * move folder node
1003      * 
1004      * @param Tinebase_Model_Tree_Node_Path $source
1005      * @param Tinebase_Model_Tree_Node $sourceNode [unused]
1006      * @param Tinebase_Model_Tree_Node_Path $destination
1007      * @param boolean $_forceOverwrite
1008      * @return Tinebase_Model_Tree_Node
1009      */
1010     protected function _moveFolderNode($source, $sourceNode, $destination, $_forceOverwrite = FALSE)
1011     {
1012         $this->_checkPathACL($source, 'get', FALSE);
1013         
1014         $destinationParentPathRecord = $destination->getParent();
1015         $destinationNodeName = NULL;
1016         
1017         if ($destination->isToplevelPath()) {
1018             $this->_moveFolderContainer($source, $destination, $_forceOverwrite);
1019             $destinationNodeName = $destination->container->getId();
1020         } else {
1021             $this->_checkPathACL($destinationParentPathRecord, 'update');
1022             
1023             if ($source->getParent()->flatpath != $destinationParentPathRecord->flatpath) {
1024                 try {
1025                     $this->_checkIfExists($destination);
1026                 } catch (Filemanager_Exception_NodeExists $fene) {
1027                     if ($_forceOverwrite && $source->statpath !== $destination->statpath) {
1028                         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1029                             . ' Removing folder node ' . $destination->statpath);
1030                         $this->_backend->rmdir($destination->statpath, TRUE);
1031                     } else if (! $_forceOverwrite) {
1032                         throw $fene;
1033                     }
1034                 }
1035             }
1036         }
1037         
1038         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1039             . ' Rename Folder ' . $source->statpath . ' -> ' . $destination->statpath);
1040         
1041         $this->_backend->rename($source->statpath, $destination->statpath);
1042         
1043         $movedNode = $this->_backend->stat($destination->statpath);
1044         if ($destinationNodeName !== NULL) {
1045             $movedNode->name = $destinationNodeName;
1046         }
1047         
1048         return $movedNode;
1049     }
1050     
1051     /**
1052      * move folder container
1053      * 
1054      * @param Tinebase_Model_Tree_Node_Path $source
1055      * @param Tinebase_Model_Tree_Node_Path $destination
1056      * @param boolean $forceOverwrite
1057      * @return Tinebase_Model_Tree_Node
1058      */
1059     protected function _moveFolderContainer($source, $destination, $forceOverwrite = FALSE)
1060     {
1061         if ($source->isToplevelPath()) {
1062             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1063                 . ' Moving container ' . $source->container->name . ' to ' . $destination->flatpath);
1064         
1065             $this->_checkACLContainer($source->container, 'update');
1066         
1067             $container = $source->container;
1068             if ($container->name !== $destination->name) {
1069                 try {
1070                     $existingContainer = Tinebase_Container::getInstance()->getContainerByName(
1071                         $this->_applicationName,
1072                         $destination->name,
1073                         $destination->containerType,
1074                         Tinebase_Core::getUser()
1075                     );
1076                     if (! $forceOverwrite) {
1077                         $fene = new Filemanager_Exception_NodeExists('container exists');
1078                         $fene->addExistingNodeInfo($this->_backend->stat($destination->statpath));
1079                         throw $fene;
1080                     } else {
1081                         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1082                             . ' Removing existing folder node and container ' . $destination->flatpath);
1083                         $this->_backend->rmdir($destination->statpath, TRUE);
1084                     }
1085                 } catch (Tinebase_Exception_NotFound $tenf) {
1086                     // ok
1087                 }
1088                 
1089                 $container->name = $destination->name;
1090                 $container = Tinebase_Container::getInstance()->update($container);
1091             }
1092         } else {
1093             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1094                 . ' Creating container ' . $destination->name);
1095             $container = $this->_createContainer($destination->name, $destination->containerType);
1096         }
1097         
1098         $destination->setContainer($container);
1099     }
1100     
1101     /**
1102      * delete nodes
1103      * 
1104      * @param array $_filenames string->single file, array->multiple
1105      * @return int delete count
1106      * 
1107      * @todo add recursive param?
1108      */
1109     public function deleteNodes($_filenames)
1110     {
1111         $deleteCount = 0;
1112         foreach ($_filenames as $filename) {
1113             if ($this->_deleteNode($filename)) {
1114                 $deleteCount++;
1115             }
1116         }
1117         
1118         return $deleteCount;
1119     }
1120
1121     /**
1122      * delete node
1123      * 
1124      * @param string $_flatpath
1125      * @return boolean
1126      * @throws Tinebase_Exception_NotFound
1127      */
1128     protected function _deleteNode($_flatpath)
1129     {
1130         $flatpathWithBasepath = $this->addBasePath($_flatpath);
1131         list($parentPathRecord, $nodeName) = Tinebase_Model_Tree_Node_Path::getParentAndChild($flatpathWithBasepath);
1132         $pathRecord = Tinebase_Model_Tree_Node_Path::createFromPath($flatpathWithBasepath);
1133         
1134         $this->_checkPathACL($parentPathRecord, 'delete');
1135         
1136         if (! $parentPathRecord->container) {
1137             // check acl for deleting toplevel container
1138             $this->_checkPathACL($pathRecord, 'delete');
1139         }
1140         
1141         $success = $this->_deleteNodeInBackend($pathRecord);
1142         
1143         if ($success && ! $parentPathRecord->container) {
1144             
1145             if (! is_object($pathRecord->container)) {
1146                 throw new Tinebase_Exception_NotFound('Container not found');
1147             }
1148             
1149             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
1150                 . ' Delete container ' . $pathRecord->container->name);
1151             Tinebase_Container::getInstance()->delete($pathRecord->container->getId());
1152         }
1153         
1154         return $success;
1155     }
1156     
1157     /**
1158      * delete node in backend
1159      * 
1160      * @param Tinebase_Model_Tree_Node_Path $_path
1161      * @return boolean
1162      */
1163     protected function _deleteNodeInBackend(Tinebase_Model_Tree_Node_Path $_path)
1164     {
1165         $success = FALSE;
1166         
1167         $node = $this->_backend->stat($_path->statpath);
1168         
1169         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . 
1170             ' Removing path ' . $_path->flatpath . ' of type ' . $node->type);
1171         
1172         switch ($node->type) {
1173             case Tinebase_Model_Tree_Node::TYPE_FILE:
1174                 $success = $this->_backend->unlink($_path->statpath);
1175                 break;
1176             case Tinebase_Model_Tree_Node::TYPE_FOLDER:
1177                 $success = $this->_backend->rmdir($_path->statpath, TRUE);
1178                 break;
1179         }
1180         
1181         return $success;
1182     }
1183     
1184     /**
1185      * Deletes a set of records.
1186      *
1187      * If one of the records could not be deleted, no record is deleted
1188      *
1189      * @param   array array of record identifiers
1190      * @return  Tinebase_Record_RecordSet
1191      */
1192     public function delete($_ids)
1193     {
1194         $nodes = $this->getMultiple($_ids);
1195         foreach ($nodes as $node) {
1196             if ($this->_checkACLContainer($this->_backend->getNodeContainer($node->getId()), 'delete')) {
1197                 $this->_backend->deleteFileNode($node);
1198             } else {
1199                 $nodes->removeRecord($node);
1200             }
1201         }
1202         
1203         return $nodes;
1204     }
1205 }