0011666: Some fixes for Expressodriver
[tine20] / tine20 / Expressodriver / Controller / Node.php
1 <?php
2
3 /**
4  * Tine 2.0
5  *
6  * @package     Expressodriver
7  * @subpackage  Controller
8  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
9  * @copyright   Copyright (c) 2007-2014 Metaways Infosystems GmbH (http://www.metaways.de)
10  * @copyright   Copyright (c) 2014 Serpro (http://www.serpro.gov.br)
11  * @author      Marcelo Teixeira <marcelo.teixeira@serpro.gov.br>
12  * @author      Edgar de Lucca <edgar.lucca@serpro.gov.br>
13  *
14  */
15
16 /**
17  * Node controller for Expressodriver
18  *
19  * @package     Expressodriver
20  * @subpackage  Controller
21  */
22 class Expressodriver_Controller_Node
23     implements Tinebase_Controller_SearchInterface, Tinebase_Controller_Record_Interface
24 {
25
26     /**
27      * application name (is needed in checkRight())
28      *
29      * @var string
30      */
31     protected $_applicationName = 'Expressodriver';
32
33     /**
34      * Storage adapters backends
35      *
36      * @var array
37      */
38     protected static $_backends = array();
39
40     /**
41      * the model handled by this controller
42      * @var string
43      */
44     protected $_modelName = 'Expressodriver_Model_Node';
45
46     /**
47      * TODO handle modlog
48      * @var boolean
49      */
50     protected $_omitModLog = TRUE;
51
52     /**
53      * holds the total count of the last recursive search
54      * @var integer
55      */
56     protected $_recursiveSearchTotalCount = 0;
57
58     /**
59      * holds the total count of result search
60      *
61      * @var integer
62      */
63     protected $_searchTotalCount = 0;
64
65     /**
66      * holds the instance of the singleton
67      *
68      * @var Expressodriver_Controller_Node
69      */
70     private static $_instance = NULL;
71
72     /**
73      * the constructor
74      *
75      * don't use the constructor. use the singleton
76      */
77     private function __construct()
78     {
79         stream_wrapper_register('fakedir', 'Expressodriver_Backend_Storage_StreamDir');
80         stream_wrapper_register('external', 'Expressodriver_Backend_Storage_StreamWrapper');
81     }
82
83     /**
84      * don't clone. Use the singleton.
85      *
86      */
87     private function __clone()
88     {
89
90     }
91
92     /**
93      * the singleton pattern
94      *
95      * @return Expressodriver_Controller_Node
96      */
97     public static function getInstance()
98     {
99         if (self::$_instance === NULL) {
100             self::$_instance = new Expressodriver_Controller_Node();
101         }
102
103         return self::$_instance;
104     }
105
106     /**
107      * inspect update of one record (before update)
108      *
109      * @param   Tinebase_Record_Interface $_record      the update record
110      * @param   Tinebase_Record_Interface $_oldRecord   the current persistent record
111      * @return  void
112      */
113     protected function _inspectBeforeUpdate($_record, $_oldRecord)
114     {
115
116     }
117
118     /**
119      * get multiple tree nodes
120      * @see Tinebase_Controller_Record_Abstract::getMultiple()
121      * @param array $_ids Ids of tree nodes
122      * @return  Tinebase_Record_RecordSet
123      */
124     public function getMultiple($_ids)
125     {
126         // replace objects with their id's
127         foreach ($_ids as &$id) {
128             if ($id instanceof Tinebase_Record_Interface) {
129                 $id = $id->getId();
130             }
131         }
132
133         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array(), TRUE);
134         foreach ($_ids as $id) {
135             $result->addRecord($this->get($id));
136         }
137
138         return $result;
139     }
140
141     /**
142      * Resolve path of multiple tree nodes
143      *
144      * @param Tinebase_Record_RecordSet|Tinebase_Model_Tree_Node $_records
145      */
146     public function resolveMultipleTreeNodesPath($_records)
147     {
148
149     }
150
151     /**
152      * Get tree node
153      * @see Tinebase_Controller_Record_Abstract::get()
154      * @param string $_id id for tree node
155      * @param string $_containerId id for container
156      */
157     public function get($_id, $_containerId = NULL)
158     {
159         $path = base64_decode($_id);
160         $node = $this->stat($path);
161         if (!$node) {
162             throw new Tinebase_Exception_NotFound('Node not found.');
163         }
164         return $this->stat($path);
165     }
166
167     /**
168      * search tree nodes
169      *
170      * @param Tinebase_Model_Filter_FilterGroup|optional $_filter
171      * @param Tinebase_Model_Pagination|optional $_pagination
172      * @param bool $_getRelations
173      * @param bool $_onlyIds
174      * @param string|optional $_action
175      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
176      */
177     public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL, $_getRelations = FALSE, $_onlyIds = FALSE, $_action = 'get')
178     {
179         $query = '';
180         $path = null;
181
182         if ($_filter->getFilter('query') && $_filter->getFilter('query')->getValue()) {
183             $query = $_filter->getFilter('query')->getValue();
184         }
185         if ($_filter->getFilter('path') && $_filter->getFilter('path')->getValue()) {
186             $path = $_filter->getFilter('path')->getValue();
187         }
188
189         if (($path === '/') || ($path == NULL)) {
190             return $this->_getRootAdapterNodes();
191         }
192
193         $backend = $this->getAdapterBackend($path);
194         $folderFiles = $backend->search($query, $this->removeUserBasePath($path));
195
196         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array(), TRUE);
197         foreach ($folderFiles as $folderFile) {
198             $result->addRecord($this->_createNodeFromRawData($folderFile, $backend->getName()));
199         }
200
201         if ($_filter->getFilter('type') && $_filter->getFilter('type')->getValue()) {
202             $result = $result->filter('type', $_filter->getFilter('type')->getValue());
203         }
204
205         $this->_searchTotalCount = $result->count();
206
207         $result->limitByPagination($_pagination);
208         $result->sort($_pagination->sort, $_pagination->dir);
209
210         return $result;
211     }
212
213     /**
214      *  return root node with an adapter
215      *
216      * @return Tinebase_Record_RecordSet
217      */
218     private function _getRootAdapterNodes()
219     {
220         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', array(), TRUE);
221         $config = Expressodriver_Controller::getInstance()->getConfigSettings();
222         foreach ($config['adapters'] as $adapter) {
223             $node = array(
224                 'name' => $adapter['name'],
225                 'path' => '/' . $adapter['name'],
226                 'id' => base64_encode('/' . $adapter['name']),
227                 'type' => Tinebase_Model_Tree_Node::TYPE_FOLDER,
228                 'contenttype' => 'application/octet-stream',
229                 'account_grants' => array('readGrant' => true, 'addGrant' => true),
230             );
231             $result->addRecord(new Tinebase_Model_Tree_Node($node, TRUE));
232         }
233
234         $this->_searchTotalCount = count($config['adapters']);
235         return $result;
236     }
237
238     /**
239      * returns a node by path
240      *
241      * @param string $_path
242      * @return Tinebase_Model_Tree_Node record of tree node
243      */
244     public function stat($_path)
245     {
246         $backend = $this->getAdapterBackend($_path);
247
248         if ($this->removeUserBasePath($_path) === '/') {
249             $data = array(
250                 'name' => $backend->getName(),
251                 'type' => Tinebase_Model_Tree_Node::TYPE_FOLDER
252             );
253         } else {
254             $data = $backend->stat($this->removeUserBasePath($_path));
255         }
256         return $this->_createNodeFromRawData($data, $backend->getName());
257     }
258
259     /**
260      * create a node from raw data sent by backend
261      *
262      * @param array $data
263      * @return Tinebase_Model_Tree_Node
264      */
265     private function _createNodeFromRawData($data, $adapterName)
266     {
267         $node = null;
268         if (!empty($data)) {
269             $data['path'] = '/' . $adapterName . (substr($data['path'], 0, 1) === '/' ? '' : '/') . $data['path'];
270             $data['id'] = base64_encode($data['path']);
271             $data['object_id'] = base64_encode($data['path']);
272
273             $node = new Tinebase_Model_Tree_Node($data, TRUE);
274         }
275         return $node;
276     }
277
278     /**
279      * remove user base path
280      *
281      * @param string $_path path
282      * @return string path
283      */
284     public function removeUserBasePath($_path)
285     {
286         $pathParts = explode('/', $_path);
287         $adapterName = $pathParts[1];
288         $completeBasePath = '/' . $adapterName;
289
290         if (strcmp($_path, $completeBasePath) === 0) {
291             $path = '/';
292         } else if (strpos($_path, $completeBasePath) === 0) {
293             $path = substr($_path, strlen($completeBasePath) + 1);
294         }
295         return $path;
296     }
297
298     /**
299      * checks filter acl and adds base path
300      *
301      * @param Tinebase_Model_Filter_FilterGroup $_filter
302      * @param string $_action get|update
303      * @return Tinebase_Model_Tree_Node_Path
304      * @throws Tinebase_Exception_AccessDenied
305      */
306     protected function _checkFilterACL(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
307     {
308         if ($_filter === NULL) {
309             $_filter = new Expressodriver_Model_NodeFilter();
310         }
311
312         $pathFilters = $_filter->getFilter('path', TRUE);
313         if (count($pathFilters) !== 1) {
314             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE))
315                 Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
316                         . 'Exactly one path filter required.');
317             $pathFilter = (count($pathFilters) > 1) ? $pathFilters[0] : new Tinebase_Model_Tree_Node_PathFilter(array(
318                 'field' => 'path',
319                 'operator' => 'equals',
320                 'value' => '/',)
321             );
322             $_filter->removeFilter('path');
323             $_filter->addFilter($pathFilter);
324         } else {
325             $pathFilter = $pathFilters[0];
326         }
327
328         // add base path and check grants
329         try {
330             $path = Tinebase_Model_Tree_Node_Path::createFromPath($this->addBasePath($pathFilter->getValue()));
331         } catch (Exception $e) {
332             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE))
333                 Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
334                         . ' Could not determine path, setting root path (' . $e->getMessage() . ')');
335             $path = Tinebase_Model_Tree_Node_Path::createFromPath($this->addBasePath('/'));
336         }
337         $pathFilter->setValue($path);
338
339         $this->_checkPathACL($path, $_action);
340
341         return $path;
342     }
343
344     /**
345      * get file node
346      *
347      * @param Tinebase_Model_Tree_Node_Path $_path
348      * @return Tinebase_Model_Tree_Node
349      */
350     public function getFileNode($_path)
351     {
352         $backend = $this->getAdapterBackend($_path);
353
354         if (!$backend->fileExists($this->removeUserBasePath($_path))) {
355             throw new Expressodriver_Exception('File does not exist,');
356         }
357
358         $node = $this->stat($_path);
359         if ($node->type === Tinebase_Model_Tree_Node::TYPE_FOLDER) {
360             throw new Expressodriver_Exception('Is a directory');
361         }
362
363         return $node;
364     }
365
366     /**
367      * add base path
368      *
369      * @param Tinebase_Model_Tree_Node_PathFilter $_pathFilter
370      * @return string
371      */
372     public function addBasePath($_path)
373     {
374         $basePath = $this->getApplicationBasePath(Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName));
375         $basePath .= '/folders';
376
377         $path = (strpos($_path, '/') === 0) ? $_path : '/' . $_path;
378         // only add base path once
379         $result = (!preg_match('@^' . preg_quote($basePath) . '@', $path)) ? $basePath . $path : $path;
380
381         return $result;
382     }
383
384     /**
385      * Gets total count of search with $_filter
386      *
387      * @param Tinebase_Model_Filter_FilterGroup $_filter
388      * @param string|optional $_action
389      * @return int
390      */
391     public function searchCount(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
392     {
393         //$path = $this->_checkFilterACL($_filter, $_action);
394         return $this->_searchTotalCount;
395     }
396
397     /**
398      * create node(s)
399      *
400      * @param array $_filenames
401      * @param string $_type directory or file
402      * @param array $_tempFileIds
403      * @param boolean $_forceOverwrite
404      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
405      */
406     public function createNodes($_filenames, $_type, $_tempFileIds = array(), $_forceOverwrite = FALSE)
407     {
408         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
409         $nodeExistsException = NULL;
410
411         foreach ($_filenames as $idx => $filename) {
412             $tempFileId = (isset($_tempFileIds[$idx])) ? $_tempFileIds[$idx] : NULL;
413
414             try {
415                 $node = $this->_createNode($filename, $_type, $tempFileId, $_forceOverwrite);
416                 $result->addRecord($node);
417             } catch (Expressodriver_Exception_NodeExists $fene) {
418                 $nodeExistsException = $this->_handleNodeExistsException($fene, $nodeExistsException);
419             }
420         }
421
422         if ($nodeExistsException) {
423             throw $nodeExistsException;
424         }
425         return $result;
426     }
427
428     /**
429      * collect information of a Expressodriver_Exception_NodeExists in a "parent" exception
430      *
431      * @param Expressodriver_Exception_NodeExists $_fene
432      * @param Expressodriver_Exception_NodeExists|NULL $_parentNodeExistsException
433      */
434     protected function _handleNodeExistsException($_fene, $_parentNodeExistsException = NULL)
435     {
436         // collect all nodes that already exist and add them to exception info
437         if (!$_parentNodeExistsException) {
438             $_parentNodeExistsException = new Expressodriver_Exception_NodeExists();
439         }
440
441         $nodesInfo = $_fene->getExistingNodesInfo();
442         if (count($nodesInfo) > 0) {
443             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
444                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
445                         . ' Adding node info to exception.');
446             $_parentNodeExistsException->addExistingNodeInfo($nodesInfo->getFirstRecord());
447         } else {
448             return $_fene;
449         }
450
451         return $_parentNodeExistsException;
452     }
453
454     /**
455      * create new node
456      *
457      * @param string $_path
458      * @param string $_type
459      * @param string $_tempFileId
460      * @param boolean $_forceOverwrite
461      * @return Tinebase_Model_Tree_Node
462      * @throws Tinebase_Exception_InvalidArgument
463      */
464     protected function _createNode($_path, $_type, $_tempFileId = NULL, $_forceOverwrite = FALSE)
465     {
466         if (!in_array($_type, array(Tinebase_Model_Tree_Node::TYPE_FILE, Tinebase_Model_Tree_Node::TYPE_FOLDER))) {
467             throw new Tinebase_Exception_InvalidArgument('Type ' . $_type . 'not supported.');
468         }
469
470         try {
471             $this->_checkIfExists($_path);
472         } catch (Expressodriver_Exception_NodeExists $fene) {
473             if ($_forceOverwrite) {
474                 $existingNode = $this->stat($_path);
475                 if (!$_tempFileId) {
476                     return $existingNode;
477                 }
478             } else if (!$_forceOverwrite) {
479                 throw $fene;
480             }
481         }
482         $newNode = $this->_createNodeInBackend($_path, $_type, $_tempFileId);
483         $backend = $this->getAdapterBackend($_path);
484
485         if ($newNode === NULL) {
486             switch ($_type) {
487                 case Tinebase_Model_Tree_Node::TYPE_FILE:
488                     // on upload, if file node was not created in backend we create a virtual node as expected by frontend
489                     $newNode = $this->_createNodeFromRawData(
490                             array(
491                         'path' => $this->removeUserBasePath($_path),
492                         'name' => urldecode(basename($_path)),
493                         'type' => $_type,
494                         'size' => 0,
495                         'contenttype' => 'inode/x-empty',
496                             ), $backend->getName()
497                     );
498                     break;
499                 case Tinebase_Model_Tree_Node::TYPE_FOLDER:
500                     throw new Tinebase_Exception_NotFound('Node not created.');
501                     break;
502             }
503         }
504
505         return $newNode;
506     }
507
508     /**
509      * create node in backend
510      *
511      * @param string $_statpath
512      * @param type
513      * @param string $_tempFileId
514      * @return Tinebase_Model_Tree_Node
515      */
516     protected function _createNodeInBackend($_statpath, $_type, $_tempFileId = NULL)
517     {
518         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
519             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
520                     ' Creating new path ' . $_statpath . ' of type ' . $_type);
521
522         $backend = $this->getAdapterBackend($_statpath);
523         switch ($_type) {
524             case Tinebase_Model_Tree_Node::TYPE_FILE:
525                 if ($_tempFileId !== NULL) {
526                     $tempFile = ($_tempFileId instanceof Tinebase_Model_TempFile) ? $_tempFileId : Tinebase_TempFile::getInstance()->getTempFile($_tempFileId);
527                     $backend->uploadFile($tempFile->path, $this->removeUserBasePath($_statpath));
528                 }
529                 break;
530             case Tinebase_Model_Tree_Node::TYPE_FOLDER:
531                 $backend->mkdir($this->removeUserBasePath($_statpath));
532                 break;
533         }
534         return $this->stat($_statpath);
535     }
536
537     /**
538      * check file existance
539      *
540      * @param Tinebase_Model_Tree_Node_Path $_path
541      * @param Tinebase_Model_Tree_Node $_node
542      * @throws Expressodriver_Exception_NodeExists
543      */
544     protected function _checkIfExists($_path, $_node = NULL)
545     {
546         $backend = $this->getAdapterBackend($_path);
547         if ($backend->fileExists($this->removeUserBasePath($_path))) {
548             $existsException = new Expressodriver_Exception_NodeExists();
549             if ($_node === NULL) {
550                 $existsException->addExistingNodeInfo($this->stat($_path));
551             } else {
552                 $existsException->addExistingNodeInfo($_node);
553             }
554             throw $existsException;
555         }
556     }
557
558     /**
559      * check acl of path
560      *
561      * @param Tinebase_Model_Tree_Node_Path $_path
562      * @param string $_action
563      * @param boolean $_topLevelAllowed
564      * @throws Tinebase_Exception_AccessDenied
565      */
566     protected function _checkPathACL(Tinebase_Model_Tree_Node_Path $_path, $_action = 'get', $_topLevelAllowed = TRUE)
567     {
568         $hasPermission = FALSE;
569
570         if ($_path->container) {
571             $hasPermission = $this->_checkACLContainer($_path->container, $_action);
572         } else if ($_topLevelAllowed) {
573             switch ($_path->containerType) {
574                 case Tinebase_Model_Container::TYPE_PERSONAL:
575                     if ($_path->containerOwner) {
576                         $hasPermission = ($_path->containerOwner === Tinebase_Core::getUser()->accountLoginName || $_action === 'get');
577                     } else {
578                         $hasPermission = ($_action === 'get');
579                     }
580                     break;
581                 case Tinebase_Model_Container::TYPE_SHARED:
582                     $hasPermission = ($_action !== 'get') ? $this->checkRight(Tinebase_Acl_Rights::MANAGE_SHARED_FOLDERS, FALSE) : TRUE;
583                     break;
584                 case Tinebase_Model_Tree_Node_Path::TYPE_ROOT:
585                     $hasPermission = ($_action === 'get');
586                     break;
587                 default :
588                     $hasPermission = TRUE;
589             }
590         } else {
591             // @todo: check acl for path
592             $hasPermission = TRUE;
593         }
594
595         if (!$hasPermission) {
596             throw new Tinebase_Exception_AccessDenied('No permission to ' . $_action . ' nodes in path ' . $_path->flatpath);
597         }
598     }
599
600     /**
601      * copy nodes
602      *
603      * @param array $_sourceFilenames array->multiple
604      * @param string|array $_destinationFilenames string->singlefile OR directory, array->multiple files
605      * @param boolean $_forceOverwrite
606      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
607      */
608     public function copyNodes($_sourceFilenames, $_destinationFilenames, $_forceOverwrite = FALSE)
609     {
610         return $this->_copyOrMoveNodes($_sourceFilenames, $_destinationFilenames, 'copy', $_forceOverwrite);
611     }
612
613     /**
614      * copy or move an array of nodes identified by their path
615      *
616      * @param array $_sourceFilenames array->multiple
617      * @param string|array $_destinationFilenames string->singlefile OR directory, array->multiple files
618      * @param string $_action copy|move
619      * @param boolean $_forceOverwrite
620      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
621      */
622     protected function _copyOrMoveNodes($_sourceFilenames, $_destinationFilenames, $_action, $_forceOverwrite = FALSE)
623     {
624         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
625         $nodeExistsException = NULL;
626
627         foreach ($_sourceFilenames as $idx => $sourcePathRecord) {
628             $destinationPathRecord = $this->_getDestinationPath($_destinationFilenames, $idx, $sourcePathRecord);
629
630             try {
631                 if ($_action === 'move') {
632                     $node = $this->_moveNode($sourcePathRecord, $destinationPathRecord, $_forceOverwrite);
633                 } else if ($_action === 'copy') {
634                     $node = $this->_copyNode($sourcePathRecord, $destinationPathRecord, $_forceOverwrite);
635                 }
636                 $result->addRecord($node);
637             } catch (Expressodriver_Exception_NodeExists $fene) {
638                 $nodeExistsException = $this->_handleNodeExistsException($fene, $nodeExistsException);
639             }
640         }
641
642         if ($nodeExistsException) {
643             // @todo add correctly moved/copied files here?
644             throw $nodeExistsException;
645         }
646         return $result;
647     }
648
649     /**
650      * get single destination from an array of destinations and an index + $_sourcePathRecord
651      *
652      * @param string|array $_destinationFilenames
653      * @param int $_idx
654      * @param Tinebase_Model_Tree_Node_Path $_sourcePathRecord
655      * @return Tinebase_Model_Tree_Node_Path
656      * @throws Expressodriver_Exception
657      *
658      * @todo add Tinebase_FileSystem::isDir() check?
659      */
660     protected function _getDestinationPath($_destinationFilenames, $_idx, $_sourcePathRecord)
661     {
662         if (is_array($_destinationFilenames)) {
663             $isdir = FALSE;
664             if (isset($_destinationFilenames[$_idx])) {
665                 $destination = $_destinationFilenames[$_idx];
666             } else {
667                 throw new Expressodriver_Exception('No destination path found.');
668             }
669         } else {
670             $isdir = TRUE;
671             $destination = $_destinationFilenames;
672         }
673
674         if ($isdir) {
675             $destination = $destination . '/' . $_sourcePathRecord;
676         }
677         return $destination;
678     }
679
680     /**
681      * copy single node
682      *
683      * @param Tinebase_Model_Tree_Node_Path $_source
684      * @param Tinebase_Model_Tree_Node_Path $_destination
685      * @param boolean $_forceOverwrite
686      * @return Tinebase_Model_Tree_Node
687      */
688     protected function _copyNode(Tinebase_Model_Tree_Node_Path $_source, Tinebase_Model_Tree_Node_Path $_destination, $_forceOverwrite = FALSE)
689     {
690         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
691             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
692                     . ' Copy Node ' . $_source->flatpath . ' to ' . $_destination->flatpath);
693
694         $newNode = NULL;
695
696         $this->_checkPathACL($_source, 'get', FALSE);
697
698         $app = Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName);
699         $path = Tinebase_Model_Tree_Node_Path::removeAppIdFromPath($_source->flatpath, $app);
700         $sourceNode = $this->stat($path);
701
702         switch ($sourceNode->type) {
703             case Tinebase_Model_Tree_Node::TYPE_FILE:
704                 $newNode = $this->_copyOrMoveFileNode($_source, $_destination, 'copy', $_forceOverwrite);
705                 break;
706             case Tinebase_Model_Tree_Node::TYPE_FOLDER:
707                 $newNode = $this->_copyFolderNode($_source, $_destination);
708                 break;
709         }
710
711         return $newNode;
712     }
713
714     /**
715      * copy file node
716      *
717      * @param Tinebase_Model_Tree_Node_Path $_source
718      * @param Tinebase_Model_Tree_Node_Path $_destination
719      * @param string $_action
720      * @param boolean $_forceOverwrite
721      * @return Tinebase_Model_Tree_Node
722      */
723     protected function _copyOrMoveFileNode($_source, $_destination, $_action, $_forceOverwrite = FALSE)
724     {
725         $destinationPath = $_destination;
726
727         $backend = $this->getAdapterBackend($_destination);
728
729         try {
730             $this->_checkIfExists($destinationPath);
731         } catch (Expressodriver_Exception_NodeExists $fene) {
732             if ($_forceOverwrite && $_source->statpath !== $_destination->statpath) {
733                 // delete old node
734                 $backend->unlink($this->removeUserBasePath($destinationPath));
735             } elseif (!$_forceOverwrite) {
736                 throw $fene;
737             }
738         }
739
740         $sourcePath = $_source;
741         switch ($_action) {
742             case 'copy':
743
744                 $backend->copy($this->removeUserBasePath($sourcePath->path), $this->removeUserBasePath($destinationPath));
745                 break;
746             case 'move':
747                 $backend->rename($this->removeUserBasePath($sourcePath->path), $this->removeUserBasePath($destinationPath));
748                 break;
749         }
750         $newNode = $this->stat($destinationPath);
751         if (!$newNode) {
752             throw new Tinebase_Exception_AccessDenied('Operation failed');
753         }
754         return $newNode;
755     }
756
757     /**
758      * copy folder node
759      *
760      * @param Tinebase_Model_Tree_Node_Path $_source
761      * @param Tinebase_Model_Tree_Node_Path $_destination
762      * @return Tinebase_Model_Tree_Node
763      * @throws Expressodriver_Exception_NodeExists
764      *
765      * @todo add $_forceOverwrite?
766      */
767     protected function _copyFolderNode(Tinebase_Model_Tree_Node_Path $_source, Tinebase_Model_Tree_Node_Path $_destination)
768     {
769         $newNode = $this->_createNode($_destination, Tinebase_Model_Tree_Node::TYPE_FOLDER);
770
771         // recursive copy for (sub-)folders/files
772         $filter = new Tinebase_Model_Tree_Node_Filter(array(array(
773                 'field' => 'path',
774                 'operator' => 'equals',
775                 'value' => Tinebase_Model_Tree_Node_Path::removeAppIdFromPath(
776                         $_source->flatpath, Tinebase_Application::getInstance()->getApplicationByName($this->_applicationName)
777                 ),
778         )));
779         $result = $this->search($filter);
780         if (count($result) > 0) {
781             $this->copyNodes($result->path, $newNode->path);
782         }
783
784         return $newNode;
785     }
786
787     /**
788      * move nodes
789      *
790      * @param array $_sourceFilenames array->multiple
791      * @param string|array $_destinationFilenames string->singlefile OR directory, array->multiple files
792      * @param boolean $_forceOverwrite
793      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
794      */
795     public function moveNodes($_sourceFilenames, $_destinationFilenames, $_forceOverwrite = FALSE)
796     {
797         return $this->_copyOrMoveNodes($_sourceFilenames, $_destinationFilenames, 'move', $_forceOverwrite);
798     }
799
800     /**
801      * move single node
802      *
803      * @param Tinebase_Model_Tree_Node_Path $_source
804      * @param Tinebase_Model_Tree_Node_Path $_destination
805      * @param boolean $_forceOverwrite
806      * @return Tinebase_Model_Tree_Node
807      */
808     protected function _moveNode($_source, $_destination, $_forceOverwrite = FALSE)
809     {
810         $sourceNode = $this->stat($_source);
811
812         if (!$sourceNode) {
813             throw new Tinebase_Exception_NotFound('Node not moved. Maybe the node was removed.');
814         }
815
816         switch ($sourceNode->type) {
817             case Tinebase_Model_Tree_Node::TYPE_FILE:
818                 $movedNode = $this->_copyOrMoveFileNode($sourceNode, $_destination, 'move', $_forceOverwrite);
819                 break;
820             case Tinebase_Model_Tree_Node::TYPE_FOLDER:
821                 $movedNode = $this->_moveFolderNode($_source, $sourceNode, $_destination, $_forceOverwrite);
822                 break;
823         }
824
825         return $movedNode;
826     }
827
828     /**
829      * move folder node
830      *
831      * @param Tinebase_Model_Tree_Node_Path $source
832      * @param Tinebase_Model_Tree_Node $sourceNode [unused]
833      * @param Tinebase_Model_Tree_Node_Path $destination
834      * @param boolean $_forceOverwrite
835      * @return Tinebase_Model_Tree_Node
836      */
837     protected function _moveFolderNode($source, $sourceNode, $destination, $_forceOverwrite = FALSE)
838     {
839         $backend = $this->getAdapterBackend($destination);
840
841         try {
842             $this->_checkIfExists($destination);
843         } catch (Expressodriver_Exception_NodeExists $fene) {
844             if ($_forceOverwrite && $source !== $destination) {
845                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
846                     Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
847                             . ' Removing folder node ' . $destination->statpath);
848                 $backend->rmdir($this->removeUserBasePath($destination), TRUE);
849             } else if (!$_forceOverwrite) {
850                 throw $fene;
851             }
852         }
853
854         $backend->rename($this->removeUserBasePath($source), $this->removeUserBasePath($destination));
855
856         $movedNode = $this->stat($destination);
857
858
859         if (!$movedNode) {
860             throw new Tinebase_Exception_AccessDenied('Operation failed');
861         }
862
863         return $movedNode;
864     }
865
866     /**
867      * move folder container
868      *
869      * @param Tinebase_Model_Tree_Node_Path $source
870      * @param Tinebase_Model_Tree_Node_Path $destination
871      * @param boolean $forceOverwrite
872      * @return Tinebase_Model_Tree_Node
873      */
874     protected function _moveFolderContainer($source, $destination, $forceOverwrite = FALSE)
875     {
876         if ($source->isToplevelPath()) {
877             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
878                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
879                         . ' Moving container ' . $source->container->name . ' to ' . $destination->flatpath);
880
881             $this->_checkACLContainer($source->container, 'update');
882             $backend = $this->getAdapterBackend($destination->statpath);
883             $container = $source->container;
884             if ($container->name !== $destination->name) {
885                 try {
886                     $existingContainer = Tinebase_Container::getInstance()->getContainerByName(
887                             $this->_applicationName, $destination->name, $destination->containerType, Tinebase_Core::getUser()
888                     );
889                     if (!$forceOverwrite) {
890                         $fene = new Expressodriver_Exception_NodeExists('container exists');
891                         $fene->addExistingNodeInfo($backend->stat($destination->statpath));
892                         throw $fene;
893                     } else {
894                         if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
895                             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
896                                     . ' Removing existing folder node and container ' . $destination->flatpath);
897                         $backend->rmdir($destination->statpath, TRUE);
898                     }
899                 } catch (Tinebase_Exception_NotFound $tenf) {
900                     // ok
901                 }
902
903                 $container->name = $destination->name;
904                 $container = Tinebase_Container::getInstance()->update($container);
905             }
906         } else {
907             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
908                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
909                         . ' Creating container ' . $destination->name);
910             $container = $this->_createContainer($destination->name, $destination->containerType);
911         }
912
913         $destination->setContainer($container);
914     }
915
916     /**
917      * delete nodes
918      *
919      * @param array $_filenames string->single file, array->multiple
920      * @return int delete count
921      *
922      * @todo add recursive param?
923      */
924     public function deleteNodes($_filenames)
925     {
926         $deleteCount = 0;
927         foreach ($_filenames as $filename) {
928             if ($this->_deleteNode($filename)) {
929                 $deleteCount++;
930             }
931         }
932
933         return $deleteCount;
934     }
935
936     /**
937      * delete node
938      *
939      * @param string $_flatpath
940      * @return boolean
941      * @throws Tinebase_Exception_NotFound
942      */
943     protected function _deleteNode($_flatpath)
944     {
945         $success = $this->_deleteNodeInBackend($_flatpath);
946         // @todo: some improvement here if we have container as parent folder
947         return $success;
948     }
949
950     /**
951      * delete node in backend
952      *
953      * @param Tinebase_Model_Tree_Node_Path $_path
954      * @return boolean
955      */
956     protected function _deleteNodeInBackend($_path)
957     {
958         $success = FALSE;
959
960         $node = $this->stat($_path);
961         $backend = $this->getAdapterBackend($_path);
962
963         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
964             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
965                     ' Removing path ' . $_path->flatpath . ' of type ' . $node->type);
966
967         switch ($node->type) {
968             case Tinebase_Model_Tree_Node::TYPE_FILE:
969                 $success = $backend->unlink($this->removeUserBasePath($_path));
970                 break;
971             case Tinebase_Model_Tree_Node::TYPE_FOLDER:
972                 $success = $backend->rmdir($this->removeUserBasePath($_path), TRUE);
973                 break;
974         }
975
976         return $success;
977     }
978
979     /**
980      * Deletes a set of records.
981      *
982      * If one of the records could not be deleted, no record is deleted
983      *
984      * @param   array array of record identifiers
985      * @return  Tinebase_Record_RecordSet
986      */
987     public function delete($_ids)
988     {
989         $nodes = $this->getMultiple($_ids);
990         foreach ($nodes as $node) {
991             $checkACL = true; // @todo: check node delete acl
992             if ($checkACL) {
993                 $this->_deleteNode($node->path);
994             } else {
995                 $nodes->removeRecord($node);
996             }
997         }
998
999         return $nodes;
1000     }
1001
1002     /**
1003      * get application base path
1004      *
1005      * @param Tinebase_Model_Application|string $_application
1006      * @param string $_type
1007      * @return string
1008      */
1009     public function getApplicationBasePath($_application, $_type = NULL)
1010     {
1011         $application = $_application instanceof Tinebase_Model_Application ? $_application : Tinebase_Application::getInstance()->getApplicationById($_application);
1012
1013         $result = '/' . $application->getId();
1014
1015         if ($_type !== NULL) {
1016             if (!in_array($_type, array(Tinebase_Model_Container::TYPE_SHARED, Tinebase_Model_Container::TYPE_PERSONAL, self::FOLDER_TYPE_RECORDS))) {
1017                 throw new Tinebase_Exception_UnexpectedValue('Type can only be shared or personal.');
1018             }
1019
1020             $result .= '/folders/' . $_type;
1021         }
1022
1023         return $result;
1024     }
1025
1026     /**
1027      * (non-PHPdoc)
1028      * @see Tinebase_Controller_Record_Abstract::update()
1029      */
1030     public function update(Tinebase_Record_Interface $_record)
1031     {
1032         return $_record;
1033     }
1034
1035     /**
1036      *Create tree node
1037      *
1038      * @param Tinebase_Record_Interface $_record
1039      * @param boolean $_duplicateCheck
1040      * @param boolean $_getOnReturn
1041      * @return Tinebase_Record_Interface
1042      */
1043     public function create(Tinebase_Record_Interface $_record, $_duplicateCheck = TRUE, $_getOnReturn = TRUE)
1044     {
1045         return $_record;
1046     }
1047
1048     /**
1049      * get an adapter instance according to the path
1050      *
1051      * pathParts:
1052      * [0] =>
1053      * [1] => external
1054      * [2] => accountLogin
1055      * [3] => adapterName
1056      * [4..] => path in backend
1057      *
1058      * @param string $_path
1059      * @return Expressodriver_Backend_Adapter_Interface
1060      * @throws Expressodriver_Exception
1061      */
1062     public function getAdapterBackend($_path)
1063     {
1064         $pathParts = explode('/', $_path);
1065         $adapterName = $pathParts[1];
1066
1067         if (!isset(self::$_backends[$adapterName])) {
1068
1069             $adapter = null;
1070             $config = Expressodriver_Controller::getInstance()->getConfigSettings();
1071             foreach ($config['adapters'] as $adapterConfig) {
1072                 if ($adapterName === $adapterConfig['name']) {
1073                     $adapter = $adapterConfig;
1074                 }
1075             }
1076             if (!is_null($adapter)) {
1077
1078                 $credentialsBackend = Tinebase_Auth_CredentialCache::getInstance();
1079                 $userCredentialCache = Tinebase_Core::getUserCredentialCache();
1080                 $credentialsBackend->getCachedCredentials($userCredentialCache);
1081
1082                 $password = !(empty($userCredentialCache->password)) ?
1083                         $userCredentialCache->password :
1084                         Expressodriver_Session::getSessionNamespace()->password[$adapterName];
1085
1086                 if (empty($password)) {
1087                     $exception = new Expressodriver_Exception_CredentialsRequired();
1088                     $exception->setAdapterName($adapterName);
1089                     throw $exception;
1090                 }
1091
1092                 $username = $adapter['useEmailAsLoginName']
1093                         ? Tinebase_Core::getUser()->accountEmailAddress
1094                         : Tinebase_Core::getUser()->accountLoginName;
1095
1096                 $options = array(
1097                     'host' => $adapter['url'],
1098                     'user' => $username,
1099                     'password' => $password,
1100                     'root' => '/',
1101                     'name' => $adapter['name'],
1102                     'useCache' => $config['default']['useCache'],
1103                     'cacheLifetime' => $config['default']['cacheLifetime'],
1104                 );
1105
1106                 self::$_backends[$adapterName] = Expressodriver_Backend_Storage_Abstract::factory($adapter['adapter'], $options);
1107             } else {
1108                 throw new Expressodriver_Exception('Adapter config does not exists');
1109             }
1110         }
1111         return self::$_backends[$adapterName];
1112     }
1113
1114     /**
1115      * (non-PHPdoc)
1116      * @see tine20/Tinebase/Controller/Record/Interface::getAll()
1117      */
1118     public function getAll($_orderBy = 'id', $_orderDirection = 'ASC')
1119     {
1120     }
1121
1122     /**
1123      * (non-PHPdoc)
1124      * @see tine20/Tinebase/Controller/Record/Interface::updateMultiple()
1125      */
1126     public function updateMultiple($_what, $_data)
1127     {
1128     }
1129
1130 }