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