7e158d314b5fb76367dbfb018e02dd700911c33b
[tine20] / tine20 / Tinebase / FileSystem.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Tinebase
6  * @subpackage  FileSystem
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Lars Kneschke <l.kneschke@metaways.de>
9  * @copyright   Copyright (c) 2010-2017 Metaways Infosystems GmbH (http://www.metaways.de)
10  *
11  */
12
13 /**
14  * filesystem controller
15  *
16  * @package     Tinebase
17  * @subpackage  FileSystem
18  */
19 class Tinebase_FileSystem implements
20     Tinebase_Controller_Interface,
21     Tinebase_Container_Interface,
22     Tinebase_Controller_Alarm_Interface
23 {
24     /**
25      * folder name/type for previews
26      *
27      * @var string
28      */
29     const FOLDER_TYPE_PREVIEWS = 'previews';
30
31     /**
32      * folder name/type for record attachments
33      *
34      * @var string
35      */
36     const FOLDER_TYPE_RECORDS = 'records';
37
38     /**
39      * folder name/type for record attachments
40      *
41      * @var string
42      */
43     const FOLDER_TYPE_SHARED = 'shared';
44
45     /**
46      * folder name/type for record attachments
47      *
48      * @var string
49      */
50     const FOLDER_TYPE_PERSONAL = 'personal';
51
52     const STREAM_OPTION_CREATE_PREVIEW = 'createPreview';
53
54     /**
55      * @var Tinebase_Tree_FileObject
56      */
57     protected $_fileObjectBackend;
58     
59     /**
60      * @var Tinebase_Tree_Node
61      */
62     protected $_treeNodeBackend = null;
63
64     /**
65      * @var string
66      */
67     protected $_treeNodeModel = 'Tinebase_Model_Tree_Node';
68
69     /**
70      * @var Tinebase_Tree_NodeGrants
71      */
72     protected $_nodeAclController = null;
73
74     /**
75      * path where physical files gets stored
76      *
77      * @var string
78      */
79     protected $_basePath;
80
81     protected $_modLogActive = false;
82
83     protected $_indexingActive = false;
84
85     protected $_previewActive = false;
86
87     protected $_streamOptionsForNextOperation = array();
88
89     protected $_notificationActive = false;
90
91     /**
92      * stat cache
93      *
94      * @var array
95      */
96     protected $_statCache = array();
97     
98     /**
99      * holds the instance of the singleton
100      *
101      * @var Tinebase_FileSystem
102      */
103     private static $_instance = null;
104     
105     /**
106      * the constructor
107      */
108     public function __construct()
109     {
110         if (! Tinebase_Core::isFilesystemAvailable()) {
111             throw new Tinebase_Exception_Backend('No base path (filesdir) configured or path not writeable');
112         }
113
114         $config = Tinebase_Core::getConfig();
115         $this->_basePath = $config->filesdir;
116
117         $fsConfig = $config->{Tinebase_Config::FILESYSTEM};
118         // FIXME why is this check needed (setup tests fail without)?
119         if ($fsConfig) {
120             $this->_modLogActive = true === $fsConfig->{Tinebase_Config::FILESYSTEM_MODLOGACTIVE};
121             $this->_indexingActive = true === $fsConfig->{Tinebase_Config::FILESYSTEM_INDEX_CONTENT};
122             $this->_notificationActive = true === $fsConfig->{Tinebase_Config::FILESYSTEM_ENABLE_NOTIFICATIONS};
123             $this->_previewActive = true === $fsConfig->{Tinebase_Config::FILESYSTEM_CREATE_PREVIEWS};
124         }
125
126         $this->_fileObjectBackend = new Tinebase_Tree_FileObject(null, array(
127             Tinebase_Config::FILESYSTEM_MODLOGACTIVE => $this->_modLogActive
128         ));
129
130         $this->_nodeAclController = Tinebase_Tree_NodeGrants::getInstance();
131     }
132     
133     /**
134      * the singleton pattern
135      *
136      * @return Tinebase_FileSystem
137      */
138     public static function getInstance()
139     {
140         if (self::$_instance === null) {
141             self::$_instance = new Tinebase_FileSystem;
142         }
143         
144         return self::$_instance;
145     }
146
147     public function resetBackends()
148     {
149         $config = Tinebase_Core::getConfig()->{Tinebase_Config::FILESYSTEM};
150         $this->_modLogActive = true === $config->{Tinebase_Config::FILESYSTEM_MODLOGACTIVE};
151         $this->_indexingActive = true === $config->{Tinebase_Config::FILESYSTEM_INDEX_CONTENT};
152         $this->_notificationActive = true === $config->{Tinebase_Config::FILESYSTEM_ENABLE_NOTIFICATIONS};
153         $this->_previewActive = true === $config->{Tinebase_Config::FILESYSTEM_CREATE_PREVIEWS};
154
155         $this->_treeNodeBackend = null;
156
157         $this->_fileObjectBackend  = new Tinebase_Tree_FileObject(null, array(
158             Tinebase_Config::FILESYSTEM_MODLOGACTIVE => $this->_modLogActive
159         ));
160     }
161
162     /**
163      * @return Tinebase_Tree_FileObject
164      */
165     public function getFileObjectBackend()
166     {
167         return $this->_fileObjectBackend;
168     }
169
170     public function setStreamOptionForNextOperation($_key, $_value)
171     {
172         $this->_streamOptionsForNextOperation[$_key] = $_value;
173     }
174
175     /**
176      * init application base paths
177      *
178      * @param Tinebase_Model_Application|string $_application
179      */
180     public function initializeApplication($_application)
181     {
182         // create app root node
183         $appPath = $this->getApplicationBasePath($_application);
184         if (!$this->fileExists($appPath)) {
185             $this->mkdir($appPath);
186         }
187         
188         $sharedBasePath = $this->getApplicationBasePath($_application, self::FOLDER_TYPE_SHARED);
189         if (!$this->fileExists($sharedBasePath)) {
190             $this->mkdir($sharedBasePath);
191         }
192         
193         $personalBasePath = $this->getApplicationBasePath($_application, self::FOLDER_TYPE_PERSONAL);
194         if (!$this->fileExists($personalBasePath)) {
195             $this->mkdir($personalBasePath);
196         }
197     }
198     
199     /**
200      * get application base path
201      *
202      * @param Tinebase_Model_Application|string $_application
203      * @param string $_type
204      * @return string
205      */
206     public function getApplicationBasePath($_application, $_type = null)
207     {
208         $application = $_application instanceof Tinebase_Model_Application
209             ? $_application
210             : Tinebase_Application::getInstance()->getApplicationById($_application);
211         
212         $result = '/' . $application->getId();
213         
214         if ($_type !== null) {
215             if (! in_array($_type, array(self::FOLDER_TYPE_SHARED, self::FOLDER_TYPE_PERSONAL,
216                     self::FOLDER_TYPE_RECORDS, self::FOLDER_TYPE_PREVIEWS))) {
217                 throw new Tinebase_Exception_UnexpectedValue('Type can only be shared or personal.');
218             }
219             
220             $result .= '/folders/' . $_type;
221         }
222         
223         return $result;
224     }
225     
226     /**
227      * Get one tree node (by id)
228      *
229      * @param integer|Tinebase_Record_Interface $_id
230      * @param boolean $_getDeleted get deleted records
231      * @param int|null $_revision
232      * @return Tinebase_Model_Tree_Node
233      */
234     public function get($_id, $_getDeleted = false, $_revision = null)
235     {
236         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
237         $treeBackend = $this->_getTreeNodeBackend();
238
239         try {
240             if (null !== $_revision) {
241                 $treeBackend->setRevision($_revision);
242             }
243             $node = $treeBackend->get($_id, $_getDeleted);
244         } finally {
245             if (null !== $_revision) {
246                 $treeBackend->setRevision(null);
247             }
248             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
249         }
250
251         return $node;
252     }
253
254     public function _getTreeNodeBackend()
255     {
256         if ($this->_treeNodeBackend === null) {
257             $this->_treeNodeBackend    = new Tinebase_Tree_Node(null, /* options */ array(
258                 'modelName' => $this->_treeNodeModel,
259                 Tinebase_Config::FILESYSTEM_ENABLE_NOTIFICATIONS => $this->_notificationActive,
260                 Tinebase_Config::FILESYSTEM_MODLOGACTIVE => $this->_modLogActive,
261             ));
262         }
263
264         return $this->_treeNodeBackend;
265     }
266
267     /**
268      * Get multiple tree nodes identified by id
269      *
270      * @param string|array $_id Ids
271      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
272      */
273     public function getMultipleTreeNodes($_id)
274     {
275         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
276         try {
277             return $this->_getTreeNodeBackend()->getMultiple($_id);
278         } finally {
279             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
280         }
281     }
282
283     /**
284      * create new node with acl
285      *
286      * @param string $path
287      * @param array|null $grants
288      * @return Tinebase_Model_Tree_Node
289      * @throws Tinebase_Exception_SystemGeneric
290      */
291     public function createAclNode($path, $grants = null)
292     {
293         $node = null;
294         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
295         try {
296             $pathRecord = Tinebase_Model_Tree_Node_Path::createFromPath($path);
297             if (true === $this->fileExists($pathRecord->statpath)) {
298                 // TODO always throw exception?
299                 throw new Tinebase_Exception_SystemGeneric('Node already exists');
300             }
301
302             // create folder node
303             $node = $this->mkdir($pathRecord->statpath);
304
305             if (null === $grants) {
306                 switch ($pathRecord->containerType) {
307                     case self::FOLDER_TYPE_PERSONAL:
308                         $node->grants = Tinebase_Model_Grants::getPersonalGrants($pathRecord->getUser(), array(
309                             Tinebase_Model_Grants::GRANT_DOWNLOAD => true,
310                             Tinebase_Model_Grants::GRANT_PUBLISH => true,
311                         ));
312                         break;
313                     case self::FOLDER_TYPE_SHARED:
314                         $node->grants = Tinebase_Model_Grants::getDefaultGrants(array(
315                             Tinebase_Model_Grants::GRANT_DOWNLOAD => true
316                         ), array(
317                             Tinebase_Model_Grants::GRANT_PUBLISH => true
318                         ));
319                         break;
320                 }
321             } else {
322                 $node->grants = $grants;
323             }
324
325             $this->_nodeAclController->setGrants($node);
326             $node->acl_node = $node->getId();
327             $this->update($node);
328
329             // append path for convenience
330             $node->path = $path;
331
332             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
333             $transactionId = null;
334         } finally {
335             if (null !== $transactionId) {
336                 Tinebase_TransactionManager::getInstance()->rollBack();
337             }
338         }
339
340         return $node;
341     }
342
343     /**
344      * set grants for node
345      *
346      * @param Tinebase_Model_Tree_Node $node
347      * @param                          $grants
348      * @return Tinebase_Model_Tree_Node
349      * @throws Timetracker_Exception_UnexpectedValue
350      * @throws Tinebase_Exception_Backend
351      *
352      * TODO check acl here?
353      */
354     public function setGrantsForNode(Tinebase_Model_Tree_Node $node, $grants)
355     {
356         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
357         try {
358             $node->grants = $grants;
359             $this->_nodeAclController->setGrants($node);
360             $node->acl_node = $node->getId();
361             $this->update($node);
362
363             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
364             $transactionId = null;
365
366             return $node;
367         } finally {
368             if (null !== $transactionId) {
369                 Tinebase_TransactionManager::getInstance()->rollBack();
370             }
371         }
372     }
373
374     /**
375      * remove acl from node (inherit acl from parent)
376      *
377      * @param Tinebase_Model_Tree_Node $node
378      * @return Tinebase_Model_Tree_Node
379      */
380     public function removeAclFromNode(Tinebase_Model_Tree_Node $node)
381     {
382         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
383         try {
384             $parentNode = $this->get($node->parent_id);
385             $node->acl_node = $parentNode->acl_node;
386             $this->update($node);
387             $this->_nodeAclController->deleteGrantsOfRecord($node->getId());
388
389             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
390             $transactionId = null;
391
392             return $node;
393         } finally {
394             if (null !== $transactionId) {
395                 Tinebase_TransactionManager::getInstance()->rollBack();
396             }
397         }
398     }
399
400     /**
401      * get contents of node
402      *
403      * @param string|Tinebase_Model_Tree_Node $nodeId
404      * @return string
405      */
406     public function getNodeContents($nodeId)
407     {
408         // getPathOfNode uses transactions and fills the stat cache, so fopen should fetch the node from the stat cache
409         // we do not start a transaction here
410         $path = $this->getPathOfNode($nodeId, /* $getPathAsString */ true);
411         $handle = $this->fopen($path, 'r');
412         $contents = stream_get_contents($handle);
413         $this->fclose($handle);
414
415         return $contents;
416     }
417     
418     /**
419      * clear stat cache
420      *
421      * @param string $path if given, only remove this path from statcache
422      */
423     public function clearStatCache($path = null)
424     {
425         if ($path !== null) {
426             unset($this->_statCache[$this->_getCacheId($path)]);
427         } else {
428             // clear the whole cache
429             $this->_statCache = array();
430         }
431     }
432     
433     /**
434      * copy file/directory
435      *
436      * @todo copy recursive
437      *
438      * @param  string  $sourcePath
439      * @param  string  $destinationPath
440      * @throws Tinebase_Exception_UnexpectedValue
441      * @return Tinebase_Model_Tree_Node
442      */
443     public function copy($sourcePath, $destinationPath)
444     {
445         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
446         try {
447             $destinationNode = $this->stat($sourcePath);
448             $sourcePathParts = $this->_splitPath($sourcePath);
449
450             try {
451                 // does destinationPath exist ...
452                 $parentNode = $this->stat($destinationPath);
453
454                 // ... and is a directory?
455                 if ($parentNode->type !== Tinebase_Model_Tree_FileObject::TYPE_FOLDER) {
456                     throw new Tinebase_Exception_UnexpectedValue
457                         ("Destination path exists and is a file. Please remove before.");
458                 }
459
460                 $destinationNodeName = basename(trim($sourcePath, '/'));
461                 $destinationPathParts = array_merge($this->_splitPath($destinationPath), (array)$destinationNodeName);
462             } catch (Tinebase_Exception_NotFound $tenf) {
463                 // does parent directory of destinationPath exist?
464                 try {
465                     $parentNode = $this->stat(dirname($destinationPath));
466                 } catch (Tinebase_Exception_NotFound $tenf) {
467                     throw new Tinebase_Exception_UnexpectedValue
468                         ("Parent directory does not exist. Please create before.");
469                 }
470
471                 $destinationNodeName = basename(trim($destinationPath, '/'));
472                 $destinationPathParts = array_merge($this->_splitPath(dirname($destinationPath)),
473                     (array)$destinationNodeName);
474             }
475
476             if ($sourcePathParts == $destinationPathParts) {
477                 throw new Tinebase_Exception_UnexpectedValue("Source path and destination path must be different.");
478             }
479
480             if (null !== ($deletedNode = $this->_getTreeNodeBackend()
481                     ->getChild($parentNode, $destinationNodeName, true, false)) && $deletedNode->is_deleted) {
482                 $this->_updateDeletedNodeName($deletedNode);
483             }
484
485             // set new node properties
486             $destinationNode->setId(null);
487             $destinationNode->parent_id = $parentNode->getId();
488             $destinationNode->name = $destinationNodeName;
489
490             $createdNode = $this->_getTreeNodeBackend()->create($destinationNode);
491
492             // update hash of all parent folders
493             $this->_updateDirectoryNodesHash(dirname(implode('/', $destinationPathParts)));
494
495             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
496             $transactionId = null;
497
498             return $createdNode;
499         } finally {
500             if (null !== $transactionId) {
501                 Tinebase_TransactionManager::getInstance()->rollBack();
502             }
503         }
504     }
505     
506     /**
507      * get modification timestamp
508      *
509      * @param  string  $path
510      * @return string  UNIX timestamp
511      */
512     public function getMTime($path)
513     {
514         $node = $this->stat($path);
515         
516         $timestamp = $node->last_modified_time instanceof Tinebase_DateTime
517             ? $node->last_modified_time->getTimestamp()
518             : $node->creation_time->getTimestamp();
519         
520         return $timestamp;
521     }
522     
523     /**
524      * check if file exists
525      *
526      * @param  string $path
527      * @param  integer|null $revision
528      * @return boolean true if file/directory exists
529      */
530     public function fileExists($path, $revision = null)
531     {
532         try {
533             $this->stat($path, $revision);
534         } catch (Tinebase_Exception_NotFound $tenf) {
535             return false;
536         }
537         
538         return true;
539     }
540     
541     /**
542      * close file handle
543      *
544      * @param  resource $handle
545      * @return boolean
546      */
547     public function fclose($handle)
548     {
549         if (!is_resource($handle)) {
550             return false;
551         }
552         
553         $options = stream_context_get_options($handle);
554         $this->_streamOptionsForNextOperation = array();
555
556         switch ($options['tine20']['mode']) {
557             case 'w':
558             case 'wb':
559             case 'x':
560             case 'xb':
561                 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
562                 try {
563                     $parentPath = dirname($options['tine20']['path']);
564
565                     list ($hash, $hashFile) = $this->createFileBlob($handle);
566
567                     $parentFolder = $this->stat($parentPath);
568
569                     $this->_updateFileObject($parentFolder, $options['tine20']['node']->object_id, $hash, $hashFile);
570
571                     $this->clearStatCache($options['tine20']['path']);
572
573                     $newNode = $this->stat($options['tine20']['path']);
574
575                     // write modlog and system notes
576                     $this->_getTreeNodeBackend()->updated($newNode, $options['tine20']['node']);
577
578                     // update hash of all parent folders
579                     $this->_updateDirectoryNodesHash($parentPath);
580
581                     Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
582                     $transactionId = null;
583
584                     Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Writing to file : ' .
585                         $options['tine20']['path'] . ' successful.');
586                 } finally {
587                     if (null !== $transactionId) {
588                         Tinebase_TransactionManager::getInstance()->rollBack();
589                     }
590                 }
591                 
592                 break;
593                 
594             default:
595                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Got mode : ' .
596                     $options['tine20']['mode'] . ' - nothing to do.');
597         }
598         
599         fclose($handle);
600
601         return true;
602     }
603
604     public function getRealPathForHash($_hash)
605     {
606         return $this->_basePath . '/' . substr($_hash, 0, 3) . '/' . substr($_hash, 3);
607     }
608
609     /**
610      * update file object with hash file info
611      *
612      * @param Tinebase_Model_Tree_Node $_parentNode
613      * @param string|Tinebase_Model_Tree_FileObject $_id file object (or id)
614      * @param string $_hash
615      * @param string $_hashFile
616      * @return Tinebase_Model_Tree_FileObject
617      */
618     protected function _updateFileObject(Tinebase_Model_Tree_Node $_parentNode, $_id, $_hash, $_hashFile = null)
619     {
620         /** @var Tinebase_Model_Tree_FileObject $currentFileObject */
621         $currentFileObject = $_id instanceof Tinebase_Record_Abstract ? $_id : $this->_fileObjectBackend->get($_id);
622
623         if (! $_hash) {
624             // use existing hash from file object
625             $_hash = $currentFileObject->hash;
626         }
627         $_hashFile = $_hashFile ?: ($this->getRealPathForHash($_hash));
628         
629         $updatedFileObject = clone($currentFileObject);
630         $updatedFileObject->hash = $_hash;
631
632         if (is_file($_hashFile)) {
633             $updatedFileObject->size = filesize($_hashFile);
634
635             if (function_exists('finfo_open')) {
636                 $finfo = finfo_open(FILEINFO_MIME_TYPE);
637                 $mimeType = finfo_file($finfo, $_hashFile);
638                 if ($mimeType !== false) {
639                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
640                         Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
641                             " Setting file contenttype to " . $mimeType);
642                     $updatedFileObject->contenttype = $mimeType;
643                 }
644                 finfo_close($finfo);
645             } else {
646                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
647                     . ' finfo_open() is not available: Could not get file information.');
648             }
649         } else {
650             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
651                 . ' File hash does not exist - directory?');
652         }
653         
654         $modLog = Tinebase_Timemachine_ModificationLog::getInstance();
655         $modLog->setRecordMetaData($updatedFileObject, 'update', $currentFileObject);
656
657         // quick hack for 2014.11 - will be resolved correctly in 2015.11-develop
658         if (isset($_SERVER['HTTP_X_OC_MTIME'])) {
659             $updatedFileObject->last_modified_time = new Tinebase_DateTime($_SERVER['HTTP_X_OC_MTIME']);
660             Tinebase_Server_WebDAV::getResponse()->setHeader('X-OC-MTime', 'accepted');
661             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " using X-OC-MTIME: {$updatedFileObject->last_modified_time->format(Tinebase_Record_Abstract::ISO8601LONG)} for {$updatedFileObject->id}");
662
663         }
664         
665         // sanitize file size, somehow filesize() seems to return empty strings on some systems
666         if (empty($updatedFileObject->size)) {
667             $updatedFileObject->size = 0;
668         }
669
670         $oldKeepRevisionValue = $this->_fileObjectBackend->getKeepOldRevision();
671         try {
672             if (isset($_parentNode->xprops(Tinebase_Model_Tree_Node::XPROPS_REVISION)[Tinebase_Model_Tree_Node::XPROPS_REVISION_ON])) {
673                 $this->_fileObjectBackend->setKeepOldRevision($_parentNode->xprops(Tinebase_Model_Tree_Node::XPROPS_REVISION)[Tinebase_Model_Tree_Node::XPROPS_REVISION_ON]);
674             }
675             /** @var Tinebase_Model_Tree_FileObject $newFileObject */
676             $newFileObject = $this->_fileObjectBackend->update($updatedFileObject);
677         } finally {
678             $this->_fileObjectBackend->setKeepOldRevision($oldKeepRevisionValue);
679         }
680
681         $sizeDiff = ((int)$newFileObject->size) - ((int)$currentFileObject->size);
682         $revisionSizeDiff = (((int)$currentFileObject->revision) === ((int)$newFileObject->revision) ? 0 : $newFileObject->revision_size);
683
684         if ($sizeDiff !== 0 || $revisionSizeDiff > 0) {
685             // update parents with new sizes
686             $this->_updateFolderSizesUpToRoot($this->_getTreeNodeBackend()->getObjectUsage($newFileObject->getId()), $sizeDiff, $revisionSizeDiff);
687         }
688
689         if (true === Tinebase_Config::getInstance()->get(Tinebase_Config::FILESYSTEM)->{Tinebase_Config::FILESYSTEM_INDEX_CONTENT}) {
690             Tinebase_ActionQueue::getInstance()->queueAction('Tinebase_FOO_FileSystem.indexFileObject', $newFileObject->getId());
691         }
692
693         return $newFileObject;
694     }
695
696     /**
697      * @param Tinebase_Record_RecordSet $_nodes
698      * @param int $_sizeDiff
699      * @param int $_revisionSizeDiff
700      */
701     protected function _updateFolderSizesUpToRoot(Tinebase_Record_RecordSet $_nodes, $_sizeDiff, $_revisionSizeDiff)
702     {
703         $objectIds = $this->_getTreeNodeBackend()->getAllFolderNodes($_nodes)->object_id;
704         if (!empty($objectIds)) {
705             /** @var Tinebase_Model_Tree_FileObject $fileObject */
706             foreach($this->_fileObjectBackend->getMultiple($objectIds) as $fileObject) {
707                 $fileObject->size = (int)$fileObject->size + (int)$_sizeDiff;
708                 if ($fileObject->size < 0) {
709                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
710                         . ' size should not become smaller than 0: ' . $fileObject->size . ' for object id: ' . $fileObject->getId());
711                     $fileObject->size = 0;
712                 }
713                 $fileObject->revision_size = (int)$fileObject->revision_size + (int)$_revisionSizeDiff;
714                 if ($fileObject->revision_size < 0) {
715                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
716                         . ' revision_size should not become smaller than 0: ' . $fileObject->size . ' for object id: ' . $fileObject->getId());
717                     $fileObject->revision_size = 0;
718                 }
719                 $this->_fileObjectBackend->update($fileObject, false);
720             }
721         }
722     }
723
724     /**
725      * @param string $_objectId
726      * @return bool
727      */
728     public function indexFileObject($_objectId)
729     {
730         /** @var Tinebase_Model_Tree_FileObject $fileObject */
731         try {
732             $fileObject = $this->_fileObjectBackend->get($_objectId);
733         } catch(Tinebase_Exception_NotFound $tenf) {
734             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
735                 . ' Could not find file object ' . $_objectId);
736             return true;
737         }
738         if (Tinebase_Model_Tree_FileObject::TYPE_FILE !== $fileObject->type) {
739             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
740                 . ' file object ' . $_objectId . ' is not a file: ' . $fileObject->type);
741             return true;
742         }
743         if ($fileObject->hash === $fileObject->indexed_hash) {
744             // nothing to do
745             return true;
746         }
747
748         // we clean up $tmpFile down there in finally
749         if (false === ($tmpFile = Tinebase_Fulltext_TextExtract::getInstance()->fileObjectToTempFile($fileObject))) {
750             return false;
751         }
752
753         $indexedHash = $fileObject->hash;
754
755         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
756
757         try {
758
759             try {
760                 $fileObject = $this->_fileObjectBackend->get($_objectId);
761             } catch(Tinebase_Exception_NotFound $tenf) {
762                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
763                     . ' Could not find file object ' . $_objectId);
764                 return true;
765             }
766             if (Tinebase_Model_Tree_FileObject::TYPE_FILE !== $fileObject->type) {
767                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
768                     . ' file object ' . $_objectId . ' is not a file: ' . $fileObject->type);
769                 return true;
770             }
771             if ($fileObject->hash === $fileObject->indexed_hash || $indexedHash === $fileObject->indexed_hash) {
772                 // nothing to do
773                 return true;
774             }
775
776             Tinebase_Fulltext_Indexer::getInstance()->addFileContentsToIndex($fileObject->getId(), $tmpFile);
777
778             $fileObject->indexed_hash = $indexedHash;
779             $this->_fileObjectBackend->update($fileObject);
780
781             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
782
783         } catch (Exception $e) {
784             Tinebase_Exception::log($e);
785             Tinebase_TransactionManager::getInstance()->rollBack();
786
787             return false;
788
789         } finally {
790             unlink($tmpFile);
791         }
792
793         return true;
794     }
795     
796     /**
797      * update hash of all directories for given path
798      * 
799      * @param string $path
800      */
801     protected function _updateDirectoryNodesHash($path)
802     {
803         // update hash of all parent folders
804         $parentNodes = $this->_getPathNodes($path);
805         $updatedNodes = $this->_fileObjectBackend->updateDirectoryNodesHash($parentNodes);
806         
807         // update nodes stored in local statCache
808         $subPath = null;
809         /** @var Tinebase_Model_Tree_Node $node */
810         foreach ($parentNodes as $node) {
811             /** @var Tinebase_Model_Tree_FileObject $directoryObject */
812             $directoryObject = $updatedNodes->getById($node->object_id);
813             
814             if ($directoryObject) {
815                 $node->revision             = $directoryObject->revision;
816                 $node->hash                 = $directoryObject->hash;
817                 $node->size                 = $directoryObject->size;
818                 $node->revision_size        = $directoryObject->revision_size;
819                 $node->available_revisions  = $directoryObject->available_revisions;
820             }
821             
822             $subPath .= "/" . $node->name;
823             $this->_addStatCache($subPath, $node);
824         }
825     }
826     
827     /**
828      * open file
829      * 
830      * @param string $_path
831      * @param string $_mode
832      * @param int|null $_revision
833      * @return resource|boolean
834      */
835     public function fopen($_path, $_mode, $_revision = null)
836     {
837         $dirName = dirname($_path);
838         $fileName = basename($_path);
839         $node = null;
840         $handle = null;
841         $fileType = isset($this->_streamOptionsForNextOperation[self::STREAM_OPTION_CREATE_PREVIEW]) && true === $this->_streamOptionsForNextOperation[self::STREAM_OPTION_CREATE_PREVIEW] ? Tinebase_Model_Tree_FileObject::TYPE_PREVIEW : Tinebase_Model_Tree_FileObject::TYPE_FILE;
842
843         $rollBack = true;
844         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
845         try {
846             switch ($_mode) {
847                 // Create and open for writing only; place the file pointer at the beginning of the file.
848                 // If the file already exists, the fopen() call will fail by returning false and generating
849                 // an error of level E_WARNING. If the file does not exist, attempt to create it. This is
850                 // equivalent to specifying O_EXCL|O_CREAT flags for the underlying open(2) system call.
851                 case 'x':
852                 case 'xb':
853                     if (!$this->isDir($dirName) || $this->fileExists($_path)) {
854                         $rollBack = false;
855                         return false;
856                     }
857
858                     $parent = $this->stat($dirName);
859                     $node = $this->createFileTreeNode($parent, $fileName, $fileType);
860
861                     $handle = Tinebase_TempFile::getInstance()->openTempFile();
862
863                     break;
864
865                 // Open for reading only; place the file pointer at the beginning of the file.
866                 case 'r':
867                 case 'rb':
868                     if ($this->isDir($_path) || !$this->fileExists($_path)) {
869                         $rollBack = false;
870                         return false;
871                     }
872
873                     $node = $this->stat($_path, $_revision);
874                     $hashFile = $this->getRealPathForHash($node->hash);
875
876                     $handle = fopen($hashFile, $_mode);
877
878                     break;
879
880                 // Open for writing only; place the file pointer at the beginning of the file and truncate the
881                 // file to zero length. If the file does not exist, attempt to create it.
882                 case 'w':
883                 case 'wb':
884                     if (!$this->isDir($dirName)) {
885                         $rollBack = false;
886                         return false;
887                     }
888
889                     if (!$this->fileExists($_path)) {
890                         $parent = $this->stat($dirName);
891                         $node = $this->createFileTreeNode($parent, $fileName, $fileType);
892                     } else {
893                         $node = $this->stat($_path, $_revision);
894                         if ($fileType !== $node->type) {
895                             $rollBack = false;
896                             return false;
897                         }
898                     }
899
900                     $handle = Tinebase_TempFile::getInstance()->openTempFile();
901
902                     break;
903
904                 default:
905                     $rollBack = false;
906                     return false;
907             }
908
909             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
910             $transactionId = null;
911         } finally {
912             if (null !== $transactionId) {
913                 if (true === $rollBack) {
914                     Tinebase_TransactionManager::getInstance()->rollBack();
915                 } else {
916                     Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
917                 }
918             }
919         }
920         
921         $contextOptions = array('tine20' => array(
922             'path' => $_path,
923             'mode' => $_mode,
924             'node' => $node
925         ));
926         stream_context_set_option($handle, $contextOptions);
927         
928         return $handle;
929     }
930     
931     /**
932      * get content type
933      * 
934      * @deprecated use Tinebase_FileSystem::stat()->contenttype
935      * @param  string  $path
936      * @return string
937      */
938     public function getContentType($path)
939     {
940         $node = $this->stat($path);
941         
942         return $node->contenttype;
943     }
944     
945     /**
946      * get etag
947      * 
948      * @deprecated use Tinebase_FileSystem::stat()->hash
949      * @param  string $path
950      * @return string
951      */
952     public function getETag($path)
953     {
954         $node = $this->stat($path);
955         
956         return $node->hash;
957     }
958     
959     /**
960      * return if path is a directory
961      * 
962      * @param  string  $path
963      * @return boolean
964      */
965     public function isDir($path)
966     {
967         try {
968             $node = $this->stat($path);
969         } catch (Tinebase_Exception_InvalidArgument $teia) {
970             return false;
971         } catch (Tinebase_Exception_NotFound $tenf) {
972             return false;
973         }
974         
975         if ($node->type !== Tinebase_Model_Tree_FileObject::TYPE_FOLDER) {
976             return false;
977         }
978         
979         return true;
980     }
981     
982     /**
983      * return if path is a file
984      *
985      * @param  string  $path
986      * @return boolean
987      */
988     public function isFile($path)
989     {
990         try {
991             $node = $this->stat($path);
992         } catch (Tinebase_Exception_InvalidArgument $teia) {
993             return false;
994         } catch (Tinebase_Exception_NotFound $tenf) {
995             return false;
996         }
997     
998         if ($node->type != Tinebase_Model_Tree_FileObject::TYPE_FILE) {
999             return false;
1000         }
1001     
1002         return true;
1003     }
1004     
1005     /**
1006      * rename file/directory
1007      *
1008      * @param  string  $oldPath
1009      * @param  string  $newPath
1010      * @return Tinebase_Model_Tree_Node|boolean
1011      */
1012     public function rename($oldPath, $newPath)
1013     {
1014         $transactionManager = Tinebase_TransactionManager::getInstance();
1015         $transactionId = $transactionManager->startTransaction(Tinebase_Core::getDb());
1016
1017         try {
1018             try {
1019                 $node = $this->stat($oldPath);
1020             } catch (Tinebase_Exception_InvalidArgument $teia) {
1021                 return false;
1022             } catch (Tinebase_Exception_NotFound $tenf) {
1023                 return false;
1024             }
1025
1026             if (dirname($oldPath) != dirname($newPath)) {
1027                 try {
1028                     $newParent = $this->stat(dirname($newPath));
1029                     $oldParent = $this->stat(dirname($oldPath));
1030                 } catch (Tinebase_Exception_InvalidArgument $teia) {
1031                     return false;
1032                 } catch (Tinebase_Exception_NotFound $tenf) {
1033                     return false;
1034                 }
1035
1036                 if ($node->acl_node === $oldParent->acl_node && $newParent->acl_node !== $node->acl_node) {
1037                     $node->acl_node = $newParent->acl_node;
1038                     if (Tinebase_Model_Tree_FileObject::TYPE_FOLDER === $node->type) {
1039                         $this->_recursiveInheritPropertyUpdate($node, 'acl_node', $newParent->acl_node, $oldParent->acl_node);
1040                     }
1041                 }
1042
1043                 if ($node->xprops(Tinebase_Model_Tree_Node::XPROPS_REVISION) == $oldParent->xprops(Tinebase_Model_Tree_Node::XPROPS_REVISION) &&
1044                     $node->xprops(Tinebase_Model_Tree_Node::XPROPS_REVISION) != $newParent->xprops(Tinebase_Model_Tree_Node::XPROPS_REVISION)
1045                 ) {
1046                     $node->{Tinebase_Model_Tree_Node::XPROPS_REVISION} = $newParent->{Tinebase_Model_Tree_Node::XPROPS_REVISION};
1047                     $oldValue = $oldParent->xprops(Tinebase_Model_Tree_Node::XPROPS_REVISION);
1048                     $newValue = $newParent->xprops(Tinebase_Model_Tree_Node::XPROPS_REVISION);
1049                     $oldValue = count($oldValue) > 0 ? json_encode($oldValue) : null;
1050                     $newValue = count($newValue) > 0 ? json_encode($newValue) : null;
1051                     if (null === $newValue) {
1052                         $node->{Tinebase_Model_Tree_Node::XPROPS_REVISION} = null;
1053                     }
1054                     // update revisionProps of subtree if changed
1055                     $this->_recursiveInheritPropertyUpdate($node, Tinebase_Model_Tree_Node::XPROPS_REVISION, $newValue, $oldValue, false);
1056                 }
1057
1058                 $node->parent_id = $newParent->getId();
1059
1060                 $this->_updateFolderSizesUpToRoot(new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array($oldParent)),
1061                     0 - (int)$node->size, 0 - (int)$node->revision_size);
1062                 $this->_updateFolderSizesUpToRoot(new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array($newParent)),
1063                     (int)$node->size, (int)$node->revision_size);
1064             }
1065
1066             if (basename($oldPath) != basename($newPath)) {
1067                 $node->name = basename($newPath);
1068             }
1069
1070             try {
1071                 $deletedNewPathNode = $this->stat($newPath, null, true);
1072                 $this->_updateDeletedNodeName($deletedNewPathNode);
1073             } catch (Tinebase_Exception_NotFound $tenf) {}
1074
1075             $node = $this->_getTreeNodeBackend()->update($node, true);
1076
1077             $transactionManager->commitTransaction($transactionId);
1078             $transactionId = null;
1079
1080             $this->clearStatCache($oldPath);
1081
1082             $this->_addStatCache($newPath, $node);
1083
1084             return $node;
1085
1086         } finally {
1087             if (null !== $transactionId) {
1088                 $transactionManager->rollBack();
1089             }
1090         }
1091     }
1092
1093     protected function _updateDeletedNodeName(Tinebase_Model_Tree_Node $_node)
1094     {
1095         $treeNodeBackend = $this->_getTreeNodeBackend();
1096         $parentId = $_node->parent_id;
1097         do {
1098             $id = uniqid();
1099             $name = $_node->name . $id;
1100             if (($len = mb_strlen($name)) > 255) {
1101                 $name = mb_substr($name, $len - 255);
1102             }
1103         } while (null !== $treeNodeBackend->getChild($parentId, $name, true, false));
1104         $_node->name = $name;
1105         $this->_getTreeNodeBackend()->update($_node, true);
1106     }
1107     
1108     /**
1109      * create directory
1110      * 
1111      * @param string $path
1112      * @return Tinebase_Model_Tree_Node
1113      */
1114     public function mkdir($path)
1115     {
1116         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1117             . ' Creating directory ' . $path);
1118         
1119         $currentPath = array();
1120         $parentNode  = null;
1121         $pathParts   = $this->_splitPath($path);
1122         $node = null;
1123
1124         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
1125         try {
1126             foreach ($pathParts as $pathPart) {
1127                 $pathPart = trim($pathPart);
1128                 $currentPath[] = $pathPart;
1129
1130                 try {
1131                     $node = $this->stat('/' . implode('/', $currentPath));
1132                 } catch (Tinebase_Exception_NotFound $tenf) {
1133                     $node = $this->createDirectoryTreeNode($parentNode, $pathPart);
1134
1135                     $this->_addStatCache($currentPath, $node);
1136                 }
1137
1138                 $parentNode = $node;
1139             }
1140
1141             // update hash of all parent folders
1142             $this->_updateDirectoryNodesHash($path);
1143
1144             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1145             $transactionId = null;
1146         } finally {
1147             if (null !== $transactionId) {
1148                 Tinebase_TransactionManager::getInstance()->rollBack();
1149             }
1150         }
1151         
1152         return $node;
1153     }
1154
1155     /**
1156      * remove directory
1157      *
1158      * @param  string $path
1159      * @param  boolean $recursive
1160      * @param  boolean $recursion
1161      * @return bool
1162      * @throws Tinebase_Exception_InvalidArgument
1163      */
1164     public function rmdir($path, $recursive = false, $recursion = false)
1165     {
1166         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
1167             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Removing directory ' . $path);
1168
1169         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
1170         try {
1171             $node = $this->stat($path);
1172
1173             $children = $this->getTreeNodeChildren($node);
1174
1175             // check if child entries exists and delete if $_recursive is true
1176             if (count($children) > 0) {
1177                 if ($recursive !== true) {
1178                     throw new Tinebase_Exception_InvalidArgument('directory not empty');
1179                 } else {
1180                     foreach ($children as $child) {
1181                         if ($this->isDir($path . '/' . $child->name)) {
1182                             $this->rmdir($path . '/' . $child->name, true, true);
1183                         } else {
1184                             $this->unlink($path . '/' . $child->name, true);
1185                         }
1186                     }
1187                 }
1188             }
1189
1190             if (false === $recursion) {
1191                 $this->_updateFolderSizesUpToRoot(new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array($node)),
1192                     0 - (int)$node->size, 0 - (int)$node->revision_size);
1193             }
1194             $this->_getTreeNodeBackend()->softDelete($node->getId());
1195             $this->clearStatCache($path);
1196
1197             // delete object only, if no other tree node refers to it
1198             // we can use treeNodeBackend property because getTreeNodeBackend was called just above
1199             if ($this->_treeNodeBackend->getObjectCount($node->object_id) == 0) {
1200                 $this->_fileObjectBackend->softDelete($node->object_id);
1201             }
1202
1203             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1204             $transactionId = null;
1205         } finally {
1206             if (null !== $transactionId) {
1207                 Tinebase_TransactionManager::getInstance()->rollBack();
1208             }
1209         }
1210         
1211         return true;
1212     }
1213     
1214     /**
1215      * scan dir
1216      * 
1217      * @param  string  $path
1218      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
1219      */
1220     public function scanDir($path)
1221     {
1222         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
1223         try {
1224             $children = $this->getTreeNodeChildren($this->stat($path));
1225
1226             foreach ($children as $node) {
1227                 $this->_addStatCache($path . '/' . $node->name, $node);
1228             }
1229
1230             return $children;
1231         } finally {
1232             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1233         }
1234     }
1235
1236     /**
1237      * return node for a path, caches found nodes in statcache
1238      *
1239      * @param  string  $path
1240      * @param  int|null $revision
1241      * @param  boolean $getDeleted
1242      * @return Tinebase_Model_Tree_Node
1243      * @throws Tinebase_Exception_NotFound
1244      */
1245     public function stat($path, $revision = null, $getDeleted = false)
1246     {
1247         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
1248
1249         try {
1250
1251             $pathParts = $this->_splitPath($path);
1252             // is pathParts[0] not an id (either 40 characters or only digits), then its an application name to resolve
1253             if (strlen($pathParts[0]) !== 40 && !ctype_digit($pathParts[0])) {
1254                 $oldPart = $pathParts[0];
1255                 $pathParts[0] = Tinebase_Application::getInstance()->getApplicationByName($pathParts[0])->getId();
1256                 // + 1 in mb_substr offset because of the leading / char
1257                 $path = '/' . $pathParts[0] . mb_substr('/' . ltrim($path, '/'), mb_strlen($oldPart) + 1);
1258             }
1259             $cacheId = $this->_getCacheId($pathParts, $revision);
1260
1261             // let's see if the path is cached in statCache
1262             if ((isset($this->_statCache[$cacheId]) || array_key_exists($cacheId, $this->_statCache))) {
1263                 try {
1264                     // let's try to get the node from backend, to make sure it still exists
1265                     $this->_getTreeNodeBackend()->setRevision($revision);
1266                     return $this->_checkRevision($this->_getTreeNodeBackend()->get($this->_statCache[$cacheId]), $revision);
1267                 } catch (Tinebase_Exception_NotFound $tenf) {
1268                     // something went wrong. let's clear the whole statCache
1269                     $this->clearStatCache();
1270                 } finally {
1271                     $this->_getTreeNodeBackend()->setRevision(null);
1272                 }
1273             }
1274
1275             $parentNode = null;
1276             $node       = null;
1277
1278             // find out if we have cached any node up in the path
1279             do {
1280                 $cacheId = $this->_getCacheId($pathParts);
1281
1282                 if ((isset($this->_statCache[$cacheId]) || array_key_exists($cacheId, $this->_statCache))) {
1283                     $node = $parentNode = $this->_statCache[$cacheId];
1284                     break;
1285                 }
1286             } while (($pathPart = array_pop($pathParts) !== null));
1287
1288             $missingPathParts = array_diff_assoc($this->_splitPath($path), $pathParts);
1289
1290             foreach ($missingPathParts as $pathPart) {
1291                 $node = $this->_getTreeNodeBackend()->getChild($parentNode, $pathPart, $getDeleted);
1292
1293                 if ($node->is_deleted && null !== $parentNode && $parentNode->is_deleted) {
1294                     throw new Tinebase_Exception_NotFound('cascading deleted nodes');
1295                 }
1296
1297                 // keep track of current path position
1298                 array_push($pathParts, $pathPart);
1299
1300                 // add found path to statCache
1301                 $this->_addStatCache($pathParts, $node);
1302
1303                 $parentNode = $node;
1304             }
1305
1306
1307
1308             if (null !== $revision) {
1309                 try {
1310                     $this->_getTreeNodeBackend()->setRevision($revision);
1311                     $node = $this->_checkRevision($this->_getTreeNodeBackend()->get($node->getId()), $revision);
1312
1313                     // add found path to statCache
1314                     $this->_addStatCache($pathParts, $node, $revision);
1315                 } finally {
1316                     $this->_getTreeNodeBackend()->setRevision(null);
1317                 }
1318             }
1319
1320             // TODO needed here?
1321             $node->path = $path;
1322
1323             return $node;
1324
1325         } finally {
1326             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1327         }
1328
1329         // just for PHPStorm Code Inspect
1330         return null;
1331     }
1332
1333     /**
1334      * @param Tinebase_Model_Tree_Node $_node
1335      * @param int|null $_revision
1336      * @return Tinebase_Model_Tree_Node
1337      * @throws Tinebase_Exception_NotFound
1338      */
1339     protected function _checkRevision(Tinebase_Model_Tree_Node $_node, $_revision)
1340     {
1341         if (null !== $_revision && empty($_node->hash)) {
1342             throw new Tinebase_Exception_NotFound('file does not have revision: ' . $_revision);
1343         }
1344
1345         return $_node;
1346     }
1347
1348     /**
1349      * get filesize
1350      * 
1351      * @deprecated use Tinebase_FileSystem::stat()->size
1352      * @param  string  $path
1353      * @return integer
1354      */
1355     public function filesize($path)
1356     {
1357         $node = $this->stat($path);
1358         
1359         return $node->size;
1360     }
1361     
1362     /**
1363      * delete file
1364      * 
1365      * @param  string  $path
1366      * @param  boolean $recursion
1367      * @return boolean
1368      */
1369     public function unlink($path, $recursion = false)
1370     {
1371         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
1372
1373         try {
1374             $node = $this->stat($path);
1375             $this->deleteFileNode($node, false === $recursion);
1376
1377             $this->clearStatCache($path);
1378
1379             // update hash of all parent folders
1380             $this->_updateDirectoryNodesHash(dirname($path));
1381
1382             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1383             $transactionId = null;
1384         } finally {
1385             if (null !== $transactionId) {
1386                 Tinebase_TransactionManager::getInstance()->rollBack();
1387             }
1388         }
1389
1390         return true;
1391     }
1392
1393     /**
1394      * delete file node
1395      *
1396      * @param Tinebase_Model_Tree_Node $node
1397      * @param bool $updateDirectoryNodesHash
1398      * @throws Tinebase_Exception_InvalidArgument
1399      */
1400     public function deleteFileNode(Tinebase_Model_Tree_Node $node, $updateDirectoryNodesHash = true)
1401     {
1402         if ($node->type === Tinebase_Model_Tree_FileObject::TYPE_FOLDER) {
1403             throw new Tinebase_Exception_InvalidArgument('can not unlink directories');
1404         }
1405
1406         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
1407         try {
1408
1409             if (true === $updateDirectoryNodesHash) {
1410                 $this->_updateFolderSizesUpToRoot(new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array($node)),
1411                     0 - (int)$node->size, 0 - (int)$node->revision_size);
1412
1413                 try {
1414                     $path = Tinebase_Model_Tree_Node_Path::createFromPath($this->getPathOfNode($node, true));
1415                     $this->_updateDirectoryNodesHash(dirname($path->statpath));
1416
1417                     // Tinebase_Model_Tree_Node_Path::_getContainerType may find that is not a personal or shared container (for example it may be a records container)
1418                 } catch (Tinebase_Exception_InvalidArgument $teia) {}
1419             }
1420
1421             $this->_getTreeNodeBackend()->softDelete($node->getId());
1422
1423             // delete object only, if no one uses it anymore
1424             // we can use treeNodeBackend property because getTreeNodeBackend was called just above
1425             if ($this->_treeNodeBackend->getObjectCount($node->object_id) === 0) {
1426                 if (false === $this->_modLogActive && true === $this->_previewActive) {
1427                     $hashes = $this->_fileObjectBackend->getHashes(array($node->object_id));
1428                 } else {
1429                     $hashes = array();
1430                 }
1431                 $this->_fileObjectBackend->softDelete($node->object_id);
1432                 if (false === $this->_modLogActive ) {
1433                     if (true === $this->_indexingActive) {
1434                         Tinebase_Fulltext_Indexer::getInstance()->removeFileContentsFromIndex($node->object_id);
1435                     }
1436                     if (true === $this->_previewActive) {
1437                         $existingHashes = $this->_fileObjectBackend->checkRevisions($hashes);
1438                         $hashesToDelete = array_diff($hashes, $existingHashes);
1439                         Tinebase_FileSystem_Previews::getInstance()->deletePreviews($hashesToDelete);
1440                     }
1441                 }
1442             }
1443
1444             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1445             $transactionId = null;
1446         } finally {
1447             if (null !== $transactionId) {
1448                 Tinebase_TransactionManager::getInstance()->rollBack();
1449             }
1450         }
1451     }
1452     
1453     /**
1454      * create directory
1455      * 
1456      * @param  string|Tinebase_Model_Tree_Node  $_parentId
1457      * @param  string                           $name
1458      * @return Tinebase_Model_Tree_Node
1459      */
1460     public function createDirectoryTreeNode($_parentId, $name)
1461     {
1462         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
1463         try {
1464             $parentId = $_parentId instanceof Tinebase_Model_Tree_Node ? $_parentId->getId() : $_parentId;
1465
1466             if (null !== ($deletedNode = $this->_getTreeNodeBackend()->getChild($parentId, $name, true, false)) &&
1467                     $deletedNode->is_deleted) {
1468                 $deletedNode->is_deleted = 0;
1469                 $object = $this->_fileObjectBackend->get($deletedNode->object_id, true);
1470                 if ($object->is_deleted) {
1471                     $object->is_deleted = 0;
1472                     $this->_fileObjectBackend->update($object);
1473                 }
1474                 //we can use _treeNodeBackend as we called get further up
1475                 $treeNode = $this->_treeNodeBackend->update($deletedNode);
1476             } else {
1477
1478                 $parentNode = $_parentId instanceof Tinebase_Model_Tree_Node
1479                     ? $_parentId
1480                     : ($_parentId ? $this->get($_parentId) : null);
1481
1482                 $directoryObject = new Tinebase_Model_Tree_FileObject(array(
1483                     'type' => Tinebase_Model_Tree_FileObject::TYPE_FOLDER,
1484                     'contentytype' => null,
1485                     'hash' => Tinebase_Record_Abstract::generateUID(),
1486                     'size' => 0
1487                 ));
1488                 Tinebase_Timemachine_ModificationLog::setRecordMetaData($directoryObject, 'create');
1489                 $directoryObject = $this->_fileObjectBackend->create($directoryObject);
1490
1491                 $treeNode = new Tinebase_Model_Tree_Node(array(
1492                     'name' => $name,
1493                     'object_id' => $directoryObject->getId(),
1494                     'parent_id' => $parentId,
1495                     'acl_node' => $parentNode && !empty($parentNode->acl_node) ? $parentNode->acl_node : null,
1496                     Tinebase_Model_Tree_Node::XPROPS_REVISION => $parentNode &&
1497                         !empty($parentNode->{Tinebase_Model_Tree_Node::XPROPS_REVISION}) ?
1498                         $parentNode->{Tinebase_Model_Tree_Node::XPROPS_REVISION} : null
1499                 ));
1500                 $treeNode = $this->_getTreeNodeBackend()->create($treeNode);
1501             }
1502
1503             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1504             $transactionId = null;
1505
1506             return $treeNode;
1507         } finally {
1508             if (null !== $transactionId) {
1509                 Tinebase_TransactionManager::getInstance()->rollBack();
1510             }
1511         }
1512     }
1513     
1514     /**
1515      * create new file node
1516      * 
1517      * @param  string|Tinebase_Model_Tree_Node  $_parentId
1518      * @param  string                           $_name
1519      * @param  string                           $_fileType
1520      * @throws Tinebase_Exception_InvalidArgument
1521      * @return Tinebase_Model_Tree_Node
1522      */
1523     public function createFileTreeNode($_parentId, $_name, $_fileType = Tinebase_Model_Tree_FileObject::TYPE_FILE)
1524     {
1525         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
1526         try {
1527             $parentId = $_parentId instanceof Tinebase_Model_Tree_Node ? $_parentId->getId() : $_parentId;
1528
1529             if (null !== ($deletedNode = $this->_getTreeNodeBackend()->getChild($parentId, $_name, true, false)) &&
1530                     $deletedNode->is_deleted) {
1531                 $deletedNode->is_deleted = 0;
1532                 /** @var Tinebase_Model_Tree_FileObject $object */
1533                 $object = $this->_fileObjectBackend->get($deletedNode->object_id, true);
1534                 if (isset($_SERVER['HTTP_X_OC_MTIME'])) {
1535                     $object->creation_time = new Tinebase_DateTime($_SERVER['HTTP_X_OC_MTIME']);
1536                     $object->last_modified_time = new Tinebase_DateTime($_SERVER['HTTP_X_OC_MTIME']);
1537                     Tinebase_Server_WebDAV::getResponse()->setHeader('X-OC-MTime', 'accepted');
1538                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
1539                         Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " using X-OC-MTIME: {$object->last_modified_time->format(Tinebase_Record_Abstract::ISO8601LONG)} for {$_name}");
1540                     }
1541                 }
1542                 $object->hash = Tinebase_Record_Abstract::generateUID();
1543                 $object->size = 0;
1544                 $object->is_deleted = 0;
1545                 $object->type = $_fileType;
1546                 $object->preview_count = 0;
1547                 $this->_fileObjectBackend->update($object);
1548                 //we can use _treeNodeBackend as we called get further up
1549                 $treeNode = $this->_treeNodeBackend->update($deletedNode);
1550             } else {
1551
1552                 $parentNode = $_parentId instanceof Tinebase_Model_Tree_Node ? $_parentId : $this->get($parentId);
1553
1554                 $fileObject = new Tinebase_Model_Tree_FileObject(array(
1555                     'type' => $_fileType,
1556                     'contentytype' => null,
1557                 ));
1558                 Tinebase_Timemachine_ModificationLog::setRecordMetaData($fileObject, 'create');
1559
1560                 // quick hack for 2014.11 - will be resolved correctly in 2015.11-develop
1561                 if (isset($_SERVER['HTTP_X_OC_MTIME'])) {
1562                     $fileObject->creation_time = new Tinebase_DateTime($_SERVER['HTTP_X_OC_MTIME']);
1563                     $fileObject->last_modified_time = new Tinebase_DateTime($_SERVER['HTTP_X_OC_MTIME']);
1564                     Tinebase_Server_WebDAV::getResponse()->setHeader('X-OC-MTime', 'accepted');
1565                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
1566                         Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " using X-OC-MTIME: {$fileObject->last_modified_time->format(Tinebase_Record_Abstract::ISO8601LONG)} for {$_name}");
1567                     }
1568
1569                 }
1570
1571                 $fileObject = $this->_fileObjectBackend->create($fileObject);
1572
1573                 $treeNode = new Tinebase_Model_Tree_Node(array(
1574                     'name' => $_name,
1575                     'object_id' => $fileObject->getId(),
1576                     'parent_id' => $parentId,
1577                     'acl_node' => $parentNode && empty($parentNode->acl_node) ? null : $parentNode->acl_node,
1578                 ));
1579
1580                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) {
1581                     Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
1582                         ' ' . print_r($treeNode->toArray(), true));
1583                 }
1584
1585                 $treeNode = $this->_getTreeNodeBackend()->create($treeNode);
1586             }
1587
1588             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1589             $transactionId = null;
1590
1591             return $treeNode;
1592         } finally {
1593             if (null !== $transactionId) {
1594                 Tinebase_TransactionManager::getInstance()->rollBack();
1595             }
1596         }
1597     }
1598
1599     /**
1600      * places contents into a file blob
1601      * 
1602      * @param  resource $contents
1603      * @return string hash
1604      * @throws Tinebase_Exception_NotImplemented
1605      */
1606     public function createFileBlob($contents)
1607     {
1608         if (! is_resource($contents)) {
1609             throw new Tinebase_Exception_NotImplemented('please implement me!');
1610         }
1611         
1612         $handle = $contents;
1613         rewind($handle);
1614         
1615         $ctx = hash_init('sha1');
1616         hash_update_stream($ctx, $handle);
1617         $hash = hash_final($ctx);
1618         
1619         $hashDirectory = $this->_basePath . '/' . substr($hash, 0, 3);
1620         if (!file_exists($hashDirectory)) {
1621             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' create hash directory: ' . $hashDirectory);
1622             if(mkdir($hashDirectory, 0700) === false) {
1623                 throw new Tinebase_Exception_UnexpectedValue('failed to create directory');
1624             }
1625         }
1626         
1627         $hashFile      = $hashDirectory . '/' . substr($hash, 3);
1628         if (!file_exists($hashFile)) {
1629             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' create hash file: ' . $hashFile);
1630             rewind($handle);
1631             $hashHandle = fopen($hashFile, 'x');
1632             stream_copy_to_stream($handle, $hashHandle);
1633             fclose($hashHandle);
1634         }
1635         
1636         return array($hash, $hashFile);
1637     }
1638     
1639     /**
1640      * get tree node children
1641      * 
1642      * @param string|Tinebase_Model_Tree_Node|Tinebase_Record_RecordSet  $nodeId
1643      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
1644      *
1645      * TODO always ignore acl here?
1646      */
1647     public function getTreeNodeChildren($nodeId)
1648     {
1649         if ($nodeId instanceof Tinebase_Model_Tree_Node) {
1650             $nodeId = $nodeId->getId();
1651             $operator = 'equals';
1652         } elseif ($nodeId instanceof Tinebase_Record_RecordSet) {
1653             $nodeId = $nodeId->getArrayOfIds();
1654             $operator = 'in';
1655         } else {
1656             $operator = 'equals';
1657         }
1658         
1659         $searchFilter = new Tinebase_Model_Tree_Node_Filter(array(
1660             array(
1661                 'field'     => 'parent_id',
1662                 'operator'  => $operator,
1663                 'value'     => $nodeId
1664             )
1665         ), Tinebase_Model_Filter_FilterGroup::CONDITION_AND, array('ignoreAcl' => true));
1666         $children = $this->searchNodes($searchFilter);
1667         
1668         return $children;
1669     }
1670     
1671     /**
1672      * search tree nodes
1673      * 
1674      * @param Tinebase_Model_Tree_Node_Filter $_filter
1675      * @param Tinebase_Record_Interface $_pagination
1676      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
1677      */
1678     public function searchNodes(Tinebase_Model_Tree_Node_Filter $_filter = null, Tinebase_Record_Interface $_pagination = null)
1679     {
1680         $result = $this->_getTreeNodeBackend()->search($_filter, $_pagination);
1681         return $result;
1682     }
1683
1684     /**
1685      * search tree nodes
1686      *
1687      * TODO replace searchNodes / or refactor this - tree objects has no search function yet / might be ambiguous...
1688      *
1689      * @param Tinebase_Model_Tree_Node_Filter $_filter
1690      * @param Tinebase_Record_Interface $_pagination
1691      * @param boolean $_onlyIds
1692      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
1693      */
1694     public function search(Tinebase_Model_Tree_Node_Filter $_filter = null, Tinebase_Record_Interface $_pagination = null, $_onlyIds = false)
1695     {
1696         $result = $this->_getTreeNodeBackend()->search($_filter, $_pagination, $_onlyIds);
1697         return $result;
1698     }
1699
1700     /**
1701     * search tree nodes count
1702     *
1703     * @param Tinebase_Model_Tree_Node_Filter $_filter
1704     * @return integer
1705     */
1706     public function searchNodesCount(Tinebase_Model_Tree_Node_Filter $_filter = null)
1707     {
1708         $result = $this->_getTreeNodeBackend()->searchCount($_filter);
1709         return $result;
1710     }
1711
1712     /**
1713      * get tree node specified by parent node (or id) and name
1714      * 
1715      * @param string|Tinebase_Model_Tree_Node $_parentId
1716      * @param string $_name
1717      * @throws Tinebase_Exception_InvalidArgument
1718      * @return Tinebase_Model_Tree_Node
1719      */
1720     public function getTreeNode($_parentId, $_name)
1721     {
1722         $parentId = $_parentId instanceof Tinebase_Model_Tree_Node ? $_parentId->getId() : $_parentId;
1723         
1724         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
1725             . ' Getting tree node ' . $parentId . '/'. $_name);
1726         
1727         return$this->_getTreeNodeBackend()->getChild($_parentId, $_name);
1728     }
1729     
1730     /**
1731      * add entry to stat cache
1732      * 
1733      * @param string|array              $path
1734      * @param Tinebase_Model_Tree_Node  $node
1735      * @param int|null                  $revision
1736      */
1737     protected function _addStatCache($path, Tinebase_Model_Tree_Node $node, $revision = null)
1738     {
1739         $this->_statCache[$this->_getCacheId($path, $revision)] = $node;
1740     }
1741     
1742     /**
1743      * generate cache id
1744      * 
1745      * @param  string|array  $path
1746      * @param  int|null $revision
1747      * @return string
1748      */
1749     protected function _getCacheId($path, $revision = null)
1750     {
1751         $pathParts = is_array($path) ? $path : $this->_splitPath($path);
1752         array_unshift($pathParts, '@' . $revision);
1753
1754         return sha1(implode(null, $pathParts));
1755     }
1756     
1757     /**
1758      * split path
1759      * 
1760      * @param  string  $path
1761      * @return array
1762      */
1763     protected function _splitPath($path)
1764     {
1765         return explode('/', trim($path, '/'));
1766     }
1767     
1768     /**
1769      * update node
1770      * 
1771      * @param Tinebase_Model_Tree_Node $_node
1772      * @return Tinebase_Model_Tree_Node
1773      */
1774     public function update(Tinebase_Model_Tree_Node $_node)
1775     {
1776         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
1777
1778         try {
1779             $currentNodeObject = $this->get($_node->getId());
1780             $fileObject = $this->_fileObjectBackend->get($currentNodeObject->object_id);
1781
1782             Tinebase_Timemachine_ModificationLog::setRecordMetaData($_node, 'update', $currentNodeObject);
1783             Tinebase_Timemachine_ModificationLog::setRecordMetaData($fileObject, 'update', $fileObject);
1784
1785             // quick hack for 2014.11 - will be resolved correctly in 2016.11-develop?
1786             if (isset($_SERVER['HTTP_X_OC_MTIME'])) {
1787                 $fileObject->last_modified_time = new Tinebase_DateTime($_SERVER['HTTP_X_OC_MTIME']);
1788                 Tinebase_Server_WebDAV::getResponse()->setHeader('X-OC-MTime', 'accepted');
1789                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
1790                     Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " using X-OC-MTIME: {$fileObject->last_modified_time->format(Tinebase_Record_Abstract::ISO8601LONG)} for {$_node->name}");
1791
1792             }
1793
1794             // update file object
1795             $fileObject->description = $_node->description;
1796             $this->_updateFileObject($this->get($currentNodeObject->parent_id), $fileObject, $_node->hash);
1797
1798             if ($currentNodeObject->acl_node !== $_node->acl_node) {
1799                 // update acl_node of subtree if changed
1800                 $this->_recursiveInheritPropertyUpdate($_node, 'acl_node', $_node->acl_node, $currentNodeObject->acl_node);
1801             }
1802
1803             $oldValue = $currentNodeObject->xprops(Tinebase_Model_Tree_Node::XPROPS_REVISION);
1804             $newValue = $_node->xprops(Tinebase_Model_Tree_Node::XPROPS_REVISION);
1805             if (!empty($newValue) && !isset($newValue[Tinebase_Model_Tree_Node::XPROPS_REVISION_NODE_ID])) {
1806                 $newValue[Tinebase_Model_Tree_Node::XPROPS_REVISION_NODE_ID] = $_node->getId();
1807             }
1808
1809             if ($oldValue != $newValue) {
1810                 $oldValue = count($oldValue) > 0 ? json_encode($oldValue) : null;
1811                 $newValue = count($newValue) > 0 ? json_encode($newValue) : null;
1812
1813                 // update revisionProps of subtree if changed
1814                 $this->_recursiveInheritPropertyUpdate($_node, Tinebase_Model_Tree_Node::XPROPS_REVISION, $newValue, $oldValue, false);
1815             }
1816
1817             /** @var Tinebase_Model_Tree_Node $newNode */
1818             $newNode = $this->_getTreeNodeBackend()->update($_node, false);
1819
1820             if (isset($_node->grants)) {
1821                 $newNode->grants = $_node->grants;
1822             }
1823
1824             $this->_getTreeNodeBackend()->updated($newNode, $currentNodeObject);
1825
1826             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1827             $transactionId = null;
1828
1829         } finally {
1830             if (null !== $transactionId) {
1831                 Tinebase_TransactionManager::getInstance()->rollBack();
1832             }
1833         }
1834
1835         return $newNode;
1836     }
1837
1838     /**
1839      * @param Tinebase_Model_Tree_Node $_node
1840      * @param string $_property
1841      * @param string $_newValue
1842      * @param string $_oldValue
1843      * @param bool   $_ignoreACL
1844      */
1845     protected function _recursiveInheritPropertyUpdate(Tinebase_Model_Tree_Node $_node, $_property, $_newValue, $_oldValue, $_ignoreACL = true)
1846     {
1847         $childIds = $this->getAllChildIds(array($_node->getId()), array(array(
1848             'field'     => $_property,
1849             'operator'  => 'equals',
1850             'value'     => $_oldValue
1851         )), $_ignoreACL);
1852         if (count($childIds) > 0) {
1853             $this->_getTreeNodeBackend()->updateMultiple($childIds, array($_property => $_newValue));
1854         }
1855     }
1856
1857     /**
1858      * @param Tinebase_Model_Tree_Node $_node
1859      * @param string $_property
1860      * @param string $_newValue
1861      * @param string $_oldValue
1862      * @param bool  $_ignoreACL
1863      */
1864     protected function _recursiveInheritFolderPropertyUpdate(Tinebase_Model_Tree_Node $_node, $_property, $_newValue, $_oldValue, $_ignoreACL = true)
1865     {
1866         $childIds = $this->getAllChildIds(array($_node->getId()), array(
1867             array(
1868                 'field'     => $_property,
1869                 'operator'  => 'equals',
1870                 'value'     => $_oldValue
1871             ),
1872             array(
1873                 'field'     => 'type',
1874                 'operator'  => 'equals',
1875                 'value'     => Tinebase_Model_Tree_FileObject::TYPE_FOLDER
1876             )
1877         ), $_ignoreACL, (true === $_ignoreACL ? null : array(Tinebase_Model_PersistentFilterGrant::GRANT_EDIT)));
1878         if (count($childIds) > 0) {
1879             $this->_getTreeNodeBackend()->updateMultiple($childIds, array($_property => $_newValue));
1880         }
1881     }
1882
1883     /**
1884      * returns all children nodes, allows to set addition filters
1885      *
1886      * @param array         $_ids
1887      * @param array         $_additionalFilters
1888      * @param bool          $_ignoreAcl
1889      * @param array|null    $_requiredGrants
1890      * @return array
1891      */
1892     public function getAllChildIds(array $_ids, array $_additionalFilters = array(), $_ignoreAcl = true, $_requiredGrants = null)
1893     {
1894         $result = array();
1895         $filter = array(
1896             array(
1897                 'field'     => 'parent_id',
1898                 'operator'  => 'in',
1899                 'value'     => $_ids
1900             )
1901         );
1902         foreach($_additionalFilters as $aF) {
1903             $filter[] = $aF;
1904         }
1905         $searchFilter = new Tinebase_Model_Tree_Node_Filter($filter,  /* $_condition = */ '', /* $_options */ array(
1906             'ignoreAcl' => $_ignoreAcl,
1907         ));
1908         if (null !== $_requiredGrants) {
1909             $searchFilter->setRequiredGrants($_requiredGrants);
1910         }
1911         $children = $this->search($searchFilter, null, true);
1912         if (count($children) > 0) {
1913             $result = array_merge($result, $children, $this->getAllChildIds($children, $_additionalFilters, $_ignoreAcl));
1914         }
1915
1916         return $result;
1917     }
1918
1919     /**
1920      * get path of node
1921      * 
1922      * @param Tinebase_Model_Tree_Node|string $node
1923      * @param boolean $getPathAsString
1924      * @return array|string
1925      */
1926     public function getPathOfNode($node, $getPathAsString = false)
1927     {
1928         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
1929         try {
1930             $node = $node instanceof Tinebase_Model_Tree_Node ? $node : $this->get($node);
1931
1932             $nodesPath = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array($node));
1933             while ($node->parent_id) {
1934                 $node = $this->get($node->parent_id);
1935                 $nodesPath->addRecord($node);
1936             }
1937
1938             $result = ($getPathAsString) ? '/' . implode('/', array_reverse($nodesPath->name)) : array_reverse($nodesPath->toArray());
1939             return $result;
1940         } finally {
1941             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1942         }
1943     }
1944     
1945     protected function _getPathNodes($path)
1946     {
1947         $pathParts = $this->_splitPath($path);
1948         
1949         if (empty($pathParts)) {
1950             throw new Tinebase_Exception_InvalidArgument('empty path provided');
1951         }
1952         
1953         $subPath   = null;
1954         $pathNodes = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
1955         
1956         foreach ($pathParts as $pathPart) {
1957             $subPath .= "/$pathPart"; 
1958             
1959             $node = $this->stat($subPath);
1960             if ($node) {
1961                 $pathNodes->addRecord($node);
1962             }
1963         }
1964         
1965         return $pathNodes;
1966     }
1967     
1968     /**
1969      * clears deleted files from filesystem + database
1970      */
1971     public function clearDeletedFiles()
1972     {
1973         $this->clearDeletedFilesFromFilesystem();
1974         $this->clearDeletedFilesFromDatabase();
1975     }
1976
1977     /**
1978      * removes deleted files that no longer exist in the database from the filesystem
1979      * @return int number of deleted files
1980      * @throws Tinebase_Exception_AccessDenied
1981      */
1982     public function clearDeletedFilesFromFilesystem()
1983     {
1984         try {
1985             $dirIterator = new DirectoryIterator($this->_basePath);
1986         } catch (Exception $e) {
1987             throw new Tinebase_Exception_AccessDenied('Could not open files directory.');
1988         }
1989         
1990         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1991             . ' Scanning ' . $this->_basePath . ' for deleted files ...');
1992         
1993         $deleteCount = 0;
1994         /** @var DirectoryIterator $item */
1995         foreach ($dirIterator as $item) {
1996             if (!$item->isDir()) {
1997                 continue;
1998             }
1999             $subDir = $item->getFilename();
2000             if ($subDir[0] == '.') continue;
2001             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
2002                 . ' Checking ' . $subDir);
2003             $subDirIterator = new DirectoryIterator($this->_basePath . '/' . $subDir);
2004             $hashsToCheck = array();
2005             // loop dirs + check if files in dir are in tree_filerevisions
2006             /** @var DirectoryIterator $file */
2007             foreach ($subDirIterator as $file) {
2008                 if ($file->isFile()) {
2009                     $hash = $subDir . $file->getFilename();
2010                     $hashsToCheck[] = $hash;
2011                 }
2012             }
2013             $existingHashes = $this->_fileObjectBackend->checkRevisions($hashsToCheck);
2014             $hashesToDelete = array_diff($hashsToCheck, $existingHashes);
2015             // remove from filesystem if not existing any more
2016             foreach ($hashesToDelete as $hashToDelete) {
2017                 $filename = $this->_basePath . '/' . $subDir . '/' . substr($hashToDelete, 3);
2018                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
2019                     . ' Deleting ' . $filename);
2020                 unlink($filename);
2021                 $deleteCount++;
2022             }
2023         }
2024         
2025         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2026             . ' Deleted ' . $deleteCount . ' obsolete file(s).');
2027         
2028         return $deleteCount;
2029     }
2030     
2031     /**
2032      * removes deleted files that no longer exist in the filesystem from the database
2033      * 
2034      * @return integer number of deleted files
2035      */
2036     public function clearDeletedFilesFromDatabase()
2037     {
2038         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2039             . ' Scanning database for deleted files ...');
2040
2041         // get all file objects from db and check filesystem existance
2042         $filter = new Tinebase_Model_Tree_FileObjectFilter();
2043         $start = 0;
2044         $limit = 500;
2045         $toDeleteIds = array();
2046
2047         do {
2048             $pagination = new Tinebase_Model_Pagination(array(
2049                 'start' => $start,
2050                 'limit' => $limit,
2051                 'sort' => 'id',
2052             ));
2053
2054             /** @var Tinebase_Record_RecordSet $fileObjects */
2055             $fileObjects = $this->_fileObjectBackend->search($filter, $pagination);
2056             /** @var Tinebase_Model_Tree_FileObject $fileObject */
2057             foreach ($fileObjects as $fileObject) {
2058                 if (($fileObject->type === Tinebase_Model_Tree_FileObject::TYPE_FILE || $fileObject->type === Tinebase_Model_Tree_FileObject::TYPE_PREVIEW)
2059                         && $fileObject->hash && !file_exists($fileObject->getFilesystemPath())) {
2060                     $toDeleteIds[] = $fileObject->getId();
2061                 }
2062             }
2063
2064             $start += $limit;
2065         } while ($fileObjects->count() >= $limit);
2066
2067         if (count($toDeleteIds) === 0) {
2068             return 0;
2069         }
2070
2071         $nodeIdsToDelete = $this->_getTreeNodeBackend()->search(
2072             new Tinebase_Model_Tree_Node_Filter(array(array(
2073                 'field'     => 'object_id',
2074                 'operator'  => 'in',
2075                 'value'     => $toDeleteIds
2076             )), /* $_condition = */ '',
2077                 /* $_options */ array(
2078                     'ignoreAcl' => true,
2079                 )
2080             ),
2081             null,
2082             Tinebase_Backend_Sql_Abstract::IDCOL
2083         );
2084
2085         // hard delete is ok here
2086         $deleteCount = $this->_getTreeNodeBackend()->delete($nodeIdsToDelete);
2087         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2088             . ' Removed ' . $deleteCount . ' obsolete filenode(s) from the database.');
2089
2090         if (true === $this->_previewActive) {
2091             $hashes = $this->_fileObjectBackend->getHashes($toDeleteIds);
2092         } else {
2093             $hashes = array();
2094         }
2095
2096         // hard delete is ok here
2097         $this->_fileObjectBackend->delete($toDeleteIds);
2098         if (true === $this->_indexingActive) {
2099             Tinebase_Fulltext_Indexer::getInstance()->removeFileContentsFromIndex($toDeleteIds);
2100         }
2101
2102         if (true === $this->_previewActive && count($hashes) > 0) {
2103             $existingHashes = $this->_fileObjectBackend->checkRevisions($hashes);
2104             $hashesToDelete = array_diff($hashes, $existingHashes);
2105             Tinebase_FileSystem_Previews::getInstance()->deletePreviews($hashesToDelete);
2106         }
2107
2108         return $deleteCount;
2109     }
2110
2111     /**
2112      * copy tempfile data to file path
2113      * 
2114      * @param  mixed   $tempFile
2115          Tinebase_Model_Tree_Node     with property hash, tempfile or stream
2116          Tinebase_Model_Tempfile      tempfile
2117          string                       with tempFile id
2118          array                        with [id] => tempFile id (this is odd IMHO)
2119          stream                       stream ressource
2120          null                         create empty file
2121      * @param  string  $path
2122      * @return Tinebase_Model_Tree_Node
2123      * @throws Tinebase_Exception_AccessDenied
2124      */
2125     public function copyTempfile($tempFile, $path)
2126     {
2127         if ($tempFile === null) {
2128             $tempStream = fopen('php://memory', 'r');
2129         } else if (is_resource($tempFile)) {
2130             $tempStream = $tempFile;
2131         } else if (is_string($tempFile) || is_array($tempFile)) {
2132             $tempFile = Tinebase_TempFile::getInstance()->getTempFile($tempFile);
2133             return $this->copyTempfile($tempFile, $path);
2134         } else if ($tempFile instanceof Tinebase_Model_Tree_Node) {
2135             if (isset($tempFile->hash)) {
2136                 $hashFile = $this->getRealPathForHash($tempFile->hash);
2137                 $tempStream = fopen($hashFile, 'r');
2138             } else if (is_resource($tempFile->stream)) {
2139                 $tempStream = $tempFile->stream;
2140             } else {
2141                 return $this->copyTempfile($tempFile->tempFile, $path);
2142             }
2143         } else if ($tempFile instanceof Tinebase_Model_TempFile) {
2144             $tempStream = fopen($tempFile->path, 'r');
2145         } else {
2146             throw new Tinebase_Exception_UnexpectedValue('unexpected tempfile value');
2147         }
2148         
2149         $this->copyStream($tempStream, $path);
2150
2151         // TODO revision properties need to be inherited
2152
2153         $node = $this->setAclFromParent($path);
2154
2155         return $node;
2156     }
2157
2158     public function setAclFromParent($path)
2159     {
2160         $node = $this->stat($path);
2161         $parent = $this->get($node->parent_id);
2162         $node->acl_node = $parent->acl_node;
2163         $this->update($node);
2164
2165         return $node;
2166     }
2167     
2168     /**
2169      * copy stream data to file path
2170      *
2171      * @param  resource  $in
2172      * @param  string  $path
2173      * @throws Tinebase_Exception_AccessDenied
2174      * @throws Tinebase_Exception_UnexpectedValue
2175      */
2176     public function copyStream($in, $path)
2177     {
2178         if (! $handle = $this->fopen($path, 'w')) {
2179             throw new Tinebase_Exception_AccessDenied('Permission denied to create file (filename ' . $path . ')');
2180         }
2181         
2182         if (! is_resource($in)) {
2183             throw new Tinebase_Exception_UnexpectedValue('source needs to be of type stream');
2184         }
2185         
2186         if (is_resource($in) !== null) {
2187             $metaData = stream_get_meta_data($in);
2188             if (true === $metaData['seekable']) {
2189                 rewind($in);
2190             }
2191             stream_copy_to_stream($in, $handle);
2192             
2193             $this->clearStatCache($path);
2194         }
2195         
2196         $this->fclose($handle);
2197     }
2198
2199     /**
2200      * recalculates all revision sizes of file objects of type file only
2201      *
2202      * on error it still continues and tries to calculate as many revision sizes as possible, but returns false
2203      *
2204      * @return bool
2205      */
2206     public function recalculateRevisionSize()
2207     {
2208         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2209             . ' starting to recalculate revision size');
2210         return $this->_fileObjectBackend->recalculateRevisionSize();
2211     }
2212
2213     /**
2214      * recalculates all folder sizes
2215      *
2216      * on error it still continues and tries to calculate as many folder sizes as possible, but returns false
2217      *
2218      * @return bool
2219      */
2220     public function recalculateFolderSize()
2221     {
2222         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2223             . ' starting to recalculate folder size');
2224         return $this->_getTreeNodeBackend()->recalculateFolderSize($this->_fileObjectBackend);
2225     }
2226
2227     /**
2228      * indexes all not indexed file objects
2229      *
2230      * on error it still continues and tries to index as many file objects as possible, but returns false
2231      *
2232      * @return bool
2233      */
2234     public function checkIndexing()
2235     {
2236         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2237             . ' starting to check indexing');
2238
2239         if (false === $this->_indexingActive) {
2240             return true;
2241         }
2242
2243         $success = true;
2244         foreach($this->_fileObjectBackend->getNotIndexedObjectIds() as $objectId) {
2245             $success = $this->indexFileObject($objectId) && $success;
2246         }
2247
2248         return $success;
2249     }
2250
2251     /**
2252      * check acl of path
2253      *
2254      * @param Tinebase_Model_Tree_Node_Path $_path
2255      * @param string $_action
2256      * @param boolean $_topLevelAllowed
2257      * @throws Tinebase_Exception_AccessDenied
2258      */
2259     public function checkPathACL(Tinebase_Model_Tree_Node_Path $_path, $_action = 'get', /** @noinspection PhpUnusedParameterInspection */ $_topLevelAllowed = true)
2260     {
2261         switch ($_path->containerType) {
2262             case Tinebase_FileSystem::FOLDER_TYPE_PERSONAL:
2263                 if ($_path->containerOwner && ($_topLevelAllowed || ! $_path->isToplevelPath())) {
2264                     $hasPermission = ($_path->containerOwner === Tinebase_Core::getUser()->accountLoginName || $_action === 'get');
2265                 } else {
2266                     $hasPermission = ($_action === 'get');
2267                 }
2268                 break;
2269             case Tinebase_FileSystem::FOLDER_TYPE_SHARED:
2270                 if ($_action !== 'get') {
2271                     // TODO check if app has MANAGE_SHARED_FOLDERS right?
2272                     $hasPermission = Tinebase_Acl_Roles::getInstance()->hasRight(
2273                         $_path->application->name,
2274                         Tinebase_Core::getUser()->getId(),
2275                         Tinebase_Acl_Rights::MANAGE_SHARED_FOLDERS
2276                     );
2277                 } else {
2278                     $hasPermission = true;
2279                 }
2280                 break;
2281             case Tinebase_Model_Tree_Node_Path::TYPE_ROOT:
2282                 $hasPermission = ($_action === 'get');
2283                 break;
2284             default:
2285                 $hasPermission = $this->checkACLNode($_path->getNode(), $_action);
2286         }
2287
2288         if (! $hasPermission) {
2289             throw new Tinebase_Exception_AccessDenied('No permission to ' . $_action . ' nodes in path ' . $_path->flatpath);
2290         }
2291     }
2292
2293     /**
2294      * check if user has the permissions for the node
2295      *
2296      * does not start a transaction!
2297      *
2298      * @param Tinebase_Model_Tree_Node $_node
2299      * @param string $_action get|update|...
2300      * @return boolean
2301      */
2302     public function checkACLNode(Tinebase_Model_Tree_Node $_node, $_action = 'get')
2303     {
2304         if (Tinebase_Core::getUser()->hasGrant($_node, Tinebase_Model_Grants::GRANT_ADMIN, 'Tinebase_Model_Tree_Node')) {
2305             return true;
2306         }
2307
2308         switch ($_action) {
2309             case 'get':
2310                 $requiredGrant = Tinebase_Model_Grants::GRANT_READ;
2311                 break;
2312             case 'add':
2313                 $requiredGrant = Tinebase_Model_Grants::GRANT_ADD;
2314                 break;
2315             case 'update':
2316                 $requiredGrant = Tinebase_Model_Grants::GRANT_EDIT;
2317                 break;
2318             case 'delete':
2319                 $requiredGrant = Tinebase_Model_Grants::GRANT_DELETE;
2320                 break;
2321             default:
2322                 throw new Tinebase_Exception_UnexpectedValue('Unknown action: ' . $_action);
2323         }
2324
2325         $result = Tinebase_Core::getUser()->hasGrant($_node, $requiredGrant, 'Tinebase_Model_Tree_Node');
2326         if (true === $result && Tinebase_Model_Grants::GRANT_DELETE === $requiredGrant) {
2327             // check that we have the grant for all children too!
2328             $allChildIds = $this->getAllChildIds(array($_node->getId()));
2329             $deleteGrantChildIds = $this->getAllChildIds(array($_node->getId()), array(), false, $requiredGrant);
2330             if ($allChildIds != $deleteGrantChildIds) {
2331                 $result = false;
2332             }
2333         }
2334
2335         return $result;
2336     }
2337
2338
2339     /**************** container interface *******************/
2340
2341     /**
2342      * check if the given user user has a certain grant
2343      *
2344      * @param   string|Tinebase_Model_User   $_accountId
2345      * @param   int|Tinebase_Record_Abstract $_containerId
2346      * @param   array|string                 $_grant
2347      * @return  boolean
2348      */
2349     public function hasGrant($_accountId, $_containerId, $_grant)
2350     {
2351         // always refetch node to have current acl_node value
2352         $node = $this->get($_containerId);
2353         /** @noinspection PhpUndefinedMethodInspection */
2354         $account = $_accountId instanceof Tinebase_Model_FullUser
2355             ? $_accountId
2356             : Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend('accountId', $_accountId, 'Tinebase_Model_FullUser');
2357         return $this->_nodeAclController->hasGrant($node, $_grant, $account);
2358     }
2359
2360     /**
2361      * return users which made personal containers accessible to given account
2362      *
2363      * @param   string|Tinebase_Model_User        $_accountId
2364      * @param   string|Tinebase_Model_Application $_recordClass
2365      * @param   array|string                      $_grant
2366      * @param   bool                              $_ignoreACL
2367      * @param   bool                              $_andGrants
2368      * @return  Tinebase_Record_RecordSet set of Tinebase_Model_User
2369      */
2370     public function getOtherUsers($_accountId, $_recordClass, $_grant, $_ignoreACL = false, $_andGrants = false)
2371     {
2372         $result = $this->_getNodesOfType(self::FOLDER_TYPE_PERSONAL, $_accountId, $_recordClass, /* $_owner = */ null, $_grant, $_ignoreACL);
2373         return $result;
2374     }
2375
2376     /**
2377      * returns the shared container for a given application accessible by the current user
2378      *
2379      * @param   string|Tinebase_Model_User        $_accountId
2380      * @param   string|Tinebase_Model_Application $_recordClass
2381      * @param   array|string                      $_grant
2382      * @param   bool                              $_ignoreACL
2383      * @param   bool                              $_andGrants
2384      * @return  Tinebase_Record_RecordSet set of Tinebase_Model_Container
2385      * @throws  Tinebase_Exception_NotFound
2386      */
2387     public function getSharedContainer($_accountId, $_recordClass, $_grant, $_ignoreACL = false, $_andGrants = false)
2388     {
2389         $result = $this->_getNodesOfType(self::FOLDER_TYPE_SHARED, $_accountId, $_recordClass, /* $_owner = */ null, $_grant, $_ignoreACL);
2390         return $result;
2391     }
2392
2393     /**
2394      * @param            $_type
2395      * @param            $_accountId
2396      * @param            $_recordClass
2397      * @param null       $_owner
2398      * @param string     $_grant
2399      * @param bool|false $_ignoreACL
2400      * @return Tinebase_Record_RecordSet
2401      * @throws Tinebase_Exception_InvalidArgument
2402      * @throws Tinebase_Exception_NotFound
2403      * @throws Tinebase_Exception_SystemGeneric
2404      */
2405     protected function _getNodesOfType($_type, $_accountId, $_recordClass, $_owner = null, $_grant = Tinebase_Model_Grants::GRANT_READ, $_ignoreACL = false)
2406     {
2407         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
2408         $accountId = Tinebase_Model_User::convertUserIdToInt($_accountId);
2409         $appAndModel = Tinebase_Application::extractAppAndModel($_recordClass);
2410         $app = Tinebase_Application::getInstance()->getApplicationByName($appAndModel['appName']);
2411         $path = $this->getApplicationBasePath($app, $_type);
2412
2413         if ($_type == self::FOLDER_TYPE_PERSONAL and $_owner == null) {
2414             return $this->_getOtherUsersNodes($_accountId, $path, $_grant, $_ignoreACL);
2415
2416         } else {
2417             // SHARED or MY_FOLDERS
2418
2419             $ownerId = $_owner instanceof Tinebase_Model_FullUser ? $_owner->getId() : $_owner;
2420             if ($ownerId) {
2421                 $path .= '/' . $ownerId;
2422             }
2423             $pathRecord = Tinebase_Model_Tree_Node_Path::createFromPath($path);
2424
2425             try {
2426                 $parentNode = $this->stat($pathRecord->statpath);
2427                 $filterArray = array(array('field' => 'parent_id', 'operator' => 'equals', 'value' => $parentNode->getId()));
2428
2429                 $filter = new Tinebase_Model_Tree_Node_Filter(
2430                     $filterArray,
2431                     /* $_condition = */ '',
2432                     /* $_options */ array(
2433                     'ignoreAcl' => $_ignoreACL,
2434                     'user' => $_accountId instanceof Tinebase_Record_Abstract
2435                         ? $_accountId->getId()
2436                         : $_accountId
2437                 ));
2438                 $filter->setRequiredGrants((array)$_grant);
2439                 $result = $this->searchNodes($filter);
2440
2441             } catch (Tinebase_Exception_NotFound $tenf) {
2442                 if ($accountId === $ownerId) {
2443                     Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2444                         . ' Creating personal root node for user ' . $accountId);
2445                     $this->createAclNode($pathRecord->statpath);
2446                 }
2447
2448                 return $result;
2449             }
2450         }
2451
2452         foreach ($result as $node) {
2453             $node->path = $path . '/' . $node->name;
2454         }
2455
2456         return $result;
2457     }
2458
2459
2460     protected function _getOtherUsersNodes($_accountId, $_path, $_grant, $_ignoreACL)
2461     {
2462         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
2463         $accountId = Tinebase_Model_User::convertUserIdToInt($_accountId);
2464
2465         // other users
2466         $accountIds = Tinebase_User::getInstance()->getUsers()->getArrayOfIds();
2467         // remove own id
2468         $accountIds = Tinebase_Helper::array_remove_by_value($accountId, $accountIds);
2469         $pathRecord = Tinebase_Model_Tree_Node_Path::createFromPath($_path);
2470         try {
2471             $parentNode = $this->stat($pathRecord->statpath);
2472         } catch (Tinebase_Exception_NotFound $tenf) {
2473             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2474                 . ' Creating PERSONAL root node');
2475             $this->createAclNode($pathRecord->statpath);
2476             return $result;
2477         }
2478         $filter = new Tinebase_Model_Tree_Node_Filter(
2479             array(
2480                 array('field' => 'name',      'operator' => 'in',     'value' => $accountIds),
2481                 array('field' => 'parent_id', 'operator' => 'equals', 'value' => $parentNode->getId())
2482             ),
2483             /* $_condition = */ '',
2484             /* $_options */ array(
2485                 'ignoreAcl' => true,
2486             )
2487         );
2488         $otherAccountNodes = $this->searchNodes($filter);
2489         $filter = new Tinebase_Model_Tree_Node_Filter(
2490             array(
2491                 array('field' => 'parent_id', 'operator' => 'in', 'value' => $otherAccountNodes->getArrayOfIds()),
2492             ),
2493             /* $_condition = */ '',
2494             /* $_options */ array(
2495                 'ignoreAcl' => $_ignoreACL,
2496                 'user' => $_accountId instanceof Tinebase_Record_Abstract
2497                     ? $_accountId->getId()
2498                     : $_accountId
2499             )
2500         );
2501         $filter->setRequiredGrants((array)$_grant);
2502         // get shared folders of other users
2503         $sharedFoldersOfOtherUsers = $this->searchNodes($filter);
2504
2505         foreach ($otherAccountNodes as $otherAccount) {
2506             if (count($sharedFoldersOfOtherUsers->filter('parent_id', $otherAccount->getId())) > 0) {
2507                 $result->addRecord($otherAccount);
2508                 $account = Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend(
2509                     'accountId',
2510                     $otherAccount->name,
2511                     'Tinebase_Model_FullUser'
2512                 );
2513                 $otherAccount->name = $account->accountDisplayName;
2514             }
2515         }
2516
2517         return $result;
2518     }
2519
2520     /**
2521      * returns the personal containers of a given account accessible by a another given account
2522      *
2523      * @param   string|Tinebase_Model_User       $_accountId
2524      * @param   string|Tinebase_Record_Interface $_recordClass
2525      * @param   int|Tinebase_Model_User          $_owner
2526      * @param   array|string                     $_grant
2527      * @param   bool                             $_ignoreACL
2528      * @return  Tinebase_Record_RecordSet of subtype Tinebase_Model_Tree_Node
2529      * @throws  Tinebase_Exception_NotFound
2530      */
2531     public function getPersonalContainer($_accountId, $_recordClass, $_owner, $_grant = Tinebase_Model_Grants::GRANT_READ, $_ignoreACL = false)
2532     {
2533         $result = $this->_getNodesOfType(self::FOLDER_TYPE_PERSONAL, $_accountId, $_recordClass, $_owner, $_grant, $_ignoreACL);
2534
2535         // TODO generalize
2536         $accountId = Tinebase_Model_User::convertUserIdToInt($_accountId);
2537         $ownerId = $_owner instanceof Tinebase_Model_FullUser ? $_owner->getId() : $_owner;
2538         $appAndModel = Tinebase_Application::extractAppAndModel($_recordClass);
2539         $app = Tinebase_Application::getInstance()->getApplicationByName($appAndModel['appName']);
2540         $path = $this->getApplicationBasePath($app, self::FOLDER_TYPE_PERSONAL);
2541         $path .= '/' . $ownerId;
2542         $pathRecord = Tinebase_Model_Tree_Node_Path::createFromPath($path);
2543
2544         // no personal node found ... creating one?
2545         if (count($result) === 0 && $accountId === $ownerId) {
2546             /** @noinspection PhpUndefinedMethodInspection */
2547             $account = (!$_accountId instanceof Tinebase_Model_User)
2548                 ? Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend('accountId', $_accountId)
2549                 : $_accountId;
2550
2551             $translation = Tinebase_Translation::getTranslation('Tinebase');
2552             $nodeName = sprintf($translation->_("%s's personal container"), $account->accountFullName);
2553             $nodeName = preg_replace('/\//', '', $nodeName);
2554             $path = $pathRecord->statpath . '/' . $nodeName;
2555
2556             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2557                 . ' Creating personal node with name ' . $nodeName);
2558
2559             $personalNode = Tinebase_FileSystem::getInstance()->createAclNode($path);
2560             $result->addRecord($personalNode);
2561         }
2562
2563         return $result;
2564     }
2565
2566     /**
2567      * return all container, which the user has the requested right for
2568      *
2569      * used to get a list of all containers accesssible by the current user
2570      *
2571      * @param   string|Tinebase_Model_User $accountId
2572      * @param   string|Tinebase_Model_Application $recordClass
2573      * @param   array|string $grant
2574      * @param   bool $onlyIds return only ids
2575      * @param   bool $ignoreACL
2576      * @return array|Tinebase_Record_RecordSet
2577      * @throws Tinebase_Exception
2578      */
2579     public function getContainerByACL($accountId, $recordClass, $grant, $onlyIds = false, $ignoreACL = false)
2580     {
2581         throw new Tinebase_Exception('implement me');
2582     }
2583
2584     /**
2585      * gets default container of given user for given app
2586      *  - did and still does return personal first container by using the application name instead of the recordClass name
2587      *  - allows now to use different models with default container in one application
2588      *
2589      * @param   string|Tinebase_Record_Interface $recordClass
2590      * @param   string|Tinebase_Model_User       $accountId use current user if omitted
2591      * @param   string                           $defaultContainerPreferenceName
2592      * @return  Tinebase_Record_Abstract
2593      */
2594     public function getDefaultContainer($recordClass, $accountId = null, $defaultContainerPreferenceName = null)
2595     {
2596         $account = Tinebase_Core::getUser();
2597         return $this->getPersonalContainer($account, $recordClass, $accountId ? $accountId : $account)->getFirstRecord();
2598     }
2599
2600     /**
2601      * get grants assigned to one account of one container
2602      *
2603      * @param   string|Tinebase_Model_User          $_accountId
2604      * @param   int|Tinebase_Record_Abstract        $_containerId
2605      * @param   string                              $_grantModel
2606      * @return Tinebase_Model_Grants
2607      *
2608      * TODO add to interface
2609      */
2610     public function getGrantsOfAccount($_accountId, $_containerId, /** @noinspection PhpUnusedParameterInspection */ $_grantModel = 'Tinebase_Model_Grants')
2611     {
2612         $path = $this->getPathOfNode($_containerId, true);
2613         $accountId = Tinebase_Model_User::convertUserIdToInt($_accountId);
2614         $pathRecord = Tinebase_Model_Tree_Node_Path::createFromStatPath($path);
2615         if ($pathRecord->isPersonalPath($_accountId)) {
2616             return new Tinebase_Model_Grants(array(
2617                 'account_id' => $accountId,
2618                 'account_type' => Tinebase_Acl_Rights::ACCOUNT_TYPE_USER,
2619                 Tinebase_Model_Grants::GRANT_READ => true,
2620                 Tinebase_Model_Grants::GRANT_ADD => true,
2621                 Tinebase_Model_Grants::GRANT_EDIT => false,
2622                 Tinebase_Model_Grants::GRANT_DELETE => false,
2623                 Tinebase_Model_Grants::GRANT_EXPORT => true,
2624                 Tinebase_Model_Grants::GRANT_SYNC => true,
2625             ));
2626         } else if ($pathRecord->isToplevelPath() && $pathRecord->containerType === Tinebase_FileSystem::FOLDER_TYPE_SHARED) {
2627             $account = $_accountId instanceof Tinebase_Model_FullUser
2628                 ? $_accountId
2629                 : Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend('accountId', $_accountId, 'Tinebase_Model_FullUser');
2630             $hasManageSharedRight = $account->hasRight($pathRecord->application->name, Tinebase_Acl_Rights::MANAGE_SHARED_FOLDERS);
2631             return new Tinebase_Model_Grants(array(
2632                 'account_id' => $accountId,
2633                 'account_type' => Tinebase_Acl_Rights::ACCOUNT_TYPE_USER,
2634                 Tinebase_Model_Grants::GRANT_READ => true,
2635                 Tinebase_Model_Grants::GRANT_ADD => $hasManageSharedRight,
2636                 Tinebase_Model_Grants::GRANT_EDIT => false,
2637                 Tinebase_Model_Grants::GRANT_DELETE => false,
2638                 Tinebase_Model_Grants::GRANT_EXPORT => true,
2639                 Tinebase_Model_Grants::GRANT_SYNC => true,
2640             ));
2641         } else if ($pathRecord->isToplevelPath() && $pathRecord->containerType === Tinebase_FileSystem::FOLDER_TYPE_PERSONAL) {
2642             // other users
2643             return new Tinebase_Model_Grants(array(
2644                 'account_id' => $accountId,
2645                 'account_type' => Tinebase_Acl_Rights::ACCOUNT_TYPE_USER,
2646                 Tinebase_Model_Grants::GRANT_READ => true,
2647             ));
2648         } else {
2649             return $this->_nodeAclController->getGrantsOfAccount($_accountId, $_containerId);
2650         }
2651     }
2652
2653
2654     /**
2655      * get all grants assigned to this container
2656      *
2657      * @param   int|Tinebase_Record_Abstract $_containerId
2658      * @param   bool                         $_ignoreAcl
2659      * @param   string                       $_grantModel
2660      * @return  Tinebase_Record_RecordSet subtype Tinebase_Model_Grants
2661      * @throws  Tinebase_Exception_AccessDenied
2662      *
2663      * TODO add to interface
2664      */
2665     public function getGrantsOfContainer($_containerId, $_ignoreAcl = false, $_grantModel = 'Tinebase_Model_Grants')
2666     {
2667         $record = $_containerId instanceof Tinebase_Model_Tree_Node ? $_containerId : $this->get($_containerId);
2668
2669         if (! $_ignoreAcl) {
2670             if (! Tinebase_Core::getUser()->hasGrant($record, Tinebase_Model_Grants::GRANT_READ)) {
2671                 throw new Tinebase_Exception_AccessDenied('not allowed to read grants');
2672             }
2673         }
2674
2675         return $this->_nodeAclController->getGrantsForRecord($record);
2676     }
2677
2678     /**
2679      * remove file revisions based on settings:
2680      * Tinebase_Config::FILESYSTEM -> Tinebase_Config::FILESYSTEM_NUMKEEPREVISIONS
2681      * Tinebase_Config::FILESYSTEM -> Tinebase_Config::FILESYSTEM_MONTHKEEPREVISIONS
2682      * or folder specific settings
2683      */
2684     public function clearFileRevisions()
2685     {
2686         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2687             . ' starting to clear file revisions');
2688
2689         $config = Tinebase_Config::getInstance()->{Tinebase_Config::FILESYSTEM};
2690         $numRevisions = (int)$config->{Tinebase_Config::FILESYSTEM_NUMKEEPREVISIONS};
2691         $monthRevisions = (int)$config->{Tinebase_Config::FILESYSTEM_MONTHKEEPREVISIONS};
2692         $treeNodeBackend = $this->_getTreeNodeBackend();
2693         $parents = array();
2694         $count = 0;
2695
2696         foreach ($treeNodeBackend->search(
2697                 new Tinebase_Model_Tree_Node_Filter(array(
2698                     array('field' => 'type', 'operator' => 'equals', 'value' => Tinebase_Model_Tree_FileObject::TYPE_FILE)
2699                 ), '', array('ignoreAcl' => true))
2700                 , null, true) as $id) {
2701             try {
2702                 /** @var Tinebase_Model_Tree_Node $fileNode */
2703                 $fileNode = $treeNodeBackend->get($id, true);
2704                 if (isset($parents[$fileNode->parent_id])) {
2705                     $parentXProps = $parents[$fileNode->parent_id];
2706                 } else {
2707                     $parentNode = $treeNodeBackend->get($fileNode->parent_id, true);
2708                     $parentXProps = $parents[$fileNode->parent_id] = $parentNode->{Tinebase_Model_Tree_Node::XPROPS_REVISION};
2709                 }
2710
2711                 if (!empty($parentXProps)) {
2712                     if (isset($parentXProps[Tinebase_Model_Tree_Node::XPROPS_REVISION_ON])
2713                         && false === $parentXProps[Tinebase_Model_Tree_Node::XPROPS_REVISION_ON]) {
2714                         $numRev = 1;
2715                         $monthRev = 0;
2716                     } else {
2717                         if (isset($parentXProps[Tinebase_Model_Tree_Node::XPROPS_REVISION_NUM])) {
2718                             $numRev = $parentXProps[Tinebase_Model_Tree_Node::XPROPS_REVISION_NUM];
2719                         } else {
2720                             $numRev = $numRevisions;
2721                         }
2722                         if (isset($parentXProps[Tinebase_Model_Tree_Node::XPROPS_REVISION_MONTH])) {
2723                             $monthRev = $parentXProps[Tinebase_Model_Tree_Node::XPROPS_REVISION_MONTH];
2724                         } else {
2725                             $monthRev = $monthRevisions;
2726                         }
2727                     }
2728                 } else {
2729                     $numRev = $numRevisions;
2730                     $monthRev = $monthRevisions;
2731                 }
2732
2733                 if ($numRev > 0) {
2734                     if (is_array($fileNode->available_revisions) && count($fileNode->available_revisions) > $numRev) {
2735                         $revisions = $fileNode->available_revisions;
2736                         sort($revisions, SORT_NUMERIC);
2737                         $count += $this->_fileObjectBackend->deleteRevisions($fileNode->object_id, array_slice($revisions, 0, count($revisions) - $numRevisions));
2738                     }
2739                 }
2740
2741                 if (1 !== $numRev && $monthRev > 0) {
2742                     $count += $this->_fileObjectBackend->clearOldRevisions($fileNode->object_id, $monthRev);
2743                 }
2744
2745             } catch(Tinebase_Exception_NotFound $tenf) {}
2746         }
2747
2748         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2749             . ' cleared ' . $count . ' file revisions');
2750     }
2751
2752     /**
2753      * create preview for files without a preview, delete previews for already deleted files
2754      */
2755     public function sanitizePreviews()
2756     {
2757         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2758             . ' starting to sanitize previews');
2759
2760         if (false === $this->_previewActive) {
2761             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2762                 . ' previews are disabled');
2763             return true;
2764         }
2765
2766         $treeNodeBackend = $this->_getTreeNodeBackend();
2767         $previewController = Tinebase_FileSystem_Previews::getInstance();
2768         $validHashes = array();
2769         $invalidHashes = array();
2770         $created = 0;
2771         $deleted = 0;
2772
2773         foreach($treeNodeBackend->search(
2774                 new Tinebase_Model_Tree_Node_Filter(array(
2775                     array('field' => 'type', 'operator' => 'equals', 'value' => Tinebase_Model_Tree_FileObject::TYPE_FILE)
2776                 ), '', array('ignoreAcl' => true))
2777                 , null, true) as $id) {
2778
2779             /** @var Tinebase_Model_Tree_Node $node */
2780             try {
2781                 $treeNodeBackend->setRevision(null);
2782                 $node = $treeNodeBackend->get($id);
2783             } catch (Tinebase_Exception_NotFound $tenf) {
2784                 continue;
2785             }
2786
2787             $availableRevisions = $node->available_revisions;
2788             if (!is_array($availableRevisions)) {
2789                 $availableRevisions = explode(',', $availableRevisions);
2790             }
2791             foreach ($availableRevisions as $revision) {
2792                 if ($node->revision != $revision) {
2793                     $treeNodeBackend->setRevision($revision);
2794                     try {
2795                         $actualNode = $treeNodeBackend->get($id);
2796                         $treeNodeBackend->setRevision(null);
2797                     } catch (Tinebase_Exception_NotFound $tenf) {
2798                         continue;
2799                     }
2800                 } else {
2801                     $actualNode = $node;
2802                 }
2803
2804                 if ($previewController->hasPreviews($actualNode->hash)) {
2805                     $validHashes[$actualNode->hash] = true;
2806                     continue;
2807                 }
2808
2809                 $previewController->createPreviews($actualNode->getId(), $actualNode->revision);
2810                 $validHashes[$actualNode->hash] = true;
2811                 ++$created;
2812             }
2813         }
2814
2815         $treeNodeBackend->setRevision(null);
2816
2817
2818         $parents = array();
2819         foreach($treeNodeBackend->search(
2820                 new Tinebase_Model_Tree_Node_Filter(array(
2821                     array('field' => 'type', 'operator' => 'equals', 'value' => Tinebase_Model_Tree_FileObject::TYPE_PREVIEW)
2822                 ), '', array('ignoreAcl' => true))
2823                 , null, true) as $id) {
2824             /** @var Tinebase_Model_Tree_Node $fileNode */
2825             $fileNode = $treeNodeBackend->get($id, true);
2826             if (Tinebase_Model_Tree_FileObject::TYPE_PREVIEW !== $fileNode->type) {
2827                 continue;
2828             }
2829             if (!isset($parents[$fileNode->parent_id])) {
2830                 $parent = $treeNodeBackend->get($fileNode->parent_id);
2831                 $parents[$fileNode->parent_id] = $parent;
2832             } else {
2833                 $parent = $parents[$fileNode->parent_id];
2834             }
2835             $name = $parent->name;
2836
2837             $parentId = $parent->parent_id;
2838             if (!isset($parents[$parentId])) {
2839                 $parent = $treeNodeBackend->get($parentId);
2840                 $parents[$parentId] = $parent;
2841             } else {
2842                 $parent = $parents[$parentId];
2843             }
2844
2845             $name = $parent->name . $name;
2846
2847             if (!isset($validHashes[$name])) {
2848                 $invalidHashes[] = $name;
2849             }
2850         }
2851
2852         $validHashes = $this->_fileObjectBackend->checkRevisions($invalidHashes);
2853         $hashesToDelete = array_diff($invalidHashes, $validHashes);
2854         if (count($hashesToDelete) > 0) {
2855             $deleted = count($hashesToDelete);
2856             $previewController->deletePreviews($hashesToDelete);
2857         }
2858
2859         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2860             . ' created ' . $created . ' new previews, deleted ' . $deleted . ' previews.');
2861
2862         return true;
2863     }
2864
2865     /**
2866      * @param string $_hash
2867      * @param integer $_count
2868      */
2869     public function updatePreviewCount($_hash, $_count)
2870     {
2871         $this->_fileObjectBackend->updatePreviewCount($_hash, $_count);
2872     }
2873
2874     /**
2875      * check the folder tree up to the root for notification settings and either send notification immediately
2876      * or create alarm for it to be send in the future. Alarms aggregate!
2877      *
2878      * @param string $_fileNodeId
2879      * @param string $_crudAction
2880      * @return boolean
2881      */
2882     public function checkForCRUDNotifications($_fileNodeId, $_crudAction)
2883     {
2884         $nodeId = $_fileNodeId;
2885         $foundUsers = array();
2886         $foundGroups = array();
2887         $alarmController = Tinebase_Alarm::getInstance();
2888
2889         do {
2890             $node = $this->get($nodeId);
2891             $notificationProps = $node->xprops(Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION);
2892
2893             if (count($notificationProps) === 0) {
2894                 continue;
2895             }
2896
2897             //sort it to handle user settings first, then group settings!
2898             //TODO write a test that tests this!
2899             usort($notificationProps, function($a) {
2900                 if (is_array($a) && isset($a[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_TYPE]) &&
2901                     $a[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_TYPE] === Tinebase_Acl_Rights::ACCOUNT_TYPE_USER) {
2902                     return false;
2903                 }
2904                 return true;
2905             });
2906
2907             foreach($notificationProps as $notificationProp) {
2908
2909                 $notifyUsers = array();
2910
2911                 if (!isset($notificationProp[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_ID]) ||
2912                         !isset($notificationProp[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_TYPE])) {
2913                     // LOG broken notification setting
2914                     continue;
2915                 }
2916
2917                 if ($notificationProp[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_TYPE] === Tinebase_Acl_Rights::ACCOUNT_TYPE_USER) {
2918                     if (isset($foundUsers[$notificationProp[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_ID]])) {
2919                         continue;
2920                     }
2921
2922                     $foundUsers[$notificationProp[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_ID]] = true;
2923                     if (isset($notificationProp[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACTIVE]) && false === (bool)$notificationProp[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACTIVE]) {
2924                         continue;
2925                     }
2926
2927                     $notifyUsers[] = $notificationProp[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_ID];
2928
2929                 } elseif ($notificationProp[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_TYPE] === Tinebase_Acl_Rights::ACCOUNT_TYPE_GROUP) {
2930                     if (isset($foundGroups[$notificationProp[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_ID]])) {
2931                         continue;
2932                     }
2933
2934                     $foundGroups[$notificationProp[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_ID]] = true;
2935
2936                     $doNotify = !isset($notificationProp[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACTIVE]) ||
2937                         true === (bool)$notificationProp[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACTIVE];
2938
2939                     // resolve Group Members
2940                     foreach (Tinebase_Group::getInstance()->getGroupMembers($notificationProp[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_ACCOUNT_ID]) as $userId) {
2941                         if (true === $doNotify && !isset($foundUsers[$userId])) {
2942                             $notifyUsers[] = $userId;
2943                         }
2944                         $foundUsers[$userId] = true;
2945                     }
2946
2947                     if (false === $doNotify) {
2948                         continue;
2949                     }
2950                 } else {
2951                     // LOG broken notification setting
2952                     continue;
2953                 }
2954
2955                 if (isset($notificationProp[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_SUMMARY]) && (int)$notificationProp[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_SUMMARY] > 0) {
2956
2957                     foreach ($notifyUsers as $accountId) {
2958                         $crudNotificationHash = $this->_getCRUDNotificationHash($accountId, $nodeId);
2959                         $alarms = $alarmController->search(new Tinebase_Model_AlarmFilter(array(
2960                             array(
2961                                 'field'     => 'model',
2962                                 'operator'  => 'equals',
2963                                 'value'     => 'Tinebase_FOOO_FileSystem'
2964                             ),
2965                             array(
2966                                 'field'     => 'record_id',
2967                                 'operator'  => 'equals',
2968                                 'value'     => $crudNotificationHash
2969                             ),
2970                             array(
2971                                 'field'     => 'sent_status',
2972                                 'operator'  => 'equals',
2973                                 'value'     => Tinebase_Model_Alarm::STATUS_PENDING
2974                             )
2975                         )));
2976
2977                         if ($alarms->count() > 0) {
2978                             /** @var Tinebase_Model_Alarm $alarm */
2979                             $alarm = $alarms->getFirstRecord();
2980                             $options = json_decode($alarm->options, true);
2981                             if (null === $options) {
2982                                 // broken! not good
2983                                 $options = array();
2984                             }
2985                             if (!isset($options['files'])) {
2986                                 // broken! not good
2987                                 $options['files'] = array();
2988                             }
2989                             if (!isset($options['files'][$_fileNodeId])) {
2990                                 $options['files'][$_fileNodeId] = array();
2991                             }
2992                             $options['files'][$_fileNodeId][$_crudAction] = true;
2993                             $alarm->options = json_encode($options);
2994                             $alarmController->update($alarm);
2995                         } else {
2996                             $alarm = new Tinebase_Model_Alarm(array(
2997                                 'record_id'     => $crudNotificationHash,
2998                                 'model'         => 'Tinebase_FOOO_FileSystem',
2999                                 'minutes_before'=> 0,
3000                                 'alarm_time'    => Tinebase_DateTime::now()->addDay((int)$notificationProp[Tinebase_Model_Tree_Node::XPROPS_NOTIFICATION_SUMMARY]),
3001                                 'options'       => array(
3002                                     'files'     => array($_fileNodeId => array($_crudAction => true)),
3003                                     'accountId' => $accountId
3004                                 ),
3005                             ));
3006                             $alarmController->create($alarm);
3007                         }
3008                     }
3009                 } else {
3010                     $this->sendCRUDNotification($notifyUsers, array($_fileNodeId => array($_crudAction => true)));
3011                 }
3012             }
3013
3014         } while(null !== ($nodeId = $node->parent_id));
3015
3016         return true;
3017     }
3018
3019     protected function _getCRUDNotificationHash($_accountId, $_nodeId)
3020     {
3021         return md5($_accountId . '_' . $_nodeId);
3022     }
3023
3024     public function sendCRUDNotification(array $_accountIds, array $_crudActions)
3025     {
3026         $fileSystem = Tinebase_FileSystem::getInstance();
3027
3028         foreach($_accountIds as $accountId) {
3029             $locale = Tinebase_Translation::getLocale(Tinebase_Core::getPreference()->getValueForUser(Tinebase_Preference::LOCALE, $accountId));
3030             $translate = Tinebase_Translation::getTranslation('Filemanager', $locale);
3031
3032             try {
3033                 $user = Tinebase_User::getInstance()->getFullUserById($accountId);
3034             } catch(Tinebase_Exception_NotFound $tenf) {
3035                 continue;
3036             }
3037
3038             $translatedMsgHeader = $translate->_('The following files have changed:'); // _('The following files have changed:')
3039             $fileStr = $translate->_('File'); // _('File')
3040             $createdStr = $translate->_('has been created.'); // _('has been created.')
3041             $updatedStr = $translate->_('has been changed.'); // _('has been changed.')
3042             $deleteStr = $translate->_('has been deleted.'); // _('has been deleted.')
3043
3044             $messageBody = '<html><body><p>' . $translatedMsgHeader . '</p>';
3045             foreach($_crudActions as $fileNodeId => $changes) {
3046
3047                 try {
3048                     $fileNode = $fileSystem->get($fileNodeId, true);
3049                 } catch(Tinebase_Exception_NotFound $tenf) {
3050                     continue;
3051                 }
3052
3053                 $path = Filemanager_Model_Node::getDeepLink($fileNode);
3054
3055                 $messageBody .= '<p>';
3056
3057                 foreach ($changes as $change => $foo) {
3058                     switch($change) {
3059                         case 'created':
3060                             $messageBody .= $fileStr . ' <a href="' . $path . '">' . $fileNode->name . '</a> ' . $createdStr . '<br/>';
3061                             break;
3062                         case 'updated':
3063                             $messageBody .= $fileStr . ' <a href="' . $path . '">' . $fileNode->name . '</a> ' . $updatedStr . '<br/>';
3064                             break;
3065                         case 'deleted':
3066                             $messageBody .= $fileStr . ' <a href="' . $path . '">' . $fileNode->name . '</a> ' . $deleteStr . '<br/>';
3067                             break;
3068                         default:
3069                             // should not happen!
3070                     }
3071                 }
3072
3073                 $messageBody .= '</p>';
3074             }
3075             $messageBody .= '</body></html>';
3076
3077             $translatedSubject = $translate->_('filemanager notification'); // _('filemanager notification')
3078
3079             Tinebase_Notification::getInstance()->send($accountId, array($user->contact_id), $translatedSubject, '', $messageBody);
3080         }
3081     }
3082
3083     /**
3084      * sendAlarm - send an alarm and update alarm status/sent_time/...
3085      *
3086      * @param  Tinebase_Model_Alarm $_alarm
3087      * @return Tinebase_Model_Alarm
3088      */
3089     public function sendAlarm(Tinebase_Model_Alarm $_alarm)
3090     {
3091         $options = json_decode($_alarm->options, true);
3092         do {
3093             if (null === $options) {
3094                 // broken! not good
3095                 break;
3096             }
3097             if (!isset($options['files'])) {
3098                 // broken! not good
3099                 break;
3100             }
3101             if (!isset($options['accountId'])) {
3102                 // broken! not good
3103                 break;
3104             }
3105
3106             $this->sendCRUDNotification((array)$options['accountId'], $options['files']);
3107         } while (false);
3108
3109         return $_alarm;
3110     }
3111 }