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