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