edb3e7c8bfcce03704884afe4318fd403245a837
[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-2017 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      *
45      * attention, this NEEDS to be off / true for replication!
46      *
47      * @var boolean
48      */
49     protected $_omitModLog = true;
50     
51     /**
52      * holds the total count of the last recursive search
53      * @var integer
54      */
55     protected $_recursiveSearchTotalCount = 0;
56
57     /**
58      * recursion check for create modlog inside copy / move
59      *
60      * @var bool
61      */
62     protected $_inCopyOrMoveNode = false;
63     
64     /**
65      * holds the instance of the singleton
66      *
67      * @var Filemanager_Controller_Node
68      */
69     private static $_instance = NULL;
70     
71     /**
72      * the constructor
73      *
74      * don't use the constructor. use the singleton
75      */
76     private function __construct() 
77     {
78         $this->_resolveCustomFields = true;
79         $this->_backend = Tinebase_FileSystem::getInstance();
80     }
81     
82     /**
83      * don't clone. Use the singleton.
84      *
85      */
86     private function __clone() 
87     {
88     }
89     
90     /**
91      * the singleton pattern
92      *
93      * @return Filemanager_Controller_Node
94      */
95     public static function getInstance() 
96     {
97         if (self::$_instance === NULL) {
98             self::$_instance = new Filemanager_Controller_Node();
99         }
100         
101         return self::$_instance;
102     }
103     
104     /**
105      * (non-PHPdoc)
106      * @see Tinebase_Controller_Record_Abstract::update()
107      */
108     public function update(Tinebase_Record_Interface $_record)
109     {
110         if (! $this->_backend->checkACLNode($_record, 'update')) {
111             if (! $this->_backend->checkACLNode($_record, 'get')) {
112                 throw new Tinebase_Exception_AccessDenied('No permission to update nodes.');
113             }
114             // we allow only notification updates for the current user itself
115             $usersNotificationSettings = null;
116             $currentUserId = Tinebase_Core::getUser()->getId();
117             foreach ($_record->xprops(Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION) as $xpNotification) {
118                 if (isset($xpNotification[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_ID]) &&
119                         isset($xpNotification[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_TYPE]) &&
120                         Tinebase_Acl_Rights::ACCOUNT_TYPE_USER === $xpNotification[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_TYPE] &&
121                         $currentUserId ===  $xpNotification[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_ID]) {
122                     $usersNotificationSettings = $xpNotification;
123                     break;
124                 }
125             }
126
127             // we reset all input and then just apply the notification settings for the current user
128             $_record = $this->get($_record->getId());
129             $found = false;
130             foreach ($_record->xprops(Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION) as $key => &$xpNotification) {
131                 if (isset($xpNotification[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_ID]) &&
132                         isset($xpNotification[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_TYPE]) &&
133                         Tinebase_Acl_Rights::ACCOUNT_TYPE_USER === $xpNotification[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_TYPE] &&
134                         $currentUserId ===  $xpNotification[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_ID]) {
135                     if (null !== $usersNotificationSettings) {
136                         $xpNotification = $usersNotificationSettings;
137                     } else {
138                         unset($_record->xprops(Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION)[$key]);
139                     }
140                     $found = true;
141                     break;
142                 }
143             }
144             if (false === $found && null !== $usersNotificationSettings) {
145                 $_record->xprops(Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION)[] = $usersNotificationSettings;
146             }
147
148             if (false === $found && null === $usersNotificationSettings){
149                 throw new Tinebase_Exception_AccessDenied('No permission to update nodes.');
150             }
151         }
152
153         return parent::update($_record);
154     }
155     
156     /**
157      * inspect update of one record (before update)
158      *
159      * @param   Filemanager_Model_Node $_record      the update record
160      * @param   Tinebase_Record_Interface $_oldRecord   the current persistent record
161      * @return  void
162      */
163     protected function _inspectBeforeUpdate($_record, $_oldRecord)
164     {
165         // protect against file object spoofing
166         foreach (array_keys($_record->toArray()) as $property) {
167             if (! in_array($property, array('name', 'description', 'relations', 'customfields', 'tags', 'notes', 'acl_node', 'grants', Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION, Tinebase_Model_Tree_Node::XPROPS_REVISION))) {
168                 $_record->{$property} = $_oldRecord->{$property};
169             }
170         }
171
172         if (!Tinebase_Core::getUser()->hasGrant($_record, Tinebase_Model_Grants::GRANT_ADMIN, 'Tinebase_Model_Tree_Node')) {
173             $_record->{Tinebase_Model_Tree_Node::XPROPS_REVISION} = $_oldRecord->{Tinebase_Model_Tree_Node::XPROPS_REVISION};
174         }
175
176         $nodePath = null;
177         if (Tinebase_Model_Tree_FileObject::TYPE_FOLDER === $_record->type) {
178             $nodePath = Tinebase_Model_Tree_Node_Path::createFromStatPath($this->_backend->getPathOfNode($_record->getId(), true));
179             $modlogNode = new Filemanager_Model_Node(array(
180                 'id' => $_record->getId(),
181                 'path' => $nodePath->statpath,
182                 'type' => Tinebase_Model_Tree_FileObject::TYPE_FOLDER,
183                 Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION => $_record->xprops(Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION),
184                 // do not set acl_node, this will be calculated on the client side
185             ), true);
186             if (isset($_record->xprops(Tinebase_Model_Tree_Node::XPROPS_REVISION)[Tinebase_Model_Tree_Node::XPROPS_REVISION_NODE_ID]) &&
187                     $_record->xprops(Tinebase_Model_Tree_Node::XPROPS_REVISION)[Tinebase_Model_Tree_Node::XPROPS_REVISION_NODE_ID] === $_record->getId() &&
188                     $_record->xprops(Tinebase_Model_Tree_Node::XPROPS_REVISION) != $_oldRecord->xprops(Tinebase_Model_Tree_Node::XPROPS_REVISION)) {
189                 $revisions = $_record->xprops(Tinebase_Model_Tree_Node::XPROPS_REVISION);
190                 unset($revisions[Tinebase_Model_Tree_Node::XPROPS_REVISION_NODE_ID]);
191                 $modlogNode->{Tinebase_Model_Tree_Node::XPROPS_REVISION} = $revisions;
192             }
193             $modlogOldNode = new Filemanager_Model_Node(array(
194                 'id' => $_record->getId(),
195                 'path' => $nodePath->statpath,
196                 'type' => Tinebase_Model_Tree_FileObject::TYPE_FOLDER,
197                 Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION => $_oldRecord->xprops(Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION),
198             ), true);
199             $this->_omitModLog = false;
200             $this->_writeModLog($modlogNode, $modlogOldNode);
201             $this->_omitModLog = true;
202         }
203
204         // update node acl
205         $aclNode = $_oldRecord->acl_node;
206         if (Tinebase_Model_Tree_FileObject::TYPE_FOLDER === $_record->type
207             && Tinebase_Core::getUser()->hasGrant($_record, Tinebase_Model_Grants::GRANT_ADMIN, 'Tinebase_Model_Tree_Node')
208         ) {
209             if (! $nodePath->isSystemPath()) {
210
211                 $modlogOldNode = $modlogNode = null;
212                 if ($_record->acl_node === null && ! $nodePath->isToplevelPath()) {
213                     // acl_node === null -> remove acl
214                     $node = $this->_backend->setAclFromParent($nodePath->statpath);
215                     $aclNode = $node->acl_node;
216
217                     $modlogNode = new Filemanager_Model_Node(array(
218                         'id' => $_record->getId(),
219                         'path' => $nodePath->statpath,
220                         'type' => Tinebase_Model_Tree_FileObject::TYPE_FOLDER,
221                         'grants' => 'unset'
222                         // do not set acl_node, this will be calculated on the client side
223                     ), true);
224                     $modlogOldNode = new Filemanager_Model_Node(array(
225                         'id' => $_record->getId(),
226                         'type' => Tinebase_Model_Tree_FileObject::TYPE_FOLDER
227                     ), true);
228
229                 } elseif ($_record->acl_node === $_record->getId() && isset($_record->grants)) {
230                     $oldGrants = Tinebase_Tree_NodeGrants::getInstance()->getGrantsForRecord($_oldRecord);
231                     if (is_array($_record->grants)) {
232                         $_record->grants = new Tinebase_Record_RecordSet('Tinebase_Model_Grants', $_record->grants);
233                     }
234                     $diff = $_record->grants->diff($oldGrants);
235                     if (!$diff->isEmpty() || $_oldRecord->acl_node !== $_record->acl_node) {
236                         $this->_backend->setGrantsForNode($_record, $_record->grants);
237                         $modlogNode = new Filemanager_Model_Node(array(
238                             'id' => $_record->getId(),
239                             'path' => $nodePath->statpath,
240                             'type' => Tinebase_Model_Tree_FileObject::TYPE_FOLDER,
241                             'grants' => $_record->grants
242                             // do not set acl_node, this will be calculated on the client side
243                         ), true);
244                         $modlogOldNode = new Filemanager_Model_Node(array(
245                             'id' => $_record->getId(),
246                             'type' => Tinebase_Model_Tree_FileObject::TYPE_FOLDER
247                         ), true);
248                     }
249                     $aclNode = $_record->acl_node;
250                 }
251
252                 if (null !== $modlogNode) {
253                     $this->_omitModLog = false;
254                     $this->_writeModLog($modlogNode, $modlogOldNode);
255                     $this->_omitModLog = true;
256                 }
257             }
258         }
259         // reset node acl value to prevent spoofing
260         $_record->acl_node = $aclNode;
261     }
262     
263     /**
264      * (non-PHPdoc)
265      * @see Tinebase_Controller_Record_Abstract::getMultiple()
266      * 
267      * @return  Tinebase_Record_RecordSet
268      */
269     public function getMultiple($_ids)
270     {
271         $results = $this->_backend->getMultipleTreeNodes($_ids);
272         $this->resolveMultipleTreeNodesPath($results);
273         
274         return $results;
275     }
276     
277     /**
278      * Resolve path of multiple tree nodes
279      * 
280      * @param Tinebase_Record_RecordSet|Tinebase_Model_Tree_Node $_records
281      */
282     public function resolveMultipleTreeNodesPath($_records)
283     {
284         $records = ($_records instanceof Tinebase_Model_Tree_Node)
285             ? new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array($_records)) : $_records;
286             
287         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
288             . ' Resolving paths for ' . count($records) .  ' records.');
289             
290         foreach ($records as $record) {
291             $path = $this->_backend->getPathOfNode($record, TRUE);
292             $record->path = Tinebase_Model_Tree_Node_Path::removeAppIdFromPath($path, $this->_applicationName);
293
294             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
295                 . ' Got path ' . $record->path .  ' for node ' . $record->name);
296         }
297     }
298     
299     /**
300      * (non-PHPdoc)
301      * @see Tinebase_Controller_Record_Abstract::get()
302      */
303     public function get($_id, $_containerId = NULL)
304     {
305         $record = parent::get($_id);
306
307         if (! $this->_backend->checkACLNode($record, 'get')) {
308             throw new Tinebase_Exception_AccessDenied('No permission to get node');
309         }
310
311         if ($record) {
312             $record->notes = Tinebase_Notes::getInstance()->getNotesOfRecord('Tinebase_Model_Tree_Node', $record->getId());
313         }
314
315         $nodePath = Tinebase_Model_Tree_Node_Path::createFromStatPath($this->_backend->getPathOfNode($record, true));
316         $record->path = Tinebase_Model_Tree_Node_Path::removeAppIdFromPath($nodePath->flatpath, $this->_applicationName);
317         $this->resolveGrants($record);
318
319         return $record;
320     }
321     
322     /**
323      * search tree nodes
324      * 
325      * @param Tinebase_Model_Filter_FilterGroup $_filter
326      * @param Tinebase_Model_Pagination $_pagination
327      * @param bool $_getRelations
328      * @param bool $_onlyIds
329      * @param string|optional $_action
330      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
331      */
332     public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Model_Pagination $_pagination = NULL, $_getRelations = FALSE, $_onlyIds = FALSE, $_action = 'get')
333     {
334         // perform recursive search on recursive filter set
335         if ($_filter->getFilter('recursive')) {
336             return $this->_searchNodesRecursive($_filter, $_pagination);
337         } else {
338             $path = $this->_checkFilterACL($_filter, $_action);
339         }
340         
341         if ($path->containerType === Tinebase_Model_Tree_Node_Path::TYPE_ROOT) {
342             $result = $this->_getRootNodes();
343         } else if ($path->containerType === Tinebase_FileSystem::FOLDER_TYPE_PERSONAL && ! $path->containerOwner) {
344             if (! file_exists($path->statpath)) {
345                 $this->_backend->mkdir($path->statpath);
346             }
347             $result = $this->_getOtherUserNodes();
348             $this->resolvePath($result, $path);
349         } else {
350             try {
351                 $result = $this->_backend->searchNodes($_filter, $_pagination);
352             } catch (Tinebase_Exception_NotFound $tenf) {
353                 // create basic nodes like personal|shared|user root
354                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . 
355                         ' ' . $path->statpath);
356                 if ($path->name === Tinebase_FileSystem::FOLDER_TYPE_SHARED ||
357                     $path->statpath === $this->_backend->getApplicationBasePath(
358                         Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName), 
359                         Tinebase_FileSystem::FOLDER_TYPE_PERSONAL
360                     ) . '/' . Tinebase_Core::getUser()->getId()
361                 ) {
362                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . 
363                         ' Creating new path ' . $path->statpath);
364                     $this->_backend->mkdir($path->statpath);
365                     $result = $this->_backend->searchNodes($_filter, $_pagination);
366                 } else {
367                     throw $tenf;
368                 }
369             }
370             $this->resolvePath($result, $path);
371             // TODO still needed?
372             //$this->_sortContainerNodes($result, $path, $_pagination);
373         }
374         $this->resolveGrants($result);
375         return $result;
376     }
377     
378     /**
379      * search tree nodes for search combo
380      * 
381      * @param Tinebase_Model_Tree_Node_Filter $_filter
382      * @param Tinebase_Record_Interface $_pagination
383      * 
384      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
385      */
386     
387     protected function _searchNodesRecursive($_filter, $_pagination)
388     {
389         $_filter->removeFilter('path');
390         $_filter->removeFilter('recursive');
391         $_filter->removeFilter('type');
392         $_filter->addFilter($_filter->createFilter('type', 'equals', Tinebase_Model_Tree_FileObject::TYPE_FILE));
393
394         $result = $this->_backend->searchNodes($_filter, $_pagination);
395
396         $_filter->addFilter($_filter->createFilter('recursive', 'equals', 'true'));
397
398         // resolve path
399         $parents = array();
400         $app = Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName);
401
402         /** @var Tinebase_Model_Tree_Node $fileNode */
403         foreach($result as $fileNode) {
404             if (!isset($parents[$fileNode->parent_id])) {
405                 $path = Tinebase_Model_Tree_Node_Path::createFromStatPath($this->_backend->getPathOfNode($this->_backend->get($fileNode->parent_id), true));
406                 $parents[$fileNode->parent_id] = Tinebase_Model_Tree_Node_Path::removeAppIdFromPath($path, $app);
407             }
408
409             $fileNode->path = $parents[$fileNode->parent_id] . '/' . $fileNode->name;
410         }
411         
412         return $result;
413     }
414     
415     /**
416      * checks filter acl and adds base path
417      * 
418      * @param Tinebase_Model_Filter_FilterGroup $_filter
419      * @param string $_action get|update
420      * @return Tinebase_Model_Tree_Node_Path
421      * @throws Tinebase_Exception_AccessDenied
422      */
423     protected function _checkFilterACL(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
424     {
425         if ($_filter === NULL) {
426             $_filter = new Tinebase_Model_Tree_Node_Filter();
427         }
428         
429         $pathFilters = $_filter->getFilter('path', TRUE);
430         if (count($pathFilters) !== 1) {
431             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
432                 . 'Exactly one path filter required.');
433             $pathFilter = (count($pathFilters) > 1) ? $pathFilters[0] : new Tinebase_Model_Tree_Node_PathFilter(array(
434                 'field'     => 'path',
435                 'operator'  => 'equals',
436                 'value'     => '/',)
437             );
438             $_filter->removeFilter('path');
439             $_filter->addFilter($pathFilter);
440         } else {
441             $pathFilter = $pathFilters[0];
442         }
443         
444         // add base path and check grants
445         try {
446             $path = Tinebase_Model_Tree_Node_Path::createFromPath($this->addBasePath($pathFilter->getValue()));
447         } catch (Exception $e) {
448             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
449                 . ' Could not determine path, setting root path (' . $e->getMessage() . ')');
450             $path = Tinebase_Model_Tree_Node_Path::createFromPath($this->addBasePath('/'));
451         }
452         $pathFilter->setValue($path);
453         
454         $this->_backend->checkPathACL($path, $_action);
455         
456         return $path;
457     }
458     
459     /**
460      * get the three root nodes
461      * 
462      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
463      *
464      * TODO think about using the "real" ids instead of myUser/other/shared
465      */
466     protected function _getRootNodes()
467     {
468         $translate = Tinebase_Translation::getTranslation($this->_applicationName);
469         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array(
470             array(
471                 'name'   => $translate->_('My folders'),
472                 'path'   => '/' . Tinebase_FileSystem::FOLDER_TYPE_PERSONAL . '/' . Tinebase_Core::getUser()->accountLoginName,
473                 'type'   => Tinebase_Model_Tree_FileObject::TYPE_FOLDER,
474                 'id'     => 'myUser',
475                 'grants' => array(),
476             ),
477             array(
478                 'name' => $translate->_('Shared folders'),
479                 'path' => '/' . Tinebase_FileSystem::FOLDER_TYPE_SHARED,
480                 'type' => Tinebase_Model_Tree_FileObject::TYPE_FOLDER,
481                 'id' => Tinebase_FileSystem::FOLDER_TYPE_SHARED,
482                 'grants' => array(),
483             ),
484             array(
485                 'name' => $translate->_('Other users folders'),
486                 'path' => '/' . Tinebase_FileSystem::FOLDER_TYPE_PERSONAL,
487                 'type' => Tinebase_Model_Tree_FileObject::TYPE_FOLDER,
488                 'id' => Tinebase_Model_Container::TYPE_OTHERUSERS,
489                 'grants' => array(),
490             ),
491         ), TRUE); // bypass validation
492         
493         return $result;
494     }
495
496     /**
497      * get other users nodes
498      * 
499      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
500      */
501     protected function _getOtherUserNodes()
502     {
503         $result = $this->_backend->getOtherUsers(Tinebase_Core::getUser(), $this->_applicationName, Tinebase_Model_Grants::GRANT_READ);
504         return $result;
505     }
506
507     /**
508      * sort nodes (only checks if we are on the container level and sort by container_name then)
509      *
510      * @param Tinebase_Record_RecordSet $nodes
511      * @param Tinebase_Model_Tree_Node_Path $path
512      * @param Tinebase_Model_Pagination $pagination
513      *
514      * TODO still needed?
515      */
516     protected function _sortContainerNodes(Tinebase_Record_RecordSet $nodes, Tinebase_Model_Tree_Node_Path $path, Tinebase_Model_Pagination $pagination = NULL)
517     {
518 //        if ($path->container || ($pagination !== NULL && $pagination->sort && $pagination->sort !== 'name')) {
519 //            // no toplevel path or no sorting by name -> sorting should be already handled by search()
520 //            return;
521 //        }
522 //
523 //        $dir = ($pagination !== NULL && $pagination->dir) ? $pagination->dir : 'ASC';
524 //
525 //        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
526 //            . ' Sorting container nodes by name (path: ' . $path->flatpath . ') / dir: ' . $dir);
527 //
528 //        $nodes->sort('container_name', $dir);
529     }
530
531     /**
532      * get file node
533      * 
534      * @param Tinebase_Model_Tree_Node_Path $_path
535      * @param integer|null $_revision
536      * @return Tinebase_Model_Tree_Node
537      */
538     public function getFileNode(Tinebase_Model_Tree_Node_Path $_path, $_revision = null)
539     {
540         $this->_backend->checkPathACL($_path, 'get');
541         
542         if (! $this->_backend->fileExists($_path->statpath, $_revision)) {
543             throw new Filemanager_Exception('File does not exist,');
544         }
545         
546         if (! $this->_backend->isFile($_path->statpath)) {
547             throw new Filemanager_Exception('Is a directory');
548         }
549         
550         return $this->_backend->stat($_path->statpath, $_revision);
551     }
552     
553     /**
554      * add base path
555      * 
556      * @param Tinebase_Model_Tree_Node_PathFilter $_pathFilter
557      * @return string
558      *
559      * TODO should be removed/replaced
560      */
561     public function addBasePath($_path)
562     {
563         $basePath = $this->_backend->getApplicationBasePath(Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName));
564         $basePath .= '/folders';
565         
566         $path = (strpos($_path, '/') === 0) ? $_path : '/' . $_path;
567         // only add base path once
568         $result = strpos($path, $basePath) !== 0 ? $basePath . $path : $path;
569         
570         return $result;
571     }
572
573     /**
574      * @param $_path
575      * @return mixed
576      * @throws Tinebase_Exception_InvalidArgument
577      * @throws Tinebase_Exception_NotFound
578      *
579      * TODO should be removed/replaced
580      */
581     public function removeBasePath($_path)
582     {
583         $basePath = $this->_backend->getApplicationBasePath(Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName));
584         $basePath .= '/folders';
585
586         return preg_replace('@^' . preg_quote($basePath) . '@', '', $_path);
587     }
588
589     /**
590      * Gets total count of search with $_filter
591      * 
592      * @param Tinebase_Model_Filter_FilterGroup $_filter
593      * @param string|optional $_action
594      * @return int
595      */
596     public function searchCount(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
597     {
598         if ($_filter->getFilter('recursive')) {
599             $_filter->removeFilter('recursive');
600             $result = $this->_backend->searchNodesCount($_filter);
601             $_filter->addFilter($_filter->createFilter('recursive', 'equals', 'true'));
602         } else {
603             $path = $this->_checkFilterACL($_filter, $_action);
604             if ($path->containerType === Tinebase_Model_Tree_Node_Path::TYPE_ROOT) {
605                 $result = count($this->_getRootNodes());
606             } else if ($path->containerType === Tinebase_FileSystem::FOLDER_TYPE_PERSONAL && !$path->containerOwner) {
607                 $result = count($this->_getOtherUserNodes());
608             } else {
609                 $result = $this->_backend->searchNodesCount($_filter);
610             }
611         }
612         
613         return $result;
614     }
615
616     /**
617      * create node(s)
618      * 
619      * @param string|array $_filenames
620      * @param string $_type directory or file
621      * @param array $_tempFileIds
622      * @param boolean $_forceOverwrite
623      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
624      */
625     public function createNodes($_filenames, $_type, $_tempFileIds = array(), $_forceOverwrite = FALSE)
626     {
627         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
628         $nodeExistsException = NULL;
629         
630         foreach ((array) $_filenames as $idx => $filename) {
631             $tempFileId = (isset($_tempFileIds[$idx])) ? $_tempFileIds[$idx] : NULL;
632
633             try {
634                 $node = $this->_createNode($filename, $_type, $tempFileId, $_forceOverwrite);
635                 if ($node) {
636                     $result->addRecord($node);
637                 }
638             } catch (Filemanager_Exception_NodeExists $fene) {
639                 $nodeExistsException = $this->_handleNodeExistsException($fene, $nodeExistsException);
640             }
641         }
642
643         if ($nodeExistsException) {
644             throw $nodeExistsException;
645         }
646         
647         return $result;
648     }
649     
650     /**
651      * collect information of a Filemanager_Exception_NodeExists in a "parent" exception
652      * 
653      * @param Filemanager_Exception_NodeExists $_fene
654      * @param Filemanager_Exception_NodeExists|NULL $_parentNodeExistsException
655      */
656     protected function _handleNodeExistsException($_fene, $_parentNodeExistsException = NULL)
657     {
658         // collect all nodes that already exist and add them to exception info
659         if (! $_parentNodeExistsException) {
660             $_parentNodeExistsException = new Filemanager_Exception_NodeExists();
661         }
662         
663         $nodesInfo = $_fene->getExistingNodesInfo();
664         if (count($nodesInfo) > 0) {
665             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
666                 . ' Adding node info to exception.');
667             $_parentNodeExistsException->addExistingNodeInfo($nodesInfo->getFirstRecord());
668         } else {
669             return $_fene;
670         }
671         
672         return $_parentNodeExistsException;
673     }
674     
675     /**
676      * create new node
677      * 
678      * @param string|Tinebase_Model_Tree_Node_Path $_path
679      * @param string $_type
680      * @param string $_tempFileId
681      * @param boolean $_forceOverwrite
682      * @return Tinebase_Model_Tree_Node
683      * @throws Tinebase_Exception_InvalidArgument
684      */
685     protected function _createNode($_path, $_type, $_tempFileId = NULL, $_forceOverwrite = FALSE)
686     {
687         if (! in_array($_type, array(Tinebase_Model_Tree_FileObject::TYPE_FILE, Tinebase_Model_Tree_FileObject::TYPE_FOLDER))) {
688             throw new Tinebase_Exception_InvalidArgument('Type ' . $_type . 'not supported.');
689         } 
690
691         $path = ($_path instanceof Tinebase_Model_Tree_Node_Path) 
692             ? $_path : Tinebase_Model_Tree_Node_Path::createFromPath($this->addBasePath($_path));
693         $parentPathRecord = $path->getParent();
694         $existingNode = null;
695         
696         // we need to check the parent record existence before commencing node creation
697
698         try {
699             $parentPathRecord->validateExistance();
700         } catch (Tinebase_Exception_NotFound $tenf) {
701             if ($parentPathRecord->isToplevelPath()) {
702                 $this->_backend->mkdir($parentPathRecord->statpath);
703             } else {
704                 throw $tenf;
705             }
706         }
707         
708         try {
709             $this->_checkIfExists($path);
710             $this->_backend->checkPathACL($parentPathRecord, 'add', /* $_topLevelAllowed */ $_type === Tinebase_Model_Tree_FileObject::TYPE_FOLDER);
711         } catch (Filemanager_Exception_NodeExists $fene) {
712             if ($_forceOverwrite) {
713
714                 // race condition for concurrent delete, try catch Tinebase_Exception_NotFound ... but throwing the exception in that rare case doesn't hurt so much
715                 $existingNode = $this->_backend->stat($path->statpath);
716                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
717                     . ' Existing node: ' . print_r($existingNode->toArray(), TRUE));
718
719                 if (! $_tempFileId) {
720                     // just return the exisiting node and do not overwrite existing file if no tempfile id was given
721                     $this->_backend->checkPathACL($path, 'get');
722                     $this->resolvePath($existingNode, $parentPathRecord);
723                     $this->resolveGrants($existingNode);
724                     return $existingNode;
725
726                 } elseif ($existingNode->type !== $_type) {
727                     throw new Tinebase_Exception_SystemGeneric('Can not overwrite a folder with a file');
728
729                 } else {
730                     // check if a new (size 0) file is overwritten
731                     // @todo check revision here?
732                     if ($existingNode->size == 0) {
733                         $this->_backend->checkPathACL($parentPathRecord, 'add');
734                     } else {
735                         $this->_backend->checkPathACL($parentPathRecord, 'update');
736                     }
737                 }
738             } else if (! $_forceOverwrite) {
739                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
740                     . ' ' . $fene);
741                 throw $fene;
742             }
743         }
744
745         $newNodePath = $parentPathRecord->statpath . '/' . $path->name;
746         $newNode = $this->_createNodeInBackend($newNodePath, $_type, $_tempFileId);
747         $this->_writeModlogForNewNode($newNode, /*$existingNode,*/ $_type, $_path);
748
749         $this->resolvePath($newNode, $parentPathRecord);
750         $this->resolveGrants($newNode);
751         return $newNode;
752     }
753
754     /**
755      * @param Tinebase_Model_Tree_Node $_newNode
756      * @param string $_type
757      * @param string $_path
758      */
759     protected function _writeModlogForNewNode($_newNode, /*$_existingNode,*/ $_type, $_path)
760     {
761         if (Tinebase_Model_Tree_FileObject::TYPE_FOLDER === $_type && false === $this->_inCopyOrMoveNode) {
762             $modlogNode = new Filemanager_Model_Node(array(
763                 'id' => $_newNode->getId(),
764                 'path' => ($_path instanceof Tinebase_Model_Tree_Node_Path) ? $this->removeBasePath($_path->flatpath) : $_path,
765                 'type' => Tinebase_Model_Tree_FileObject::TYPE_FOLDER,
766                 // no grants, no acl_node, it will all be handled on client side
767                 //'grants' => Tinebase_Tree_NodeGrants::getInstance()->getGrantsForRecord($_newNode)
768             ), true);
769             $this->_omitModLog = false;
770             $this->_writeModLog($modlogNode, null);
771             $this->_omitModLog = true;
772         }
773     }
774     
775     /**
776      * create node in backend
777      * 
778      * @param string $_statpath
779      * @param type
780      * @param string $_tempFileId
781      * @return Tinebase_Model_Tree_Node
782      */
783     protected function _createNodeInBackend($_statpath, $_type, $_tempFileId = NULL)
784     {
785         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . 
786             ' Creating new path ' . $_statpath . ' of type ' . $_type);
787
788         $node = NULL;
789         switch ($_type) {
790             case Tinebase_Model_Tree_FileObject::TYPE_FILE:
791                 if (null === $_tempFileId) {
792                     $this->_backend->createFileTreeNode($this->_backend->stat(dirname($_statpath)), basename($_statpath));
793                 } else {
794                     $this->_backend->copyTempfile($_tempFileId, $_statpath);
795                 }
796                 break;
797
798             case Tinebase_Model_Tree_FileObject::TYPE_FOLDER:
799                 $path = Tinebase_Model_Tree_Node_Path::createFromStatPath($_statpath);
800                 if ($path->getParent()->isToplevelPath()) {
801                     $node = $this->_backend->createAclNode($_statpath);
802                 } else {
803                     $node = $this->_backend->mkdir($_statpath);
804                 }
805                 break;
806         }
807
808         return $node !== null ? $node : $this->_backend->stat($_statpath);
809     }
810     
811     /**
812      * check file existance
813      * 
814      * @param Tinebase_Model_Tree_Node_Path $_path
815      * @param Tinebase_Model_Tree_Node $_node
816      * @throws Filemanager_Exception_NodeExists
817      */
818     protected function _checkIfExists(Tinebase_Model_Tree_Node_Path $_path, $_node = NULL)
819     {
820         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
821             . ' Check existance of ' . $_path->statpath);
822
823         if ($this->_backend->fileExists($_path->statpath)) {
824             
825             if (! $_node) {
826                 $_node = $this->_backend->stat($_path->statpath);
827             }
828             
829             if ($_node) {
830                 $existsException = new Filemanager_Exception_NodeExists();
831                 $existsException->addExistingNodeInfo($_node);
832                 throw $existsException;
833             }
834         }
835     }
836     
837     /**
838      * create new container
839      * 
840      * @param string $_name
841      * @param string $_type
842      * @return Tinebase_Model_Container
843      * @throws Tinebase_Exception_Record_NotAllowed
844      */
845     protected function _createContainer($_name, $_type)
846     {
847         $ownerId = ($_type === Tinebase_FileSystem::FOLDER_TYPE_PERSONAL) ? Tinebase_Core::getUser()->getId() : NULL;
848         try {
849             $existingContainer = Tinebase_Container::getInstance()->getContainerByName(
850                 $this->_applicationName, $_name, $_type, $ownerId);
851             throw new Filemanager_Exception_NodeExists('Container ' . $_name . ' of type ' . $_type . ' already exists.');
852         } catch (Tinebase_Exception_NotFound $tenf) {
853             // go on
854         }
855         
856         $app = Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName);
857         $container = Tinebase_Container::getInstance()->addContainer(new Tinebase_Model_Container(array(
858             'name'           => $_name,
859             'type'           => $_type,
860             'backend'        => 'sql',
861             'application_id' => $app->getId(),
862             'model'          => $this->_modelName
863         )));
864         
865         return $container;
866     }
867
868     /**
869      * resolve node paths for frontends
870      *
871      * if a single record is given, use the resulting record set, because the referenced record is no longer updated!
872      *
873      * @param Tinebase_Record_RecordSet|Tinebase_Model_Tree_Node $_records
874      * @param Tinebase_Model_Tree_Node_Path $_path
875      */
876     public function resolvePath($_records, Tinebase_Model_Tree_Node_Path $_path)
877     {
878         $records = ($_records instanceof Tinebase_Model_Tree_Node) 
879             ? new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array($_records)) : $_records;
880
881         $app = Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName);
882         $flatpathWithoutBasepath = Tinebase_Model_Tree_Node_Path::removeAppIdFromPath($_path->flatpath, $app);
883         if ($records) {
884             foreach ($records as $record) {
885                 $record->path = $flatpathWithoutBasepath . '/' . $record->name;
886             }
887         }
888
889         return $records;
890     }
891
892     /**
893      * @param $_records
894      * @return Tinebase_Record_RecordSet
895      * @throws Tinebase_Exception_NotFound
896      */
897     public function resolveGrants($_records)
898     {
899         $records = ($_records instanceof Tinebase_Model_Tree_Node)
900             ? new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array($_records)) : $_records;
901         if ($records) {
902             foreach ($records as $record) {
903                 $grantNode = $this->_getGrantNode($record);
904                 $record->account_grants = $this->_backend->getGrantsOfAccount(
905                     Tinebase_Core::getUser(),
906                     $grantNode
907                 )->toArray();
908                 if (! isset($record->grants)) {
909                     try {
910                         $record->grants = Tinebase_FileSystem::getInstance()->getGrantsOfContainer($record);
911                     } catch (Tinebase_Exception_AccessDenied $tead) {
912                         $record->grants = new Tinebase_Record_RecordSet('Tinebase_Model_Grants');
913                     }
914                 }
915             }
916         }
917
918         return $records;
919     }
920
921     protected function _getGrantNode($record)
922     {
923         try {
924             switch ($record->getId()) {
925                 case 'myUser':
926                     $path = $this->_backend->getApplicationBasePath($this->_applicationName, Tinebase_FileSystem::FOLDER_TYPE_PERSONAL);
927                     $path .= '/' . Tinebase_Core::getUser()->getId();
928                     $grantRecord = $this->_backend->stat($path);
929                     break;
930                 case Tinebase_FileSystem::FOLDER_TYPE_SHARED:
931                     $path = $this->_backend->getApplicationBasePath($this->_applicationName, Tinebase_FileSystem::FOLDER_TYPE_SHARED);
932                     $grantRecord = $this->_backend->stat($path);
933                     break;
934                 case Tinebase_Model_Container::TYPE_OTHERUSERS:
935                     $path = $this->_backend->getApplicationBasePath($this->_applicationName, Tinebase_FileSystem::FOLDER_TYPE_PERSONAL);
936                     $grantRecord = $this->_backend->stat($path);
937                     break;
938                 default:
939                     $grantRecord = clone($record);
940             }
941         } catch (Tinebase_Exception_NotFound $tenf) {
942             if (isset($path)) {
943                 $grantRecord = $this->_backend->createAclNode($path);
944             } else {
945                 throw $tenf;
946             }
947         }
948
949         return $grantRecord;
950     }
951
952     /**
953      * copy nodes
954      * 
955      * @param array $_sourceFilenames array->multiple
956      * @param string|array $_destinationFilenames string->singlefile OR directory, array->multiple files
957      * @param boolean $_forceOverwrite
958      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
959      */
960     public function copyNodes($_sourceFilenames, $_destinationFilenames, $_forceOverwrite = FALSE)
961     {
962         return $this->_copyOrMoveNodes($_sourceFilenames, $_destinationFilenames, 'copy', $_forceOverwrite);
963     }
964     
965     /**
966      * copy or move an array of nodes identified by their path
967      * 
968      * @param array $_sourceFilenames array->multiple
969      * @param string|array $_destinationFilenames string->singlefile OR directory, array->multiple files
970      * @param string $_action copy|move
971      * @param boolean $_forceOverwrite
972      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
973      */
974     protected function _copyOrMoveNodes($_sourceFilenames, $_destinationFilenames, $_action, $_forceOverwrite = FALSE)
975     {
976         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
977         $nodeExistsException = NULL;
978
979         $this->_inCopyOrMoveNode = true;
980         
981         foreach ($_sourceFilenames as $idx => $source) {
982             $sourcePathRecord = Tinebase_Model_Tree_Node_Path::createFromPath($this->addBasePath($source));
983             $destinationPathRecord = $this->_getDestinationPath($_destinationFilenames, $idx, $sourcePathRecord);
984             
985             if ($this->_backend->fileExists($destinationPathRecord->statpath) && $sourcePathRecord->flatpath == $destinationPathRecord->flatpath) {
986                 throw new Filemanager_Exception_DestinationIsSameNode();
987             }
988             
989             // test if destination is subfolder of source
990             $dest = explode('/', $destinationPathRecord->statpath);
991             $source = explode('/', $sourcePathRecord->statpath);
992             $isSub = TRUE;
993
994             $i = 0;
995             for ($iMax = count($source); $i < $iMax; $i++) {
996                 
997                 if (! isset($dest[$i])) {
998                     break;
999                 }
1000                 
1001                 if ($source[$i] != $dest[$i]) {
1002                     $isSub = FALSE;
1003                 }
1004             }
1005             if ($isSub) {
1006                 throw new Filemanager_Exception_DestinationIsOwnChild();
1007             }
1008             
1009             try {
1010                 if ($_action === 'move') {
1011                     $node = $this->_moveNode($sourcePathRecord, $destinationPathRecord, $_forceOverwrite);
1012                 } else if ($_action === 'copy') {
1013                     $node = $this->_copyNode($sourcePathRecord, $destinationPathRecord, $_forceOverwrite);
1014                 }
1015
1016                 if ($node instanceof Tinebase_Record_Abstract) {
1017                     $result->addRecord($node);
1018
1019                     if (Tinebase_Model_Tree_FileObject::TYPE_FOLDER === $node->type) {
1020                         $modlogNode = new Filemanager_Model_Node(array(
1021                             'id' => $node->getId(),
1022                             'path' => is_array($_destinationFilenames) ? array($_destinationFilenames[$idx]) : $_destinationFilenames,
1023                             'type' => Tinebase_Model_Tree_FileObject::TYPE_FOLDER,
1024                             'name' => $_action,
1025                             // do not set acl_node, this will be calculated on the client side
1026                         ), true);
1027                         $modlogOldNode = new Filemanager_Model_Node(array(
1028                             'id' => $node->getId(),
1029                             'path' => $_sourceFilenames[$idx],
1030                         ), true);
1031                         $this->_omitModLog = false;
1032                         $this->_writeModLog($modlogNode, $modlogOldNode);
1033                         $this->_omitModLog = true;
1034                     }
1035
1036                 } else {
1037                     if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
1038                         . ' Could not copy or move node to destination ' . $destinationPathRecord->flatpath);
1039                 }
1040             } catch (Filemanager_Exception_NodeExists $fene) {
1041                 $this->_inCopyOrMoveNode = false;
1042                 $nodeExistsException = $this->_handleNodeExistsException($fene, $nodeExistsException);
1043             }
1044         }
1045         
1046         $this->resolvePath($result, $destinationPathRecord->getParent());
1047         $this->resolveGrants($result);
1048
1049         if ($nodeExistsException) {
1050             // @todo add correctly moved/copied files here?
1051             throw $nodeExistsException;
1052         }
1053
1054         $this->_inCopyOrMoveNode = false;
1055         
1056         return $result;
1057     }
1058     
1059     /**
1060      * get single destination from an array of destinations and an index + $_sourcePathRecord
1061      * 
1062      * @param string|array $_destinationFilenames
1063      * @param int $_idx
1064      * @param Tinebase_Model_Tree_Node_Path $_sourcePathRecord
1065      * @return Tinebase_Model_Tree_Node_Path
1066      * @throws Filemanager_Exception
1067      * 
1068      * @todo add Tinebase_FileSystem::isDir() check?
1069      */
1070     protected function _getDestinationPath($_destinationFilenames, $_idx, $_sourcePathRecord)
1071     {
1072         if (is_array($_destinationFilenames)) {
1073             $isdir = FALSE;
1074             if (isset($_destinationFilenames[$_idx])) {
1075                 $destination = $_destinationFilenames[$_idx];
1076             } else {
1077                 throw new Filemanager_Exception('No destination path found.');
1078             }
1079         } else {
1080             $isdir = TRUE;
1081             $destination = $_destinationFilenames;
1082         }
1083         
1084         if ($isdir) {
1085             $destination = $destination . '/' . $_sourcePathRecord->name;
1086         }
1087         
1088         return Tinebase_Model_Tree_Node_Path::createFromPath($this->addBasePath($destination));
1089     }
1090     
1091     /**
1092      * copy single node
1093      * 
1094      * @param Tinebase_Model_Tree_Node_Path $_source
1095      * @param Tinebase_Model_Tree_Node_Path $_destination
1096      * @param boolean $_forceOverwrite
1097      * @return Tinebase_Model_Tree_Node
1098      */
1099     protected function _copyNode(Tinebase_Model_Tree_Node_Path $_source, Tinebase_Model_Tree_Node_Path $_destination, $_forceOverwrite = FALSE)
1100     {
1101         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
1102             . ' Copy Node ' . $_source->flatpath . ' to ' . $_destination->flatpath);
1103                 
1104         $newNode = NULL;
1105         
1106         $this->_backend->checkPathACL($_source, 'get', FALSE);
1107         
1108         $sourceNode = $this->_backend->stat($_source->statpath);
1109         
1110         switch ($sourceNode->type) {
1111             case Tinebase_Model_Tree_FileObject::TYPE_FILE:
1112                 $newNode = $this->_copyOrMoveFileNode($_source, $_destination, 'copy', $_forceOverwrite);
1113                 break;
1114             case Tinebase_Model_Tree_FileObject::TYPE_FOLDER:
1115                 $newNode = $this->_copyFolderNode($_source, $_destination);
1116                 break;
1117         }
1118         
1119         return $newNode;
1120     }
1121     
1122     /**
1123      * copy file node
1124      * 
1125      * @param Tinebase_Model_Tree_Node_Path $_source
1126      * @param Tinebase_Model_Tree_Node_Path $_destination
1127      * @param string $_action
1128      * @param boolean $_forceOverwrite
1129      * @return Tinebase_Model_Tree_Node
1130      */
1131     protected function _copyOrMoveFileNode(Tinebase_Model_Tree_Node_Path $_source, Tinebase_Model_Tree_Node_Path $_destination, $_action, $_forceOverwrite = FALSE)
1132     {
1133         $this->_backend->checkPathACL($_destination->getParent(), 'update', FALSE);
1134         
1135         try {
1136             $this->_checkIfExists($_destination);
1137         } catch (Filemanager_Exception_NodeExists $fene) {
1138             if ($_forceOverwrite && $_source->statpath !== $_destination->statpath) {
1139                 // delete old node
1140                 $this->_backend->unlink($_destination->statpath);
1141             } elseif (! $_forceOverwrite) {
1142                 throw $fene;
1143             }
1144         }
1145         
1146         switch ($_action) {
1147             case 'copy':
1148                 $newNode = $this->_backend->copy($_source->statpath, $_destination->statpath);
1149                 break;
1150             case 'move':
1151                 $newNode = $this->_backend->rename($_source->statpath, $_destination->statpath);
1152                 break;
1153         }
1154
1155         return $newNode;
1156     }
1157     
1158     /**
1159      * copy folder node
1160      * 
1161      * @param Tinebase_Model_Tree_Node_Path $_source
1162      * @param Tinebase_Model_Tree_Node_Path $_destination
1163      * @return Tinebase_Model_Tree_Node
1164      * @throws Filemanager_Exception_NodeExists
1165      * 
1166      * @todo add $_forceOverwrite?
1167      */
1168     protected function _copyFolderNode(Tinebase_Model_Tree_Node_Path $_source, Tinebase_Model_Tree_Node_Path $_destination)
1169     {
1170         $newNode = $this->_createNode($_destination, Tinebase_Model_Tree_FileObject::TYPE_FOLDER);
1171         
1172         // recursive copy for (sub-)folders/files
1173         $filter = new Tinebase_Model_Tree_Node_Filter(array(array(
1174             'field'    => 'path', 
1175             'operator' => 'equals', 
1176             'value'    => Tinebase_Model_Tree_Node_Path::removeAppIdFromPath(
1177                 $_source->flatpath, 
1178                 Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName)
1179             ),
1180         )));
1181         $result = $this->search($filter);
1182         if (count($result) > 0) {
1183             $this->copyNodes($result->path, $newNode->path);
1184         }
1185         
1186         return $newNode;
1187     }
1188     
1189     /**
1190      * move nodes
1191      * 
1192      * @param array $_sourceFilenames array->multiple
1193      * @param string|array $_destinationFilenames string->singlefile OR directory, array->multiple files
1194      * @param boolean $_forceOverwrite
1195      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
1196      */
1197     public function moveNodes($_sourceFilenames, $_destinationFilenames, $_forceOverwrite = FALSE)
1198     {
1199         return $this->_copyOrMoveNodes($_sourceFilenames, $_destinationFilenames, 'move', $_forceOverwrite);
1200     }
1201     
1202     /**
1203      * move single node
1204      * 
1205      * @param Tinebase_Model_Tree_Node_Path $_source
1206      * @param Tinebase_Model_Tree_Node_Path $_destination
1207      * @param boolean $_forceOverwrite
1208      * @return Tinebase_Model_Tree_Node
1209      */
1210     protected function _moveNode(Tinebase_Model_Tree_Node_Path $_source, Tinebase_Model_Tree_Node_Path $_destination, $_forceOverwrite = FALSE)
1211     {
1212         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
1213             . ' Move Node ' . $_source->flatpath . ' to ' . $_destination->flatpath);
1214         
1215         $sourceNode = $this->_backend->stat($_source->statpath);
1216         
1217         switch ($sourceNode->type) {
1218             case Tinebase_Model_Tree_FileObject::TYPE_FILE:
1219                 $movedNode = $this->_copyOrMoveFileNode($_source, $_destination, 'move', $_forceOverwrite);
1220                 break;
1221             case Tinebase_Model_Tree_FileObject::TYPE_FOLDER:
1222                 $movedNode = $this->_moveFolderNode($_source, $_destination, $_forceOverwrite);
1223                 break;
1224         }
1225         
1226         return $movedNode;
1227     }
1228     
1229     /**
1230      * move folder node
1231      * 
1232      * @param Tinebase_Model_Tree_Node_Path $source
1233      * @param Tinebase_Model_Tree_Node $sourceNode [unused]
1234      * @param Tinebase_Model_Tree_Node_Path $destination
1235      * @param boolean $_forceOverwrite
1236      * @return Tinebase_Model_Tree_Node
1237      * @throws Filemanager_Exception_NodeExists
1238      */
1239     protected function _moveFolderNode($source, $destination, $_forceOverwrite = FALSE)
1240     {
1241         $this->_backend->checkPathACL($source, 'get', FALSE);
1242         
1243         $destinationParentPathRecord = $destination->getParent();
1244         $destinationNodeName = NULL;
1245         
1246         $this->_backend->checkPathACL($destinationParentPathRecord, 'update');
1247         // TODO do we need this if??
1248         //if ($source->getParent()->flatpath != $destinationParentPathRecord->flatpath) {
1249             try {
1250                 $this->_checkIfExists($destination);
1251             } catch (Filemanager_Exception_NodeExists $fene) {
1252                 if ($_forceOverwrite && $source->statpath !== $destination->statpath) {
1253                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1254                         . ' Removing folder node ' . $destination->statpath);
1255                     $this->_backend->rmdir($destination->statpath, TRUE);
1256                 } else if (! $_forceOverwrite) {
1257                     throw $fene;
1258                 }
1259             }
1260 //        } else {
1261 //            if (! $_forceOverwrite) {
1262 //                $this->_checkIfExists($destination);
1263 //            }
1264 //        }
1265
1266         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1267             . ' Rename Folder ' . $source->statpath . ' -> ' . $destination->statpath);
1268
1269         $this->_backend->rename($source->statpath, $destination->statpath);
1270
1271         $movedNode = $this->_backend->stat($destination->statpath);
1272         if ($destinationNodeName !== NULL) {
1273             $movedNode->name = $destinationNodeName;
1274         }
1275         
1276         return $movedNode;
1277     }
1278
1279     /**
1280      * delete nodes
1281      * 
1282      * @param array $_filenames string->single file, array->multiple
1283      * @return int delete count
1284      * 
1285      * @todo add recursive param?
1286      */
1287     public function deleteNodes($_filenames)
1288     {
1289         $deleteCount = 0;
1290         foreach ($_filenames as $filename) {
1291             if ($this->_deleteNode($filename)) {
1292                 $deleteCount++;
1293             }
1294         }
1295         
1296         return $deleteCount;
1297     }
1298
1299     /**
1300      * delete node
1301      * 
1302      * @param string $_flatpath
1303      * @return boolean
1304      * @throws Tinebase_Exception_NotFound
1305      */
1306     protected function _deleteNode($_flatpath)
1307     {
1308         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1309             ' Delete path: ' . $_flatpath);
1310
1311         $flatpathWithBasepath = $this->addBasePath($_flatpath);
1312         list($parentPathRecord, $nodeName) = Tinebase_Model_Tree_Node_Path::getParentAndChild($flatpathWithBasepath);
1313         $pathRecord = Tinebase_Model_Tree_Node_Path::createFromPath($flatpathWithBasepath);
1314         
1315         $this->_backend->checkPathACL($parentPathRecord, 'delete');
1316         $success = $this->_deleteNodeInBackend($pathRecord, $_flatpath);
1317
1318         return $success;
1319     }
1320     
1321     /**
1322      * delete node in backend
1323      * 
1324      * @param Tinebase_Model_Tree_Node_Path $_path
1325      * @param string $_flatpath
1326      * @return boolean
1327      */
1328     protected function _deleteNodeInBackend(Tinebase_Model_Tree_Node_Path $_path, $_flatpath)
1329     {
1330         $success = FALSE;
1331         
1332         $node = $this->_backend->stat($_path->statpath);
1333         
1334         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . 
1335             ' Removing path ' . $_path->flatpath . ' of type ' . $node->type);
1336         
1337         switch ($node->type) {
1338             case Tinebase_Model_Tree_FileObject::TYPE_FILE:
1339                 $success = $this->_backend->unlink($_path->statpath);
1340                 break;
1341             case Tinebase_Model_Tree_FileObject::TYPE_FOLDER:
1342                 $success = $this->_backend->rmdir($_path->statpath, TRUE);
1343
1344                 if (FALSE !== $success && false === $this->_inCopyOrMoveNode) {
1345                     $modlogNode = new Filemanager_Model_Node(array(
1346                         'id' => $node->getId(),
1347                         'path' => $_flatpath,
1348                         'type' => Tinebase_Model_Tree_FileObject::TYPE_FOLDER
1349                     ), true);
1350                     $this->_omitModLog = false;
1351                     $this->_writeModLog(null, $modlogNode);
1352                     $this->_omitModLog = true;
1353                 }
1354                 break;
1355         }
1356         
1357         return $success;
1358     }
1359     
1360     /**
1361      * Deletes a set of records.
1362      *
1363      * If one of the records could not be deleted, no record is deleted
1364      *
1365      * NOTE: it is not possible to delete folders like this, it would lead to
1366      * Tinebase_Exception_InvalidArgument: can not unlink directories
1367      *
1368      * @param   array array of record identifiers
1369      * @return  Tinebase_Record_RecordSet
1370      */
1371     public function delete($_ids)
1372     {
1373         $nodes = $this->getMultiple($_ids);
1374         /** @var Tinebase_Model_Tree_Node $node */
1375         foreach ($nodes as $node) {
1376             if ($this->_backend->checkACLNode($node, 'delete')) {
1377                 $this->_backend->deleteFileNode($node);
1378             } else {
1379                 $nodes->removeRecord($node);
1380             }
1381         }
1382         
1383         return $nodes;
1384     }
1385
1386     /**
1387      * file message
1388      *
1389      * @param                          $targetPath
1390      * @param Felamimail_Model_Message $message
1391      * @returns Filemanager_Model_Node
1392      * @throws
1393      * @throws Filemanager_Exception_NodeExists
1394      * @throws Tinebase_Exception_AccessDenied
1395      * @throws null
1396      */
1397     public function fileMessage($targetPath, Felamimail_Model_Message $message)
1398     {
1399         // save raw message in temp file
1400         $rawContent = Felamimail_Controller_Message::getInstance()->getMessageRawContent($message);
1401         $tempFilename = Tinebase_TempFile::getInstance()->getTempPath();
1402         file_put_contents($tempFilename, $rawContent);
1403         $tempFile = Tinebase_TempFile::getInstance()->createTempFile($tempFilename);
1404
1405         $filename = $this->_getMessageNodeFilename($message);
1406
1407         $emlNode = $this->createNodes(
1408             array($targetPath . '/' . $filename),
1409             Tinebase_Model_Tree_FileObject::TYPE_FILE,
1410             array($tempFile->getId()),
1411             /* $_forceOverwrite */ true
1412         )->getFirstRecord();
1413
1414         $emlNode->description = $this->_getMessageNodeDescription($message);
1415         $emlNode->last_modified_time = Tinebase_DateTime::now();
1416         return $this->update($emlNode);
1417     }
1418
1419     /**
1420      * create node filename from message data
1421      *
1422      * @param $message
1423      * @return string
1424      */
1425     protected function _getMessageNodeFilename($message)
1426     {
1427         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1428             . ' ' . print_r($message->toArray(), true));
1429
1430         // remove '/' and '\' from name as this might break paths
1431         $subject = preg_replace('/[\/\\\]+/', '_', $message->subject);
1432         // remove possible harmful utf-8 chars
1433         // TODO should not be enabled by default (configurable?)
1434         $subject = Tinebase_Helper::mbConvertTo($subject, 'ASCII');
1435         $name = mb_substr($subject, 0, 245) . '_' . substr(md5($message->messageuid . $message->folder_id), 0, 10) . '.eml';
1436
1437         return $name;
1438     }
1439
1440     /**
1441      * create node description from message data
1442      *
1443      * @param Felamimail_Model_Message $message
1444      * @return string
1445      *
1446      * TODO use/create toString method for Felamimail_Model_Message?
1447      */
1448     protected function _getMessageNodeDescription(Felamimail_Model_Message $message)
1449     {
1450         // switch to user tz
1451         $message->setTimezone(Tinebase_Core::getUserTimezone());
1452
1453         $translate = Tinebase_Translation::getTranslation('Felamimail');
1454
1455         $description = '';
1456         $fieldsToAddToDescription = array(
1457             $translate->_('Received') => 'received',
1458             $translate->_('To') => 'to',
1459             $translate->_('Cc') => 'cc',
1460             $translate->_('Bcc') => 'bcc',
1461             $translate->_('From (E-Mail)') => 'from_email',
1462             $translate->_('From (Name)') => 'from_name',
1463             $translate->_('Body') => 'body',
1464             $translate->_('Attachments') => 'attachments'
1465         );
1466
1467         foreach ($fieldsToAddToDescription as $label => $field) {
1468             $description .= $label . ': ';
1469
1470             switch ($field) {
1471                 case 'received':
1472                     $description .= $message->received->toString();
1473                     break;
1474                 case 'body':
1475                     $completeMessage = Felamimail_Controller_Message::getInstance()->getCompleteMessage($message);
1476                     $plainText = $completeMessage->getPlainTextBody();
1477                     $description .= $plainText ."\n";
1478                     break;
1479                 case 'attachments':
1480                     foreach ((array) $message->{$field} as $attachment) {
1481                         if (is_array($attachment) && isset($attachment['filename']))
1482                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1483                             . ' ' . print_r($attachment, true));
1484                         $description .= '  ' . $attachment['filename'] . "\n";
1485                     }
1486                     break;
1487                 default:
1488                     $value = $message->{$field};
1489                     if (is_array($value)) {
1490                         $description .= implode(', ', $value);
1491                     } else {
1492                         $description .= $value;
1493                     }
1494             }
1495             $description .= "\n";
1496         }
1497
1498         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1499             . ' Description: ' . $description);
1500
1501         return $description;
1502     }
1503
1504     /**
1505      * @param Tinebase_Model_ModificationLog $modification
1506      */
1507     public function applyReplicationModificationLog(Tinebase_Model_ModificationLog $modification)
1508     {
1509         switch ($modification->change_type) {
1510             case Tinebase_Timemachine_ModificationLog::CREATED:
1511                 $diff = new Tinebase_Record_Diff(json_decode($modification->new_value, true));
1512                 $this->createNodes(array($diff->diff['path']), $diff->diff['type']);
1513                 break;
1514
1515             case Tinebase_Timemachine_ModificationLog::UPDATED:
1516                 $diff = new Tinebase_Record_Diff(json_decode($modification->new_value, true));
1517                 if (isset($diff->diff['name'])) {
1518                     $this->_copyOrMoveNodes(array($diff->oldData['path']), $diff->diff['path'], $diff->diff['name']);
1519                 } elseif(isset($diff->diff['grants'])) {
1520                     $record = $this->_backend->stat($diff->diff['path']);
1521                     if ('unset' === $diff->diff['grants']) {
1522                         $this->_backend->removeAclFromNode($record);
1523                     } else {
1524                         $this->_backend->setGrantsForNode($record, $diff->diff['grants']);
1525                     }
1526                 } else {
1527                     throw new Tinebase_Exception_InvalidArgument('update modlogs need the property name containing copy or move or grants for grants update');
1528                 }
1529                 break;
1530
1531             case Tinebase_Timemachine_ModificationLog::DELETED:
1532                 $diff = new Tinebase_Record_Diff(json_decode($modification->new_value, true));
1533                 $this->deleteNodes(array($diff->oldData['path']));
1534                 break;
1535
1536             default:
1537                 throw new Tinebase_Exception('unknown Tinebase_Model_ModificationLog->old_value: ' . $modification->old_value);
1538         }
1539     }
1540
1541     /**
1542      * Return usage array of a folder
1543      *
1544      * @param $_id
1545      * @return array of folder usage
1546      */
1547     public function getFolderUsage($_id)
1548     {
1549         $childIds = $this->_backend->getAllChildIds($_id, array(
1550             'field'     => 'type',
1551             'operator'  => 'equals',
1552             'value'     => Tinebase_Model_Tree_FileObject::TYPE_FILE
1553         ), false);
1554
1555         $createdBy = array();;
1556         $type = array();
1557         foreach($childIds as $id) {
1558             try {
1559                 $fileNode = $this->_backend->get($id);
1560             } catch(Tinebase_Exception_NotFound $tenf) {
1561                 continue;
1562             }
1563
1564             if (!isset($createdBy[$fileNode->created_by])) {
1565                 $createdBy[$fileNode->created_by] = array(
1566                     'size'          => $fileNode->size,
1567                     'revision_size' => $fileNode->revision_size
1568                 );
1569             } else {
1570                 $createdBy[$fileNode->created_by]['size']           += $fileNode->size;
1571                 $createdBy[$fileNode->created_by]['revision_size']  += $fileNode->revision_size;
1572             }
1573
1574             $ext = pathinfo($fileNode->name, PATHINFO_EXTENSION);
1575
1576             if (!isset($type[$ext])) {
1577                 $type[$ext] = array(
1578                     'size'          => $fileNode->size,
1579                     'revision_size' => $fileNode->revision_size
1580                 );
1581             } else {
1582                 $type[$ext]['size']           += $fileNode->size;
1583                 $type[$ext]['revision_size']  += $fileNode->revision_size;
1584             }
1585         }
1586
1587         return array('createdBy' => $createdBy, 'type' => $type);
1588     }
1589 }