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