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