Merge branch '2016.11-develop' into 2017.02
[tine20] / tine20 / Tinebase / Container.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Tinebase
6  * @subpackage  Container
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2007-2017 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Lars Kneschke <l.kneschke@metaways.de>
10  * 
11  * @todo        refactor that: remove code duplication, remove Zend_Db_Table_Abstract usage, use standard record controller/backend functions
12  *              -> make use of Tinebase_Backend_Sql_Grants
13  * @todo        move (or replace from) functions to backend
14  * @todo        switch containers to hash ids
15  */
16
17 /**
18  * this class handles access rights(grants) to containers
19  * 
20  * any record in Tine 2.0 is tied to a container. the rights of an account on a record gets 
21  * calculated by the grants given to this account on the container holding the record (if you know what i mean ;-))
22  * 
23  * @package     Tinebase
24  * @subpackage  Acl
25  */
26 class Tinebase_Container extends Tinebase_Backend_Sql_Abstract implements Tinebase_Controller_SearchInterface, Tinebase_Container_Interface
27 {
28     use Tinebase_Controller_Record_ModlogTrait;
29
30     /**
31      * Table name without prefix
32      *
33      * @var string
34      */
35     protected $_tableName = 'container';
36     
37     /**
38      * Model name
39      *
40      * @var string
41      */
42     protected $_modelName = 'Tinebase_Model_Container';
43     
44     /**
45      * if modlog is active, we add 'is_deleted = 0' to select object in _getSelect()
46      *
47      * @var boolean
48      */
49     protected $_modlogActive = TRUE;
50     
51     /**
52      * the table object for the container_acl table
53      *
54      * @var Zend_Db_Table_Abstract
55      */
56     protected $_containerAclTable;
57     
58     /**
59      * container content history backend
60      * 
61      * @var Tinebase_Backend_Sql
62      */
63     protected $_contentBackend = NULL;
64     
65     /**
66      * don't clone. Use the singleton.
67      *
68      */
69     private function __clone() {}
70     
71     /**
72      * holds the instance of the singleton
73      *
74      * @var Tinebase_Container
75      */
76     private static $_instance = NULL;
77     
78     /**
79      * default column(s) for count
80      *
81      * @var string
82      */
83     protected $_defaultCountCol = 'id';
84     
85     /**
86      * cache timeout for ACL related cache entries (in seconds)
87      * 
88      * @see 0007266: make groups / group memberships cache cleaning more efficient
89      * @var integer
90      */
91     const ACL_CACHE_TIMEOUT = 30;
92     
93     /**
94      * the singleton pattern
95      *
96      * @return Tinebase_Container
97      */
98     public static function getInstance() 
99     {
100         if (self::$_instance === NULL) {
101             self::$_instance = new Tinebase_Container();
102         }
103         
104         return self::$_instance;
105     }
106     
107     /**
108      * get content backend
109      * 
110      * @return Tinebase_Backend_Sql
111      * 
112      * @todo move this to constructor when this no longer extends Tinebase_Backend_Sql_Abstract
113      */
114     public function getContentBackend()
115     {
116         if ($this->_contentBackend === NULL) {
117             $this->_contentBackend  = new Tinebase_Backend_Sql(array(
118                 'modelName' => 'Tinebase_Model_ContainerContent', 
119                 'tableName' => 'container_content',
120             ));
121         }
122         
123         return $this->_contentBackend;
124     }
125
126     /**
127      * get list of records
128      *
129      * @param Tinebase_Model_Filter_FilterGroup $_filter
130      * @param Tinebase_Model_Pagination $_pagination
131      * @param bool $_getRelations
132      * @param bool $_onlyIds
133      * @param string $_action
134      * @return Tinebase_Record_RecordSet
135      */
136     public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Model_Pagination $_pagination = NULL, $_getRelations = FALSE, $_onlyIds = FALSE, $_action = 'get')
137     {
138         return parent::search($_filter, $_pagination, $_onlyIds);
139     }
140
141     /**
142      * Gets total count of search with $_filter
143      *
144      * @param Tinebase_Model_Filter_FilterGroup $_filter
145      * @param string $_action
146      * @return int
147      */
148     public function searchCount(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
149     {
150         return parent::searchCount($_filter);
151     }
152     
153     /**
154      * creates a new container
155      *
156      * @param   Tinebase_Model_Container $_container the new container
157      * @param   Tinebase_Record_RecordSet $_grants the grants for the new folder 
158      * @param   bool  $_ignoreAcl
159      * @return  Tinebase_Model_Container the newly created container
160      * @throws  Tinebase_Exception_Record_Validation
161      * @throws  Tinebase_Exception_AccessDenied
162      */
163     public function addContainer(Tinebase_Model_Container $_container, $_grants = NULL, $_ignoreAcl = FALSE)
164     {
165         $_container->isValid(TRUE);
166         
167         if ($_ignoreAcl !== TRUE) {
168             switch ($_container->type) {
169                 case Tinebase_Model_Container::TYPE_PERSONAL:
170                     // is the user allowed to create personal container?
171                     break;
172                     
173                 case Tinebase_Model_Container::TYPE_SHARED:
174                     $application = Tinebase_Application::getInstance()->getApplicationById($_container->application_id);
175                     $appName = (string) $application;
176                     $manageRight = FALSE;
177                     
178                     // check for MANAGE_SHARED_FOLDERS right
179                     $appAclClassName = $appName . '_Acl_Rights';
180                     if (@class_exists($appAclClassName)) {
181                         $appAclObj = call_user_func(array($appAclClassName, 'getInstance'));
182                         $allRights = $appAclObj->getAllApplicationRights();
183                         if (in_array(Tinebase_Acl_Rights::MANAGE_SHARED_FOLDERS, $allRights)) {
184                             $manageRight = Tinebase_Core::getUser()->hasRight($appName, Tinebase_Acl_Rights::MANAGE_SHARED_FOLDERS);
185                         }
186                     }
187                     
188
189                     if(!$manageRight && !Tinebase_Core::getUser()->hasRight($appName, Tinebase_Acl_Rights::ADMIN)) {
190                         throw new Tinebase_Exception_AccessDenied('Permission to add shared container denied.');
191                     }
192                     break;
193                     
194                 default:
195                     throw new Tinebase_Exception_InvalidArgument('Can add personal or shared folders only when ignoring ACL.');
196                     break;
197             }
198         }
199         
200         if (! empty($_container->owner_id)) {
201             $accountId = $_container->owner_id instanceof Tinebase_Model_User ? $_container->owner_id->getId() : $_container->owner_id;
202         } else {
203             $accountId = (is_object(Tinebase_Core::getUser())) ? Tinebase_Core::getUser()->getId() : NULL;
204             if ($_container->type === Tinebase_Model_Container::TYPE_PERSONAL) {
205                 $_container->owner_id = $accountId;
206             }
207         }
208         
209         if($_grants === NULL || count($_grants) == 0) {
210             $grants = Tinebase_Model_Grants::getPersonalGrants($accountId);
211             if (    $_container->type === Tinebase_Model_Container::TYPE_SHARED
212                  && ! Tinebase_Config::getInstance()->get(Tinebase_Config::ANYONE_ACCOUNT_DISABLED)) {
213     
214                 // add all grants to creator and
215                 // add read grants to any other user
216                 $grants->addRecord(new Tinebase_Model_Grants(array(
217                     'account_id'      => '0',
218                     'account_type'    => Tinebase_Acl_Rights::ACCOUNT_TYPE_ANYONE,
219                     Tinebase_Model_Grants::GRANT_READ    => true,
220                     Tinebase_Model_Grants::GRANT_EXPORT  => true,
221                     Tinebase_Model_Grants::GRANT_SYNC    => true,
222                 ), TRUE));
223             }
224         } else {
225             $grants = $_grants;
226         }
227         
228         $event = new Tinebase_Event_Container_BeforeCreate();
229         $event->accountId = $accountId;
230         $event->container = $_container;
231         $event->grants = $grants;
232         Tinebase_Event::fireEvent($event);
233         
234         Tinebase_Timemachine_ModificationLog::setRecordMetaData($_container, 'create');
235         $container = $this->create($_container);
236         $this->setGrants($container->getId(), $grants, TRUE, FALSE);
237
238         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::'
239             . __LINE__ . ' Created new ' . $_container->type . ' container for account id ' . $accountId);
240
241         return $container;
242     }
243
244     /**
245      * do something after creation of record
246      *
247      * @param Tinebase_Record_Interface $_newRecord
248      * @param Tinebase_Record_Interface $_recordToCreate
249      * @return void
250      */
251     protected function _inspectAfterCreate(Tinebase_Record_Interface $_newRecord, Tinebase_Record_Interface $_recordToCreate)
252     {
253         $this->_writeModLog($_newRecord, null);
254     }
255     
256     /**
257      * add grants to container
258      *
259      * @todo    check that grant is not already given to container/type/account combination
260      * @param   int|Tinebase_Model_Container $_containerId
261      * @param   int $_accountId
262      * @param   array $_grants list of grants to add
263      * @return  boolean
264      * @throws  Tinebase_Exception_AccessDenied
265      */
266     public function addGrants($_containerId, $_accountType, $_accountId, array $_grants, $_ignoreAcl = FALSE)
267     {
268         $containerId = Tinebase_Model_Container::convertContainerId($_containerId);
269         
270         if($_ignoreAcl !== TRUE and !$this->hasGrant(Tinebase_Core::getUser(), $_containerId, Tinebase_Model_Grants::GRANT_ADMIN)) {
271             throw new Tinebase_Exception_AccessDenied('Permission to manage grants on container denied.');
272         }
273         
274         switch($_accountType) {
275             case Tinebase_Acl_Rights::ACCOUNT_TYPE_USER:
276                 $accountId = Tinebase_Model_User::convertUserIdToInt($_accountId);
277                 break;
278             case Tinebase_Acl_Rights::ACCOUNT_TYPE_GROUP:
279                 $accountId = Tinebase_Model_Group::convertGroupIdToInt($_accountId);
280                 break;
281             case Tinebase_Acl_Rights::ACCOUNT_TYPE_ANYONE:
282                 $accountId = '0';
283                 break;
284             case Tinebase_Acl_Rights::ACCOUNT_TYPE_ROLE:
285                 $accountId = Tinebase_Model_Role::convertRoleIdToInt($_accountId);
286                 break;
287             default:
288                 throw new Tinebase_Exception_InvalidArgument('invalid $_accountType');
289                 break;
290         }
291         
292         $containerGrants = $this->getGrantsOfContainer($containerId, TRUE);
293         $containerGrants->addIndices(array('account_type', 'account_id'));
294         $existingGrants = $containerGrants->filter('account_type', $_accountType)->filter('account_id', $_accountId)->getFirstRecord();
295         
296         foreach($_grants as $grant) {
297             if ($existingGrants === NULL || ! $existingGrants->{$grant}) {
298                 $data = array(
299                     'id'            => Tinebase_Record_Abstract::generateUID(),
300                     'container_id'  => $containerId,
301                     'account_type'  => $_accountType,
302                     'account_id'    => $accountId,
303                     'account_grant' => $grant
304                 );
305                 $this->_getContainerAclTable()->insert($data);
306             }
307         }
308
309         $newGrants = $this->getGrantsOfContainer($containerId, true);
310         $this->_writeModLog(
311             new Tinebase_Model_Container(array('id' => $containerId, 'account_grants' => $newGrants), true),
312             new Tinebase_Model_Container(array('id' => $containerId, 'account_grants' => $containerGrants), true)
313         );
314         
315         $this->_setRecordMetaDataAndUpdate($containerId, 'update');
316         
317         return true;
318     }
319     
320     /**
321      * resolves the $_recordClass argument for legacy handling: $_recordClass was before $_application
322      * in getDefaultContainer and getPersonalContainer
323      * @param string|Tinebase_Record_Interface $_recordClass
324      * @throws Tinebase_Exception_InvalidArgument
325      */
326     protected function _resolveRecordClassArgument($_recordClass)
327     {
328         $ret = array();
329         if(is_string($_recordClass)) {
330             $split = explode('_', $_recordClass);
331             switch(count($split)) {
332                 case 1:
333                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::'
334                         . __LINE__ . ' Using application name is deprecated. Please use the classname of the model or the class itself.');
335                     $ret['appName'] = $_recordClass;
336                     if(! $ret['recordClass'] = Tinebase_Core::getApplicationInstance($_recordClass)->getDefaultModel()) {
337                         throw new Tinebase_Exception_NotFound('A default model could not be found for application ' . $_recordClass);
338                     }
339                     break;
340                 case 3: 
341                     $ret['appName'] = $split[0];
342                     $ret['recordClass'] = $_recordClass;
343                     break;
344                 default: throw new Tinebase_Exception_InvalidArgument('Invalid value as recordClass given: ' . print_r($_recordClass, 1));
345             }
346         } elseif (in_array('Tinebase_Record_Interface', class_implements($_recordClass))) {
347             $ret['appName'] = $_recordClass->getApplication();
348             $ret['recordClass'] = get_class($_recordClass);
349         } else {
350             throw new Tinebase_Exception_InvalidArgument('Invalid value as recordClass given: ' . print_r($_recordClass, 1));
351         }
352         
353         return $ret;
354     }
355     
356     /**
357      * set modified timestamp for container
358      * 
359      * @param int|Tinebase_Model_Container $container
360      * @param string                       $action    one of {create|update|delete}
361      * @return Tinebase_Model_Container
362      */
363     protected function _setRecordMetaDataAndUpdate($container, $action)
364     {
365         if (! $container instanceof Tinebase_Model_Container) {
366             $container = $this->getContainerById($container);
367         }
368         Tinebase_Timemachine_ModificationLog::getInstance()->setRecordMetaData($container, $action, $container);
369         
370         $this->_clearCache($container);
371         
372         return $this->update($container, true);
373     }
374
375     /**
376      * return all container, which the user has the requested right for
377      *
378      * used to get a list of all containers accesssible by the current user
379      * 
380      * @param   string|Tinebase_Model_User          $accountId
381      * @param   string|Tinebase_Model_Application   $recordClass
382      * @param   array|string                        $grant
383      * @param   bool                                $onlyIds return only ids
384      * @param   bool                                $ignoreACL
385      * @return  Tinebase_Record_RecordSet|array
386      * @throws  Tinebase_Exception_NotFound
387      */
388     public function getContainerByACL($accountId, $recordClass, $grant, $onlyIds = FALSE, $ignoreACL = FALSE)
389     {
390         // legacy handling 
391         $meta = $this->_resolveRecordClassArgument($recordClass);
392         
393         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
394             . ' app: ' . $meta['appName'] . ' / account: ' . $accountId . ' / grant:' . implode('/', (array)$grant));
395         
396         $accountId     = Tinebase_Model_User::convertUserIdToInt($accountId);
397         $applicationId = Tinebase_Application::getInstance()->getApplicationByName($meta['appName'])->getId();
398         $grant         = $ignoreACL ? '*' : $grant;
399         
400         // always bring values in the same order for $classCacheId 
401         if (is_array($grant)) {
402             sort($grant);
403         }
404         
405         $classCacheId = $accountId . $applicationId . implode('', (array)$grant) . (int)$onlyIds . (int)$ignoreACL;
406         
407         try {
408             return $this->loadFromClassCache(__FUNCTION__, $classCacheId);
409         } catch (Tinebase_Exception_NotFound $tenf) {
410             // continue...
411         }
412         
413         $select = $this->_getSelect($onlyIds ? 'id' : '*')
414             ->distinct()
415             ->join(array(
416                 /* table  */ 'container_acl' => SQL_TABLE_PREFIX . 'container_acl'), 
417                 /* on     */ "{$this->_db->quoteIdentifier('container_acl.container_id')} = {$this->_db->quoteIdentifier('container.id')}",
418                 /* select */ array()
419             )
420             ->where("{$this->_db->quoteIdentifier('container.application_id')} = ?", $applicationId);
421             
422         if (!$onlyIds) {
423             // we only need to order by name if we fetch all container data (legacy, maybe this can be removed)
424             $select->order('container.name');
425         }
426         
427         $this->addGrantsSql($select, $accountId, $grant);
428         
429         $stmt = $this->_db->query('/*' . __FUNCTION__ . '*/' . $select);
430         
431         if ($onlyIds) {
432             $result = $stmt->fetchAll(Zend_Db::FETCH_COLUMN);
433         } else {
434             $result = new Tinebase_Record_RecordSet('Tinebase_Model_Container', $stmt->fetchAll(Zend_Db::FETCH_ASSOC), true);
435         }
436         
437         // any account should have at least one personal folder
438         // @todo add test for empty case
439         if (empty($result)) {
440             $personalContainer = $this->getDefaultContainer($meta['appName'], $accountId);
441             if ($personalContainer instanceof Tinebase_Model_Container) {
442                 $result = ($onlyIds) ? 
443                     array($personalContainer->getId()) : 
444                     new Tinebase_Record_RecordSet('Tinebase_Model_Container', array($personalContainer), TRUE);
445             }
446         }
447         
448         $this->saveInClassCache(__FUNCTION__, $classCacheId, $result);
449         
450         return $result;
451     }
452     
453     /**
454      * return a container by containerId
455      * - cache the results because this function is called very often
456      *
457      * @todo what about grant checking here???
458      * 
459      * @param   int|Tinebase_Model_Container $_containerId the id of the container
460      * @param   bool                         $_getDeleted get deleted records
461      * @return  Tinebase_Model_Container
462      * @throws  Tinebase_Exception_NotFound
463      */
464     public function getContainerById($_containerId, $_getDeleted = FALSE)
465     {
466         $containerId = Tinebase_Model_Container::convertContainerId($_containerId);
467         
468         $cacheId = $containerId . 'd' . (int)$_getDeleted;
469         
470         try {
471             return $this->loadFromClassCache(__FUNCTION__, $cacheId, Tinebase_Cache_PerRequest::VISIBILITY_SHARED);
472         } catch (Tinebase_Exception_NotFound $tenf) {
473             // continue...
474         }
475
476         /** @var Tinebase_Model_Container $result */
477         $result = $this->get((string)$containerId, $_getDeleted);
478         
479         $this->saveInClassCache(__FUNCTION__, $cacheId, $result, Tinebase_Cache_PerRequest::VISIBILITY_SHARED);
480         
481         return $result;
482     }
483     
484     /**
485      * return a container identified by path
486      *
487      * @param   string  $_path        the path to the container
488      * @param   bool    $_getDeleted  get deleted records
489      * @return  Tinebase_Model_Container
490      * @throws  Tinebase_Exception_NotFound
491      */
492     public function getByPath($_path, $_getDeleted = FALSE)
493     {
494         if (($containerId = Tinebase_Model_Container::pathIsContainer($_path) === false)) {
495             throw new Tinebase_Exception_UnexpectedValue ("Invalid path $_path supplied.");
496         }
497     
498         return $this->getContainerById($containerId, $_getDeleted);
499     }
500     
501     /**
502      * return a container by container name
503      *
504      * @param   string|Tinebase_Model_Application  $recordClass app name
505      * @param   int|Tinebase_Model_Container       $containerName
506      * @param   string                             $type
507      * @param   string                             $ownerId
508      * @return  Tinebase_Model_Container
509      * @throws  Tinebase_Exception_NotFound
510      * @throws  Tinebase_Exception_UnexpectedValue
511      */
512     public function getContainerByName($recordClass, $containerName, $type, $ownerId = NULL)
513     {
514         // legacy handling
515         $meta = $this->_resolveRecordClassArgument($recordClass);
516         
517         if (! in_array($type, array(Tinebase_Model_Container::TYPE_PERSONAL, Tinebase_Model_Container::TYPE_SHARED))) {
518             throw new Tinebase_Exception_UnexpectedValue ("Invalid type $type supplied.");
519         }
520         
521         if ($type == Tinebase_Model_Container::TYPE_PERSONAL && empty($ownerId)) {
522             throw new Tinebase_Exception_UnexpectedValue ('$ownerId can not be empty for personal folders');
523         }
524         
525         $ownerId = $ownerId instanceof Tinebase_Model_User ? $ownerId->getId() : $ownerId;
526         
527         $applicationId = Tinebase_Application::getInstance()->getApplicationByName($meta['appName'])->getId();
528
529         $select = $this->_getSelect()
530             ->where("{$this->_db->quoteIdentifier('container.application_id')} = ?", $applicationId)
531             ->where("{$this->_db->quoteIdentifier('container.name')} = ?", $containerName)
532             ->where("{$this->_db->quoteIdentifier('container.type')} = ?", $type)
533             ->where("{$this->_db->quoteIdentifier('container.is_deleted')} = ?", 0, Zend_Db::INT_TYPE);
534
535         if ($type == Tinebase_Model_Container::TYPE_PERSONAL) {
536             $select->where("{$this->_db->quoteIdentifier('owner_id')} = ?", $ownerId);
537         }
538         
539         $stmt = $this->_db->query('/*' . __FUNCTION__ . '*/' . $select);
540         $containersData = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
541         
542         if (count($containersData) == 0) {
543             throw new Tinebase_Exception_NotFound("Container $containerName not found.");
544         }
545         if (count($containersData) > 1) {
546             throw new Tinebase_Exception_Duplicate("Container $containerName name duplicate.");
547         }
548
549         $container = new Tinebase_Model_Container($containersData[0]);
550         
551         return $container;
552     }
553     
554     /**
555      * returns the personal container of a given account accessible by a another given account
556      *
557      * @param   string|Tinebase_Model_User          $_accountId
558      * @param   string|Tinebase_Record_Interface    $_recordClass
559      * @param   int|Tinebase_Model_User             $_owner
560      * @param   array|string                        $_grant
561      * @param   bool                                $_ignoreACL
562      * @return  Tinebase_Record_RecordSet of subtype Tinebase_Model_Container
563      * @throws  Tinebase_Exception_NotFound
564      */
565     public function getPersonalContainer($_accountId, $_recordClass, $_owner, $_grant = Tinebase_Model_Grants::GRANT_READ, $_ignoreACL = false)
566     {
567         $meta = $this->_resolveRecordClassArgument($_recordClass);
568         
569         $accountId   = Tinebase_Model_User::convertUserIdToInt($_accountId);
570         $ownerId     = Tinebase_Model_User::convertUserIdToInt($_owner);
571         $grant       = $_ignoreACL ? '*' : $_grant;
572         $application = Tinebase_Application::getInstance()->getApplicationByName($meta['appName']);
573         
574         $classCacheId = $accountId .
575                         $application->getId() .
576                         ($meta['recordClass'] ? $meta['recordClass'] : null) .
577                         $ownerId .
578                         implode('', (array)$grant) .
579                         (int)$_ignoreACL;
580         
581         try {
582             return $this->loadFromClassCache(__FUNCTION__, $classCacheId);
583         } catch (Tinebase_Exception_NotFound $tenf) {
584             // continue...
585         }
586         
587         $select = $this->_db->select()
588             ->distinct() // TODO needed?
589             ->from(array('container' => SQL_TABLE_PREFIX . 'container'))
590             ->join(array(
591                 /* table  */ 'container_acl' => SQL_TABLE_PREFIX . 'container_acl'),
592                 /* on     */ "{$this->_db->quoteIdentifier('container.id')} = {$this->_db->quoteIdentifier('container_acl.container_id')}",
593                 /* select */ array()
594             )
595
596             ->where("{$this->_db->quoteIdentifier('container.application_id')} = ?", $application->getId())
597             ->where("{$this->_db->quoteIdentifier('container.type')} = ?", Tinebase_Model_Container::TYPE_PERSONAL)
598             ->where("{$this->_db->quoteIdentifier('container.is_deleted')} = ?", 0, Zend_Db::INT_TYPE)
599             ->where("{$this->_db->quoteIdentifier('container.owner_id')} = ?", $ownerId)
600
601             ->order('container.creation_time');
602             
603         $this->addGrantsSql($select, $accountId, $grant);
604         
605         if ($meta['recordClass']) {
606             $select->where("{$this->_db->quoteIdentifier('container.model')} = ?", $meta['recordClass']);
607         }
608         
609         $stmt = $this->_db->query('/*' . __FUNCTION__ . '*/' . $select);
610         $containersData = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
611
612         // if no containers where found, maybe something went wrong when creating the initial folder
613         // let's check if the controller of the application has a function to create the needed folders
614         if (empty($containersData) && $accountId === $ownerId) {
615             $application = Tinebase_Core::getApplicationInstance($meta['appName']);
616
617             if ($application instanceof Tinebase_Application_Container_Interface && method_exists($application, 'createPersonalFolder')) {
618                 return $application->createPersonalFolder($accountId);
619             } else if ($meta['recordClass']) {
620                 $containersData = array($this->createDefaultContainer($meta['recordClass'], $meta['appName'], $accountId));
621             }
622         }
623
624         $containers = new Tinebase_Record_RecordSet('Tinebase_Model_Container', $containersData, TRUE);
625
626         $this->saveInClassCache(__FUNCTION__, $classCacheId, $containers);
627
628         return $containers;
629     }
630
631     /**
632      * create default container for a user
633      *
634      * @param string $recordClass
635      * @param string $applicationName
636      * @param Tinebase_Model_User|string $account
637      * @param string $containerName
638      * @return Tinebase_Model_Container
639      * @throws Tinebase_Exception_AccessDenied
640      * @throws Tinebase_Exception_InvalidArgument
641      * @throws Tinebase_Exception_NotFound
642      *
643      * @todo add record name to container name?
644      */
645     public function createDefaultContainer($recordClass, $applicationName, $account, $containerName = null)
646     {
647         if (! $account instanceof Tinebase_Model_User) {
648             $account = Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend('accountId', $account);
649         }
650
651         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
652             . ' Creating new default container ' . $containerName . ' for '
653             . $account->accountFullName . ' for model ' . $recordClass);
654
655         if (! $containerName) {
656             $translation = Tinebase_Translation::getTranslation('Tinebase');
657             $containerName = sprintf($translation->_("%s's personal container"), $account->accountFullName);
658         }
659
660         $container = $this->addContainer(new Tinebase_Model_Container(array(
661             'name'              => $containerName,
662             'type'              => Tinebase_Model_Container::TYPE_PERSONAL,
663             'owner_id'          => $account->getId(),
664             'backend'           => 'Sql',
665             'model'             => $recordClass,
666             'application_id'    => Tinebase_Application::getInstance()->getApplicationByName($applicationName)->getId()
667         )));
668
669         return $container;
670     }
671     
672     /**
673      * appends container_acl sql 
674      * 
675      * @param  Zend_Db_Select    $_select
676      * @param  String            $_accountId
677      * @param  Array|String      $_grant
678      * @param  String            $_aclTableName (deprecated)
679      * @param  bool              $_andGrants
680      * @return void
681      */
682     public static function addGrantsSql($_select, $_accountId, $_grant, $_aclTableName = 'container_acl', $_andGrants = FALSE, $joinCallBack = NULL)
683     {
684         $accountId = $_accountId instanceof Tinebase_Record_Abstract
685             ? $_accountId->getId()
686             : $_accountId;
687         
688         $db = $_select->getAdapter();
689         
690         $grants = is_array($_grant) ? $_grant : array($_grant);
691
692         $groupMemberships   = Tinebase_Group::getInstance()->getGroupMemberships($accountId);
693         $roleMemberships    = Tinebase_Acl_Roles::getInstance()->getRoleMemberships($accountId);
694         // enforce string for pgsql
695         array_walk($roleMemberships, function(&$item) {$item = (string)$item;});
696         
697         $quotedActId   = $db->quoteIdentifier("{$_aclTableName}.account_id");
698         $quotedActType = $db->quoteIdentifier("{$_aclTableName}.account_type");
699         
700         $accountSelect = new Tinebase_Backend_Sql_Filter_GroupSelect($_select);
701         $accountSelect
702             ->orWhere("{$quotedActId} = ? AND {$quotedActType} = " . $db->quote(Tinebase_Acl_Rights::ACCOUNT_TYPE_USER), $accountId)
703             ->orWhere("{$quotedActId} IN (?) AND {$quotedActType} = " . $db->quote(Tinebase_Acl_Rights::ACCOUNT_TYPE_GROUP), empty($groupMemberships) ? ' ' : $groupMemberships)
704             ->orWhere("{$quotedActId} IN (?) AND {$quotedActType} = " . $db->quote(Tinebase_Acl_Rights::ACCOUNT_TYPE_ROLE), empty($roleMemberships) ? ' ' : $roleMemberships);
705         
706         if (! Tinebase_Config::getInstance()->get(Tinebase_Config::ANYONE_ACCOUNT_DISABLED)) {
707             $accountSelect->orWhere("{$quotedActType} = ?", Tinebase_Acl_Rights::ACCOUNT_TYPE_ANYONE);
708         }
709         $accountSelect->appendWhere(Zend_Db_Select::SQL_AND);
710         
711         // we only need to filter, if the filter does not contain %
712         if (!in_array('*', $grants)) {
713             // @todo fetch wildcard from specific db adapter
714             $grants = str_replace('*', '%', $grants);
715         
716             $quotedGrant   = $db->quoteIdentifier($_aclTableName . '.account_grant');
717
718             $iteration = 0;
719             $grantsSelect = new Tinebase_Backend_Sql_Filter_GroupSelect($_select);
720             foreach ($grants as $grant) {
721                 if ($_andGrants) {
722                     if ($iteration > 0) {
723                         $callbackIdentifier = call_user_func($joinCallBack, $_select, $iteration);
724                         $grantsSelect->where($db->quoteIdentifier($callbackIdentifier . '.account_grant') . ' LIKE ?', $grant);
725                     } else {
726                         $grantsSelect->where($quotedGrant . ' LIKE ?', $grant);
727                     }
728                     ++$iteration;
729                 } else {
730                     $grantsSelect->orWhere($quotedGrant . ' LIKE ?', $grant);
731                 }
732             }
733
734             // admin grant includes all other grants
735             if (! in_array(Tinebase_Model_Grants::GRANT_ADMIN, $grants)) {
736                 $grantsSelect->orWhere($quotedGrant . ' LIKE ?', Tinebase_Model_Grants::GRANT_ADMIN);
737             }
738
739             $grantsSelect->appendWhere(Zend_Db_Select::SQL_AND);
740         }
741     }
742
743     /**
744      * gets default container of given user for given app
745      *  - did and still does return personal first container by using the application name instead of the recordClass name
746      *  - allows now to use different models with default container in one application
747      *
748      * @param   string|Tinebase_Record_Interface    $recordClass
749      * @param   string|Tinebase_Model_User          $accountId use current user if omitted
750      * @param   string                              $defaultContainerPreferenceName
751      * @return  Tinebase_Model_Container
752      * 
753      * @todo get default container name from app/translations?
754      */
755     public function getDefaultContainer($recordClass, $accountId = NULL, $defaultContainerPreferenceName = NULL)
756     {
757         // legacy handling
758         $meta = $this->_resolveRecordClassArgument($recordClass);
759
760         $account = ($accountId !== NULL)
761             ? Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend('accountId', $accountId)
762             : Tinebase_Core::getUser();
763         
764         if ($defaultContainerPreferenceName !== NULL) {
765             $defaultContainerId = Tinebase_Core::getPreference($meta['appName'])->getValueForUser($defaultContainerPreferenceName, $account->getId());
766             try {
767                 $result = $this->getContainerById($defaultContainerId);
768                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
769                     . ' Got default container from preferences: ' . $result->name);
770                 return $result;
771             } catch (Tinebase_Exception $te) {
772                 Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Default container not found (' . $te->getMessage() . ')');
773                 // default may be gone -> remove default adb pref
774                 $appPref = Tinebase_Core::getPreference($meta['appName']);
775                 if ($appPref) {
776                     $appPref->deleteUserPref($defaultContainerPreferenceName);
777                 }
778             }
779         }
780         
781         $result = $this->getPersonalContainer($account, $recordClass, $account, Tinebase_Model_Grants::GRANT_ADD)->getFirstRecord();
782         
783         if ($result === NULL) {
784             $result = $this->createDefaultContainer($recordClass, $meta['appName'], $accountId);
785         }
786         
787         if ($defaultContainerPreferenceName !== NULL) {
788             // save as new pref
789             Tinebase_Core::getPreference($meta['appName'])->setValue($defaultContainerPreferenceName, $result->getId());
790         }
791
792         return $result;
793     }
794     
795     /**
796      * returns the shared container for a given application accessible by the current user
797      *
798      * @param   string|Tinebase_Model_User          $_accountId
799      * @param   string|Tinebase_Model_Application   $recordClass
800      * @param   array|string                        $_grant
801      * @param   bool                                $_ignoreACL
802      * @param   bool                                $_andGrants
803      * @return  Tinebase_Record_RecordSet set of Tinebase_Model_Container
804      * @throws  Tinebase_Exception_NotFound
805      */
806     public function getSharedContainer($_accountId, $recordClass, $_grant, $_ignoreACL = FALSE, $_andGrants = FALSE)
807     {
808         // legacy handling
809         $meta = $this->_resolveRecordClassArgument($recordClass);
810         $application = Tinebase_Application::getInstance()->getApplicationByName($meta['appName']);
811         $accountId   = Tinebase_Model_User::convertUserIdToInt($_accountId);
812         $grant       = $_ignoreACL ? '*' : $_grant;
813         
814         $classCacheId = Tinebase_Helper::convertCacheId(
815             $accountId .
816             $application->getId() .
817             implode('', (array)$grant) .
818             (int)$_ignoreACL .
819             (int)$_andGrants
820         );
821
822         try {
823             return $this->loadFromClassCache(__FUNCTION__, $classCacheId);
824         } catch (Tinebase_Exception_NotFound $tenf) {
825             // continue...
826         }
827         
828         $select = $this->_getSelect()
829             ->distinct()
830             ->join(array(
831                 /* table  */ 'container_acl' => SQL_TABLE_PREFIX . 'container_acl'), 
832                 /* on     */ "{$this->_db->quoteIdentifier('container_acl.container_id')} = {$this->_db->quoteIdentifier('container.id')}",
833                 array()
834             )
835             
836             ->where("{$this->_db->quoteIdentifier('container.application_id')} = ?", $application->getId())
837             ->where("{$this->_db->quoteIdentifier('container.type')} = ?", Tinebase_Model_Container::TYPE_SHARED);
838
839         // TODO maybe this could be removed or changed to a preference later
840         $sortOrder = Tinebase_Config::getInstance()->featureEnabled(Tinebase_Config::FEATURE_CONTAINER_CUSTOM_SORT)
841             ? array('container.order', 'container.name')
842             : 'container.name';
843         $select->order($sortOrder);
844
845         $this->addGrantsSql($select, $accountId, $grant, 'container_acl', $_andGrants, __CLASS__ . '::addGrantsSqlCallback');
846         
847         $stmt = $this->_db->query('/*' . __FUNCTION__ . '*/' . $select);
848         
849         $containers = new Tinebase_Record_RecordSet('Tinebase_Model_Container', $stmt->fetchAll(Zend_Db::FETCH_ASSOC), TRUE);
850         
851         $this->saveInClassCache(__FUNCTION__, $classCacheId, $containers);
852         
853         Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
854             . ' Found ' . count($containers) . ' shared container(s) in application ' . $application->name);
855         
856         return $containers;
857     }
858     
859     /**
860      * return users which made personal containers accessible to given account
861      *
862      * @param   string|Tinebase_Model_User          $_accountId
863      * @param   string|Tinebase_Model_Application   $recordClass
864      * @param   array|string                        $_grant
865      * @param   bool                                $_ignoreACL
866      * @param   bool                                $_andGrants
867      * @return  Tinebase_Record_RecordSet set of Tinebase_Model_User
868      */
869     public function getOtherUsers($_accountId, $recordClass, $_grant, $_ignoreACL = FALSE, $_andGrants = FALSE)
870     {
871         $meta = $this->_resolveRecordClassArgument($recordClass);
872         $userIds = $this->_getOtherAccountIds($_accountId, $meta['appName'], $_grant, $_ignoreACL, $_andGrants);
873         
874         $users = Tinebase_User::getInstance()->getMultiple($userIds);
875         $users->sort('accountDisplayName');
876         
877         return $users;
878     }
879
880     /**
881      * appends container_acl sql
882      *
883      * @param  Zend_Db_Select    $_select
884      * @param  integer           $iteration
885      * @return string table identifier to work on
886      */
887     public static function addGrantsSqlCallback($_select, $iteration)
888     {
889         $db = $_select->getAdapter();
890         $_select->join(array(
891             /* table  */ 'container_acl' . $iteration => SQL_TABLE_PREFIX . 'container_acl'),
892             /* on     */ $db->quoteIdentifier('container_acl' . $iteration . '.container_id') . ' = ' . $db->quoteIdentifier('container.id'),
893             array()
894         );
895         return 'container_acl' . $iteration;
896     }
897
898     /**
899      * return account ids of accounts which made personal container accessible to given account
900      *
901      * @param   string|Tinebase_Model_User          $_accountId
902      * @param   string|Tinebase_Model_Application   $_application
903      * @param   array|string                        $_grant
904      * @param   bool                                $_ignoreACL
905      * @param   bool                                $_andGrants
906      * @return  array of array of containerData
907      */
908     protected function _getOtherAccountIds($_accountId, $_application, $_grant, $_ignoreACL = FALSE, $_andGrants = FALSE)
909     {
910         $accountId   = Tinebase_Model_User::convertUserIdToInt($_accountId);
911         $application = Tinebase_Application::getInstance()->getApplicationByName($_application);
912         $grant       = $_ignoreACL ? '*' : $_grant;
913
914         $classCacheId = Tinebase_Helper::convertCacheId($accountId . $application->getId() . implode('', (array)$grant) . (int)$_ignoreACL . (int)$_andGrants);
915         try {
916             return $this->loadFromClassCache(__FUNCTION__, $classCacheId);
917         } catch (Tinebase_Exception_NotFound $tenf) {
918             // continue...
919         }
920
921         $select = $this->_db->select()
922             ->from(array('container' => SQL_TABLE_PREFIX . 'container'), array('owner_id'))
923             ->join(array(
924                 /* table  */ 'container_acl' => SQL_TABLE_PREFIX . 'container_acl'),
925                 /* on     */ "{$this->_db->quoteIdentifier('container_acl.container_id')} = {$this->_db->quoteIdentifier('container.id')}",
926                 /* select */ array('container_id' => 'container.id')
927             )
928             ->join(array(
929                 /* table  */ 'accounts' => SQL_TABLE_PREFIX . 'accounts'),
930                 /* on     */ "{$this->_db->quoteIdentifier('container.owner_id')} = {$this->_db->quoteIdentifier('accounts.id')}",
931                 /* select */ array()
932             )
933             ->where("{$this->_db->quoteIdentifier('container.application_id')} = ?", $application->getId())
934             ->where("{$this->_db->quoteIdentifier('container.type')} = ?", Tinebase_Model_Container::TYPE_PERSONAL)
935             ->where("{$this->_db->quoteIdentifier('container.is_deleted')} = ?", 0, Zend_Db::INT_TYPE)
936             ->where("{$this->_db->quoteIdentifier('container.owner_id')} != ?", $accountId)
937             ->where("{$this->_db->quoteIdentifier('accounts.status')} = ?", 'enabled');
938
939         $this->addGrantsSql($select, $accountId, $grant, 'container_acl', $_andGrants, __CLASS__ . '::addGrantsSqlCallback');
940
941         $stmt = $this->_db->query('/*' . __FUNCTION__ . '*/' . $select);
942         $accountIds = $stmt->fetchAll(Zend_Db::FETCH_COLUMN);
943
944         $this->saveInClassCache(__FUNCTION__, $classCacheId, $accountIds);
945         
946         return $accountIds;
947     }
948     
949     /**
950      * return set of all personal container of other users made accessible to the given account 
951      *
952      * @param   string|Tinebase_Model_User          $_accountId
953      * @param   string|Tinebase_Model_Application   $recordClass
954      * @param   array|string                        $_grant
955      * @param   bool                                $_ignoreACL
956      * @return  Tinebase_Record_RecordSet set of Tinebase_Model_Container
957      */
958     public function getOtherUsersContainer($_accountId, $recordClass, $_grant, $_ignoreACL = FALSE)
959     {
960         // legacy handling
961         $meta = $this->_resolveRecordClassArgument($recordClass);
962         $result = $this->_getOtherUsersContainerData($_accountId, $meta['appName'], $_grant, $_ignoreACL);
963
964         return $result;
965     }
966     
967     /**
968      * return containerData of containers which made personal accessible to given account
969      *
970      * @param   string|Tinebase_Model_User          $_accountId
971      * @param   string|Tinebase_Model_Application   $_application
972      * @param   array|string                        $_grant
973      * @param   bool                                $_ignoreACL
974      * @return  Tinebase_Record_RecordSet set of Tinebase_Model_Container
975      */
976     protected function _getOtherUsersContainerData($_accountId, $_application, $_grant, $_ignoreACL = FALSE)
977     {
978         $accountId   = Tinebase_Model_User::convertUserIdToInt($_accountId);
979         $application = Tinebase_Application::getInstance()->getApplicationByName($_application);
980         $grant       = $_ignoreACL ? '*' : $_grant;
981         
982         $classCacheId = $accountId .
983                         $application->getId() .
984                         implode('', (array)$grant) .
985                         (int)$_ignoreACL;
986         
987         try {
988             return $this->loadFromClassCache(__FUNCTION__, $classCacheId);
989         } catch (Tinebase_Exception_NotFound $tenf) {
990             // continue...
991         }
992
993         $select = $this->_db->select()
994             ->from(array('container' => SQL_TABLE_PREFIX . 'container'))
995             ->join(array(
996                 /* table  */ 'container_acl' => SQL_TABLE_PREFIX . 'container_acl'),
997                 /* on     */ "{$this->_db->quoteIdentifier('container.id')} = {$this->_db->quoteIdentifier('container_acl.container_id')}",
998                 /* select */ array()
999             )
1000             ->join(array(
1001                 /* table  */ 'accounts' => SQL_TABLE_PREFIX . 'accounts'),
1002                 /* on     */ "{$this->_db->quoteIdentifier('container.owner_id')} = {$this->_db->quoteIdentifier('accounts.id')}",
1003                 /* select */ array()
1004             )
1005             ->where("{$this->_db->quoteIdentifier('container.application_id')} = ?", $application->getId())
1006             ->where("{$this->_db->quoteIdentifier('container.type')} = ?", Tinebase_Model_Container::TYPE_PERSONAL)
1007             ->where("{$this->_db->quoteIdentifier('container.is_deleted')} = ?", 0, Zend_Db::INT_TYPE)
1008             ->where("{$this->_db->quoteIdentifier('container.owner_id')} != ?", $accountId)
1009             ->where("{$this->_db->quoteIdentifier('accounts.status')} = ?", 'enabled')
1010             
1011             ->order('accounts.display_name');
1012
1013         $this->addGrantsSql($select, $accountId, $grant);
1014         
1015         Tinebase_Backend_Sql_Abstract::traitGroup($select);
1016         
1017         $stmt = $this->_db->query('/*' . __FUNCTION__ . '*/' . $select);
1018
1019         $result = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
1020         $containers = new Tinebase_Record_RecordSet('Tinebase_Model_Container', $result, TRUE);
1021
1022         $this->saveInClassCache(__FUNCTION__, $classCacheId, $containers);
1023         
1024         return $containers;
1025     }
1026
1027     /**
1028      * Deletes entries
1029      *
1030      * @param string|integer|Tinebase_Record_Interface|array $_id
1031      * @return int The number of affected rows.
1032      */
1033     public function delete($_id)
1034     {
1035         if (empty($_id)) {
1036             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' No records deleted.');
1037             return 0;
1038         }
1039
1040         $idArray = (! is_array($_id)) ? array(Tinebase_Record_Abstract::convertId($_id, $this->_modelName)) : $_id;
1041
1042         foreach($idArray as $id) {
1043             $this->deleteContainer($id);
1044         }
1045     }
1046
1047     /**
1048      * delete container if user has the required right
1049      *
1050      * @param   int|Tinebase_Model_Container $_containerId
1051      * @param   boolean $_ignoreAcl
1052      * @throws  Tinebase_Exception_AccessDenied
1053      * @throws  Tinebase_Exception_Record_SystemContainer
1054      * @throws  Tinebase_Exception_InvalidArgument
1055      * 
1056      * @todo move records in deleted container to personal container?
1057      */
1058     public function deleteContainer($_containerId, $_ignoreAcl = false)
1059     {
1060         $containerId = Tinebase_Model_Container::convertContainerId($_containerId);
1061         $container = ($_containerId instanceof Tinebase_Model_Container) ? $_containerId : $this->getContainerById($containerId, $_ignoreAcl);
1062         $this->checkSystemContainer($containerId);
1063
1064         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1065             . ' Deleting container id ' . $containerId . ' ...');
1066
1067         $deletedContainer = NULL;
1068
1069         if ($_ignoreAcl !== TRUE) {
1070             if(!$this->hasGrant(Tinebase_Core::getUser(), $containerId, Tinebase_Model_Grants::GRANT_ADMIN)) {
1071                 throw new Tinebase_Exception_AccessDenied('Permission to delete container denied.');
1072             }
1073
1074             if($container->type !== Tinebase_Model_Container::TYPE_PERSONAL and $container->type !== Tinebase_Model_Container::TYPE_SHARED) {
1075                 throw new Tinebase_Exception_InvalidArgument('Can delete personal or shared containers only.');
1076             }
1077
1078             // get personal container
1079             $personalContainer = $this->getDefaultContainer($container->model, Tinebase_Core::getUser());
1080             if ((string)($personalContainer->getId()) === (string)$containerId) {
1081                 // _('You are not allowed to delete your default container!')
1082                 throw new Tinebase_Exception_SystemGeneric('You are not allowed to delete your default container!');
1083             }
1084         }
1085
1086         $tm = Tinebase_TransactionManager::getInstance();
1087         $myTransactionId = $tm->startTransaction(Tinebase_Core::getDb());
1088
1089         try {
1090             $this->_writeModLog(null, $container);
1091
1092             $this->deleteContainerContents($container, $_ignoreAcl);
1093             $deletedContainer = $this->_setRecordMetaDataAndUpdate($container, 'delete');
1094
1095             $event = new Tinebase_Event_Record_Delete();
1096             $event->observable = $deletedContainer;
1097             Tinebase_Record_PersistentObserver::getInstance()->fireEvent($event);
1098         } catch (Exception $e) {
1099             $tm->rollBack();
1100             throw $e;
1101         }
1102         
1103         $tm->commitTransaction($myTransactionId);
1104         
1105         return $deletedContainer;
1106
1107         /*
1108         // move all contained objects to next available personal container and try again to delete container
1109         $app = Tinebase_Application::getApplicationById($container->application_id);
1110
1111         // get personal containers
1112         $personalContainers = $this->getPersonalContainer(
1113             Tinebase_Core::getUser(),
1114             $app->name,
1115             $container->owner,
1116             Tinebase_Model_Grants::GRANT_ADD
1117         );
1118         
1119         //-- determine first matching personal container (or create new one)
1120         // $personalContainer =
1121         
1122         //-- move all records to personal container
1123         
1124         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1125             . ' Moving all records from container ' . $containerId . ' to personal container ' . $personalContainer->getId()
1126         );
1127         */
1128     }
1129     
1130     /**
1131      * set delete flag of records belonging to the given container
1132      * 
1133      * @param Tinebase_Model_Container $container
1134      */
1135     public function deleteContainerContents($container, $_ignoreAcl = FALSE)
1136     {
1137         $model = $container->model;
1138
1139         if (empty($model)) {
1140             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1141                 . ' No container model defined');
1142             return;
1143         }
1144
1145         $controller = Tinebase_Core::getApplicationInstance($model, /* $_modelName */ '', $_ignoreAcl);
1146
1147         if ($controller) {
1148             if ($_ignoreAcl === TRUE && method_exists($controller, 'doContainerACLChecks')) {
1149                 $acl = $controller->doContainerACLChecks(FALSE);
1150             }
1151
1152             if (method_exists($controller, 'deleteContainerContents')) {
1153                 $controller->deleteContainerContents($container, $_ignoreAcl);
1154             } else {
1155                 Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
1156                     . ' No deleteContainerContents defined in controller ' . get_class($controller));
1157             }
1158
1159             if ($_ignoreAcl === TRUE && method_exists($controller, 'doContainerACLChecks')) {
1160                 $controller->doContainerACLChecks($acl);
1161             }
1162         }
1163     }
1164     
1165     /**
1166      * drop container by application id, this is a straight and hard DB deletion
1167      * ATTENTION this does not follow deleteContainer() cleanup logic, this JUST deletes the containers, nothing more
1168      * 
1169      * @param string $_applicationId
1170      * @return integer number of deleted containers
1171      */
1172     public function dropContainerByApplicationId($_applicationId)
1173     {
1174         $this->resetClassCache();
1175         
1176         return $this->deleteByProperty($_applicationId, 'application_id');
1177     }
1178     
1179     /**
1180      * set container name, if the user has the required right
1181      *
1182      * @param   int|Tinebase_Model_Container $_containerId
1183      * @param   string $_containerName the new name
1184      * @return  Tinebase_Model_Container
1185      * @throws  Tinebase_Exception_AccessDenied
1186      */
1187     public function setContainerName($_containerId, $_containerName)
1188     {
1189         $container = ($_containerId instanceof Tinebase_Model_Container) ? $_containerId : $this->getContainerById($_containerId);
1190         
1191         if (!$this->hasGrant(Tinebase_Core::getUser(), $container, Tinebase_Model_Grants::GRANT_ADMIN)) {
1192             throw new Tinebase_Exception_AccessDenied('Permission to rename container denied.');
1193         }
1194         
1195         $container->name = $_containerName;
1196         
1197         return $this->_setRecordMetaDataAndUpdate($container, 'update');
1198     }
1199     
1200     /**
1201      * set container color, if the user has the required right
1202      *
1203      * @param   int|Tinebase_Model_Container $_containerId
1204      * @param   string $_color the new color
1205      * @return  Tinebase_Model_Container
1206      * @throws  Tinebase_Exception_AccessDenied
1207      */
1208     public function setContainerColor($_containerId, $_color)
1209     {
1210         $container = ($_containerId instanceof Tinebase_Model_Container) ? $_containerId : $this->getContainerById($_containerId);
1211
1212         if (! $this->hasGrant(Tinebase_Core::getUser(), $container, Tinebase_Model_Grants::GRANT_ADMIN)) {
1213             throw new Tinebase_Exception_AccessDenied('Permission to set color of container denied.');
1214         }
1215         
1216         if (! preg_match('/^#[0-9a-fA-F]{6}$/', $_color)) {
1217             throw new Tinebase_Exception_UnexpectedValue('color is not valid');
1218         }
1219         
1220         $container->color = $_color;
1221         
1222         return $this->_setRecordMetaDataAndUpdate($container, 'update');
1223     }
1224     
1225     /**
1226      * check if the given user user has a certain grant
1227      *
1228      * @param   string|Tinebase_Model_User          $_accountId
1229      * @param   int|Tinebase_Record_Abstract        $_containerId
1230      * @param   array|string                        $_grant
1231      * @return  boolean
1232      */
1233     public function hasGrant($_accountId, $_containerId, $_grant)
1234     {
1235         $accountId = Tinebase_Model_User::convertUserIdToInt($_accountId);
1236         
1237         try {
1238             $containerId = Tinebase_Model_Container::convertContainerId($_containerId);
1239         } catch (Tinebase_Exception_InvalidArgument $teia) {
1240             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' ' . $teia->getMessage());
1241             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $teia->getTraceAsString());
1242             return false;
1243         }
1244         
1245         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1246             . ' account: ' . $accountId . ' / containerId: ' . $containerId . ' / grant:' . implode('/', (array)$_grant));
1247         
1248         $classCacheId = $accountId . $containerId . implode('', (array)$_grant);
1249         
1250         try {
1251             $allGrants = $this->loadFromClassCache(__FUNCTION__, $classCacheId);
1252         } catch (Tinebase_Exception_NotFound $tenf) {
1253             // NOTE: some tests ask for already deleted container ;-)
1254             $select = $this->_getSelect(array(), true)
1255                 ->distinct()
1256                 ->where("{$this->_db->quoteIdentifier('container.id')} = ?", $containerId)
1257                 ->join(array(
1258                     /* table  */ 'container_acl' => SQL_TABLE_PREFIX . 'container_acl'), 
1259                     /* on     */ "{$this->_db->quoteIdentifier('container_acl.container_id')} = {$this->_db->quoteIdentifier('container.id')}",
1260                     /* select */ array('container_acl.account_grant')
1261                 );
1262                 
1263             $this->addGrantsSql($select, $accountId, '*');
1264             
1265             $stmt = $this->_db->query('/*' . __FUNCTION__ . '*/' . $select);
1266             
1267             $allGrants = $stmt->fetchAll(Zend_Db::FETCH_COLUMN);
1268             $this->saveInClassCache(__FUNCTION__, $classCacheId, $allGrants);
1269         }
1270         
1271         $matchingGrants = array_intersect((array)$_grant, $allGrants);
1272         
1273         return !!count($matchingGrants);
1274     }
1275     
1276     /**
1277      * get all grants assigned to this container
1278      *
1279      * @param   int|Tinebase_Model_Container $_containerId
1280      * @param   bool                         $_ignoreAcl
1281      * @param   string                       $_grantModel
1282      * @return  Tinebase_Record_RecordSet subtype Tinebase_Model_Grants
1283      * @throws  Tinebase_Exception_AccessDenied
1284      */
1285     public function getGrantsOfContainer($_containerId, $_ignoreAcl = FALSE, $_grantModel = 'Tinebase_Model_Grants') 
1286     {
1287         $grants = new Tinebase_Record_RecordSet('Tinebase_Model_Grants');
1288         
1289         $containerId = Tinebase_Model_Container::convertContainerId($_containerId);
1290         
1291         $select = $this->_getAclSelectByContainerId($containerId)
1292             ->group(array('container_acl.container_id', 'container_acl.account_type', 'container_acl.account_id'));
1293         
1294         Tinebase_Backend_Sql_Abstract::traitGroup($select);
1295         
1296         $stmt = $this->_db->query('/*' . __FUNCTION__ . '*/' . $select);
1297
1298         $grantsData = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
1299         
1300         foreach($grantsData as $grantData) {
1301             $givenGrants = explode(',', $grantData['account_grants']);
1302             foreach($givenGrants as $grant) {
1303                 $grantData[$grant] = TRUE;
1304             }
1305             
1306             $containerGrant = new $_grantModel($grantData, TRUE);
1307
1308             $grants->addRecord($containerGrant);
1309         }
1310         
1311         if ($_ignoreAcl !== TRUE) {
1312             if (TRUE !== $this->hasGrant(Tinebase_Core::getUser()->getId(), $containerId, Tinebase_Model_Grants::GRANT_ADMIN)) {
1313                 throw new Tinebase_Exception_AccessDenied('Permission to get grants of container denied.');
1314             }
1315         }
1316         return $grants;
1317     }
1318     
1319     /**
1320      * get select with acl (grants) by container ID
1321      * 
1322      * @param integer $containerId
1323      * @return Zend_Db_Select
1324      */
1325     protected function _getAclSelectByContainerId($containerId)
1326     {
1327          $select = $this->_db->select()
1328             ->from(
1329                 array('container_acl' => SQL_TABLE_PREFIX . 'container_acl'),
1330                 array('*', 'account_grants' => $this->_dbCommand->getAggregate('container_acl.account_grant'))
1331             )
1332             ->where("{$this->_db->quoteIdentifier('container_acl.container_id')} = ?", $containerId);
1333          return $select;
1334     }
1335     
1336     /**
1337      * get grants assigned to one account of one container
1338      *
1339      * @param   string|Tinebase_Model_User          $_accountId
1340      * @param   int|Tinebase_Model_Container        $_containerId
1341      * @param   string                              $_grantModel
1342      * @return Tinebase_Model_Grants
1343      */
1344     public function getGrantsOfAccount($_accountId, $_containerId, $_grantModel = 'Tinebase_Model_Grants') 
1345     {
1346         $accountId          = Tinebase_Model_User::convertUserIdToInt($_accountId);
1347         $containerId        = Tinebase_Model_Container::convertContainerId($_containerId);
1348         $container          = ($_containerId instanceof Tinebase_Model_Container) ? $_containerId : $this->getContainerById($_containerId);
1349         
1350         $classCacheId = $accountId . $containerId . $container->seq . $_grantModel;
1351         
1352         try {
1353             $grants = $this->loadFromClassCache(__FUNCTION__, $classCacheId, Tinebase_Cache_PerRequest::VISIBILITY_SHARED);
1354             if ($grants instanceof Tinebase_Model_Grants) {
1355                 return $grants;
1356             } else {
1357                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ .
1358                     ' Invalid data in cache ... fetching fresh data from DB');
1359             }
1360         } catch (Tinebase_Exception_NotFound $tenf) {
1361             // not found in cache
1362         }
1363         
1364         $select = $this->_getAclSelectByContainerId($containerId)
1365             ->group('container_acl.account_grant');
1366         
1367         $this->addGrantsSql($select, $accountId, '*');
1368         
1369         Tinebase_Backend_Sql_Abstract::traitGroup($select);
1370         
1371         $stmt = $this->_db->query('/*' . __FUNCTION__ . '*/' . $select);
1372         
1373         $rows = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
1374         
1375         $grants = $this->_getGrantsFromArray($rows, $accountId, $_grantModel);
1376         
1377         $this->saveInClassCache(__FUNCTION__, $classCacheId, $grants, Tinebase_Cache_PerRequest::VISIBILITY_SHARED, self::ACL_CACHE_TIMEOUT);
1378         
1379         return $grants;
1380     }
1381     
1382     /**
1383      * get grants assigned to given account of multiple records
1384      *
1385      * @param   Tinebase_Record_RecordSet   $_records records to get the grants for
1386      * @param   string|Tinebase_Model_User  $_accountId the account to get the grants for
1387      * @param   string                      $_containerProperty container property
1388      * @param   string                      $_grantModel
1389      * @throws  Tinebase_Exception_NotFound
1390      */
1391     public function getGrantsOfRecords(Tinebase_Record_RecordSet $_records, $_accountId, $_containerProperty = 'container_id', $_grantModel = 'Tinebase_Model_Grants')
1392     {
1393         $containers = $this->getContainerGrantsOfRecords($_records, $_accountId, $_containerProperty, $_grantModel);
1394         
1395         if (!$containers) {
1396             return;
1397         }
1398         
1399         // add container & grants to records
1400         foreach ($_records as &$record) {
1401             if (!$containerId = $record->$_containerProperty) {
1402                 continue;
1403             }
1404             
1405             if (! is_array($containerId) && ! $containerId instanceof Tinebase_Record_Abstract && isset($containers[$containerId])) {
1406                 if (isset($containers[$containerId]->path)) {
1407                     $record->$_containerProperty = $containers[$containerId];
1408                 } else {
1409                     // if path is not determinable, skip this container
1410                     // @todo is it correct to remove record from recordSet???
1411                     $_records->removeRecord($record);
1412                 }
1413             }
1414         }
1415     }
1416     
1417     /**
1418      * get grants for containers assigned to given account of multiple records
1419      *
1420      * @param   Tinebase_Record_RecordSet   $_records records to get the grants for
1421      * @param   string|Tinebase_Model_User  $_accountId the account to get the grants for
1422      * @param   string                      $_containerProperty container property
1423      * @param   string                      $_grantModel
1424      * @throws  Tinebase_Exception_NotFound
1425      * @return  array of containers|void
1426      */
1427     public function getContainerGrantsOfRecords(Tinebase_Record_RecordSet $_records, $_accountId, $_containerProperty = 'container_id', $_grantModel = 'Tinebase_Model_Grants')
1428     {
1429         $containerIds = array();
1430         foreach ($_records as $record) {
1431             if (isset($record[$_containerProperty]) && !isset($containerIds[Tinebase_Model_Container::convertContainerId($record[$_containerProperty])])) {
1432                 $containerIds[Tinebase_Model_Container::convertContainerId($record[$_containerProperty])] = null;
1433             }
1434         }
1435         
1436         if (empty($containerIds)) {
1437             return array();
1438         }
1439         
1440         $accountId = $_accountId instanceof Tinebase_Record_Abstract
1441             ? $_accountId->getId()
1442             : $_accountId;
1443         
1444         $select = $this->_getSelect('*', TRUE)
1445             ->where("{$this->_db->quoteIdentifier('container.id')} IN (?)", array_keys($containerIds))
1446             ->join(array(
1447                 /* table  */ 'container_acl' => SQL_TABLE_PREFIX . 'container_acl'), 
1448                 /* on     */ "{$this->_db->quoteIdentifier('container_acl.container_id')} = {$this->_db->quoteIdentifier('container.id')}",
1449                 /* select */ array('*', 'account_grants' => $this->_dbCommand->getAggregate('container_acl.account_grant'))
1450             )
1451             ->group('container.id', 'container_acl.account_type', 'container_acl.account_id');
1452             
1453         $this->addGrantsSql($select, $accountId, '*');
1454         
1455         Tinebase_Backend_Sql_Abstract::traitGroup($select);
1456         
1457         $stmt = $this->_db->query('/*' . __FUNCTION__ . '*/' . $select);
1458         $rows = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
1459         
1460         $containers = array();
1461         // add results to container ids and get grants array
1462         foreach ($rows as $row) {
1463             // NOTE id is non-ambiguous
1464             $row['id']   = $row['container_id'];
1465             
1466             $grantsArray = array_unique(explode(',', $row['account_grants']));
1467             $row['account_grants'] = $this->_getGrantsFromArray($grantsArray, $accountId, $_grantModel)->toArray();
1468             
1469             $containers[$row['id']] = new Tinebase_Model_Container($row, TRUE);
1470             
1471             try {
1472                 $containers[$row['id']]->path = $containers[$row['id']]->getPath();
1473             } catch (Exception $e) {
1474                 // @todo is it correct to catch all exceptions here?
1475                 Tinebase_Exception::log($e);
1476             }
1477         }
1478         
1479         return $containers;
1480     }
1481     
1482     /**
1483      * set all grants for given container
1484      *
1485      * @param   int|Tinebase_Model_Container $_containerId
1486      * @param   Tinebase_Record_RecordSet $_grants
1487      * @param   boolean $_ignoreAcl
1488      * @param   boolean $_failSafe don't allow to remove all admin grants for container
1489      * @return  Tinebase_Record_RecordSet subtype Tinebase_Model_Grants
1490      * @throws  Tinebase_Exception_AccessDenied
1491      * @throws  Tinebase_Exception_Backend
1492      * @throws  Tinebase_Exception_Record_NotAllowed
1493      */
1494     public function setGrants($_containerId, Tinebase_Record_RecordSet $_grants, $_ignoreAcl = FALSE, $_failSafe = TRUE) 
1495     {
1496         $containerId = Tinebase_Model_Container::convertContainerId($_containerId);
1497         
1498         if($_ignoreAcl !== TRUE) {
1499             if(!$this->hasGrant(Tinebase_Core::getUser(), $containerId, Tinebase_Model_Grants::GRANT_ADMIN)) {
1500                 throw new Tinebase_Exception_AccessDenied('Permission to set grants of container denied.');
1501             }
1502         }
1503         
1504         // do failsafe check
1505         if ($_failSafe) {
1506             $adminGrant = FALSE;
1507             foreach ($_grants as $recordGrants) {
1508                 if ($recordGrants->{Tinebase_Model_Grants::GRANT_ADMIN}) {
1509                     $adminGrant = TRUE;
1510                 }
1511             }
1512             if (count($_grants) == 0 || ! $adminGrant) {
1513                 throw new Tinebase_Exception_UnexpectedValue('You are not allowed to remove all (admin) grants for this container.');
1514             }
1515         }
1516         
1517         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Setting grants for container id ' . $containerId . ' ...');
1518         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_grants->toArray(), TRUE));
1519         
1520         try {
1521
1522             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
1523             
1524             $where = $this->_getContainerAclTable()->getAdapter()->quoteInto($this->_db->quoteIdentifier('container_id') . ' = ?', $containerId);
1525             $this->_getContainerAclTable()->delete($where);
1526             
1527             foreach ($_grants as $recordGrants) {
1528                 $data = array(
1529                     'container_id'  => $containerId,
1530                     'account_id'    => $recordGrants['account_id'],
1531                     'account_type'  => $recordGrants['account_type'],
1532                 );
1533                 
1534                 foreach ($recordGrants as $grantName => $grant) {
1535                     if (in_array($grantName, $recordGrants->getAllGrants()) && $grant === TRUE) {
1536                         $data['id'] = $recordGrants->generateUID();
1537                         $this->_getContainerAclTable()->insert($data + array('account_grant' => $grantName));
1538                     }
1539                 }
1540             }
1541
1542             $newGrants = $this->getGrantsOfContainer($containerId, true);
1543             $this->_writeModLog(
1544                 new Tinebase_Model_Container(array('id' => $containerId, 'account_grants' => $newGrants), true),
1545                 new Tinebase_Model_Container(array('id' => $containerId), true)
1546             );
1547             
1548             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1549             
1550             $this->_setRecordMetaDataAndUpdate($containerId, 'update');
1551             
1552         } catch (Exception $e) {
1553             Tinebase_TransactionManager::getInstance()->rollBack();
1554             Tinebase_Exception::log($e);
1555             throw new Tinebase_Exception_Backend($e->getMessage());
1556         }
1557         
1558         return $this->getGrantsOfContainer($containerId, $_ignoreAcl);
1559     }
1560     
1561     /**
1562      * get owner (account_id) of container
1563      * 
1564      * @param Tinebase_Model_Container $_container
1565      * @return string|boolean
1566      */
1567     public function getContainerOwner(Tinebase_Model_Container $_container)
1568     {
1569         if ($_container->type !== Tinebase_Model_Container::TYPE_PERSONAL) {
1570             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
1571                 . ' Only personal containers have an owner.');
1572             return FALSE;
1573         }
1574
1575         if ($_container->owner_id) {
1576             return $_container->owner_id;
1577         }
1578         
1579         $grants = (! $_container->account_grants) ? $this->getGrantsOfContainer($_container, true) : $_container->account_grants;
1580         
1581         if (count($grants) === 0) {
1582             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
1583                 . ' Container ' . $_container->name . ' has no account grants.');
1584             return FALSE;
1585         }
1586         
1587         // return first admin user
1588         foreach ($grants as $grant) {
1589             if ($grant->{Tinebase_Model_Grants::GRANT_ADMIN} && $grant->account_type == Tinebase_Acl_Rights::ACCOUNT_TYPE_USER) {
1590                 return $grant->account_id;
1591             }
1592         }
1593         
1594         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
1595             __METHOD__ . '::' . __LINE__ . ' Container ' . $_container->name . ' has no owner.');
1596         
1597         return FALSE;
1598     }
1599     
1600     /**
1601      * only one admin is allowed for personal containers
1602      * 
1603      * @param $_container
1604      * @throws Tinebase_Exception_Record_NotAllowed
1605      * @throws Tinebase_Exception_UnexpectedValue
1606      *
1607      * @deprecated: could be removed because we have an owner property and could have multiple admins for a personal container now
1608      */
1609     public function checkContainerOwner(Tinebase_Model_Container $_container)
1610     {
1611         if ($_container->type !== Tinebase_Model_Container::TYPE_PERSONAL || empty($_container->account_grants)) {
1612             return;
1613         }
1614         
1615         if (! $_container->account_grants instanceof Tinebase_Record_RecordSet) {
1616             throw new Tinebase_Exception_UnexpectedValue('RecordSet of grants expected.');
1617         }
1618
1619         $_container->account_grants->addIndices(array(Tinebase_Model_Grants::GRANT_ADMIN));
1620         $adminGrants = $_container->account_grants->filter(Tinebase_Model_Grants::GRANT_ADMIN, TRUE);
1621         if (count($adminGrants) > 1) {
1622             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Multiple admin grants detected in container "' . $_container->name . '"');
1623             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($adminGrants->toArray(), TRUE));
1624             throw new Tinebase_Exception_Record_NotAllowed('Personal containers can have only one owner!', 403);
1625         }
1626     }
1627     
1628     /**
1629      * remove all container related entries from cache
1630      * 
1631      * @param int|Tinebase_Model_Container $containerId
1632      */
1633     protected function _clearCache($containerId) 
1634     {
1635         $containerId = Tinebase_Model_Container::convertContainerId($containerId);
1636         $cache = Tinebase_Core::getCache();
1637         
1638         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
1639             . ' Removing all cache entries for container id ' . $containerId);
1640         
1641         $idsToDelete = array(
1642             'getContainerById' . $containerId . 'd0',
1643             'getContainerById' . $containerId . 'd1',
1644         );
1645         
1646         foreach ($idsToDelete as $id) {
1647             $cache->remove($id);
1648         }
1649         
1650         $this->resetClassCache();
1651     }
1652
1653     /**
1654      * lazy loading for containerACLTable
1655      * 
1656      * @return Tinebase_Db_Table
1657      */
1658     protected function _getContainerAclTable()
1659     {
1660         if (! $this->_containerAclTable) {
1661             $this->_containerAclTable = new Tinebase_Db_Table(array('name' => SQL_TABLE_PREFIX . 'container_acl'));
1662         }
1663         
1664         return $this->_containerAclTable;
1665     }
1666     
1667     /**
1668      * get grants record from an array with grant values
1669      *
1670      * @param array $_grantsArray
1671      * @param int $_accountId
1672      * @param string $_grantModel
1673      * @return Tinebase_Model_Grants (or child class)
1674      */
1675     protected function _getGrantsFromArray(array $_grantsArray, $_accountId, $_grantModel = 'Tinebase_Model_Grants')
1676     {
1677         $grants = array();
1678         foreach($_grantsArray as $key => $value) {
1679             $grantValue = (is_array($value)) ? $value['account_grant'] : $value;
1680             $grants[$grantValue] = TRUE;
1681         }
1682         $grantsFields = array(
1683             'account_id'     => $_accountId,
1684             'account_type'   => Tinebase_Acl_Rights::ACCOUNT_TYPE_USER,
1685         );
1686         $grantsFields = array_merge($grantsFields, $grants);
1687         
1688         $grants = new $_grantModel($grantsFields, TRUE);
1689
1690         return $grants;
1691     }
1692
1693     /**
1694      * increase content sequence of container
1695      * - should be increased for each create/update/delete operation in this container
1696      * 
1697      * @param integer|Tinebase_Model_Container $containerId
1698      * @param string $action
1699      * @param string $recordId
1700      * @return integer new content seq
1701      */
1702     public function increaseContentSequence($containerId, $action = NULL, $recordId = NULL)
1703     {
1704         $containerId = Tinebase_Model_Container::convertContainerId($containerId);
1705
1706         $newContentSeq = NULL;
1707         try {
1708             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($this->_db);
1709         
1710             $quotedIdentifier = $this->_db->quoteIdentifier('content_seq');
1711             $data = array(
1712                 'content_seq' => new Zend_Db_Expr('(CASE WHEN ' . $quotedIdentifier . ' >= 1 THEN ' . $quotedIdentifier . ' + 1 ELSE 1 END)')
1713             );
1714             $where = array(
1715                 $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $containerId)
1716             );
1717             $this->_db->update($this->_tablePrefix . $this->_tableName, $data, $where);
1718             
1719             $newContentSeq = $this->getContentSequence($containerId);
1720             if ($newContentSeq === NULL) {
1721                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
1722                     . ' Something strange happend: content seq of NULL has been detected for container ' . $containerId . ' . Setting it to 0.');
1723                 $newContentSeq = 0;
1724             } else {
1725                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1726                     . ' Increased content seq of container ' . $containerId . ' to ' . $newContentSeq);
1727             }
1728             
1729             // create new entry in container_content table
1730             if ($action !== NULL && $recordId !== NULL) {
1731                 $contentRecord = new Tinebase_Model_ContainerContent(array(
1732                     'container_id' => $containerId,
1733                     'action'       => $action,
1734                     'record_id'    => $recordId,
1735                     'time'         => Tinebase_DateTime::now(),
1736                     'content_seq'  => $newContentSeq,
1737                 ));
1738                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
1739                     . ' Creating "' . $action . '" action content history record for record id ' . $recordId);
1740                 $this->getContentBackend()->create($contentRecord);
1741             }
1742             
1743             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1744             
1745             $this->resetClassCache();
1746             
1747         } catch (Exception $e) {
1748             Tinebase_TransactionManager::getInstance()->rollBack();
1749             throw $e;
1750         }
1751         
1752         return $newContentSeq;
1753     }
1754     
1755     /**
1756      * get content history since given content_seq
1757      *
1758      * returns null if the lastContentSeq was not found in DB and was not -1 or 0
1759      * -1 is used when paging or for inbox which actually doesnt have a sync token
1760      * 0 is the initial container content_seq, thus the initial sync token for an empty container
1761      * 
1762      * @param integer|Tinebase_Model_Container $containerId
1763      * @param integer $lastContentSeq
1764      * @return Tinebase_Record_RecordSet|null
1765      */
1766     public function getContentHistory($containerId, $lastContentSeq = -1)
1767     {
1768         $filter = new Tinebase_Model_ContainerContentFilter(array(
1769             array('field' => 'container_id', 'operator' => 'equals',  'value' => Tinebase_Model_Container::convertContainerId($containerId)),
1770             array('field' => 'content_seq',  'operator' => 'greater', 'value' => ($lastContentSeq == -1 ? $lastContentSeq : $lastContentSeq - 1)),
1771         ));
1772         $pagination = new Tinebase_Model_Pagination(array(
1773             'sort' => 'content_seq'
1774         ));
1775         $result = $this->getContentBackend()->search($filter, $pagination);
1776         if ($lastContentSeq != -1 && $lastContentSeq != 0) {
1777             if ($result->count() === 0) {
1778                 return null;
1779             }
1780             $firstRecord = $result->getFirstRecord();
1781             if ($firstRecord->content_seq != $lastContentSeq) {
1782                 return null;
1783             }
1784             $result->removeRecord($firstRecord);
1785         }
1786         return $result;
1787     }
1788
1789     /**
1790      * get content sequences for single container or array of ids
1791      * 
1792      * @param array|integer|Tinebase_Model_Container $containerIds
1793      * @return array with key = container id / value = content seq number | integer
1794      *
1795      * TODO improve function: should only have one param & return type
1796      */
1797     public function getContentSequence($containerIds)
1798     {
1799         if (empty($containerIds)) {
1800             return NULL;
1801         }
1802         
1803         $containerIds = (! is_array($containerIds))
1804             ? Tinebase_Model_Container::convertContainerId($containerIds)
1805             : $containerIds;
1806         
1807         $select = $this->_getSelect(array('id', 'content_seq'), TRUE);
1808         $select->where($this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' IN (?)', (array) $containerIds));
1809         $stmt = $this->_db->query('/*' . __FUNCTION__ . '*/' . $select);
1810         $result = $stmt->fetchAll();
1811         foreach ($result as $key => $value) {
1812             $result[$value['id']] = $value['content_seq'];
1813         }
1814
1815         $result = (is_array($containerIds))
1816             ? $result
1817             : (is_scalar($containerIds) && isset($result[$containerIds]) ? $result[$containerIds] : NULL);
1818         return $result;
1819     }
1820     
1821     /**
1822      * checks if container to delete is a "system" container 
1823      * 
1824      * @param array|integer|Tinebase_Model_Container $containerIds
1825      * @throws Tinebase_Exception_Record_SystemContainer
1826      * 
1827      * @TODO: generalize when there are more "system" containers
1828      * @todo move Admin_Model_Config::DEFAULTINTERNALADDRESSBOOK to adb config
1829      */
1830     public function checkSystemContainer($containerIds)
1831     {
1832         if (!is_array($containerIds)) $containerIds = array($containerIds);
1833         $appConfigDefaults = Admin_Controller::getInstance()->getConfigSettings();
1834         // at the moment, just the internal addressbook is checked
1835         try {
1836             $defaultAddressbook = $this->get($appConfigDefaults[Admin_Model_Config::DEFAULTINTERNALADDRESSBOOK])->toArray();
1837         } catch (Tinebase_Exception_NotFound $e) {
1838             $defaultAddressbook = null;
1839         }
1840
1841         if ($defaultAddressbook && in_array($defaultAddressbook['id'], $containerIds)) {
1842             // _('You are not allowed to delete this Container. Please define another container as the default addressbook for internal contacts!')
1843             throw new Tinebase_Exception_Record_SystemContainer('You are not allowed to delete this Container. Please define another container as the default addressbook for internal contacts!');
1844         }
1845     }
1846     
1847     /**
1848      * create a new system container
1849      * - by default user group gets READ grant
1850      * - by default admin group gets all grants
1851      *
1852      * NOTE: this should never be called in user land and only in admin/setup contexts
1853      * 
1854      * @param Tinebase_Model_Application|string $application app record, app id or app name
1855      * @param string $name
1856      * @param string $idConfig save id in config if given
1857      * @param Tinebase_Record_RecordSet $grants use this to overwrite default grants
1858      * @param string $model the model the container contains
1859      * @return Tinebase_Model_Container
1860      */
1861     public function createSystemContainer($application, $name, $configId = NULL, Tinebase_Record_RecordSet $grants = NULL, $model = NULL)
1862     {
1863         $application = ($application instanceof Tinebase_Model_Application) ? $application : Tinebase_Application::getInstance()->getApplicationById($application);
1864         if ($model === null) {
1865             $controller = Tinebase_Core::getApplicationInstance($application->name, /* $_modelName = */ '', /* $_ignoreACL = */ true);
1866             if (! method_exists($controller, 'getDefaultModel')) {
1867                 throw new Tinebase_Exception_UnexpectedValue('controller has no getDefaultModel method');
1868             }
1869             $model = $controller->getDefaultModel();
1870         }
1871         
1872         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
1873             . ' Creating system container for model ' . $model);
1874         
1875         $newContainer = new Tinebase_Model_Container(array(
1876             'name'              => $name,
1877             'type'              => Tinebase_Model_Container::TYPE_SHARED,
1878             'backend'           => 'Sql',
1879             'application_id'    => $application->getId(),
1880             'model'             => $model
1881         ));
1882
1883         $grants = ($grants) ? $grants : Tinebase_Model_Grants::getDefaultGrants();
1884         $newContainer = Tinebase_Container::getInstance()->addContainer($newContainer, $grants, TRUE);
1885
1886         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1887             . ' Created new system container ' . $name . ' for application ' . $application->name);
1888         
1889         if ($configId !== NULL) {
1890             $configClass = $application->name . '_Config';
1891             if (@class_exists($configClass)) {
1892                 $config = call_user_func(array($configClass, 'getInstance'));
1893                 
1894                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1895                     . ' Setting system container config "' . $configId . '" = ' . $newContainer->getId());
1896                 
1897                 $config->set($configId, $newContainer->getId());
1898             } else {
1899                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
1900                     . ' Could not find preferences class ' . $configClass);
1901             }
1902         }
1903         
1904         $this->resetClassCache();
1905         
1906         return $newContainer;
1907     }
1908
1909     /**
1910      * Updates existing container and clears the cache entry of the container
1911      *
1912      * @param Tinebase_Record_Interface $_record
1913      * @param boolean $_updateDeleted = false;
1914      * @return Tinebase_Record_Interface Record|NULL
1915      */
1916     public function update(Tinebase_Record_Interface $_record, $_updateDeleted = false)
1917     {
1918         $this->_clearCache($_record);
1919
1920         $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
1921
1922         //use get (avoids cache) or getContainerById, guess its better to avoid the cache
1923         $oldContainer = $this->get($_record->getId(), $_updateDeleted);
1924
1925         $result = parent::update($_record);
1926
1927         unset($result->account_grants);
1928         unset($oldContainer->account_grants);
1929         $this->_writeModLog($result, $oldContainer);
1930
1931         $event = new Tinebase_Event_Record_Update();
1932         $event->observable = $result;
1933         Tinebase_Record_PersistentObserver::getInstance()->fireEvent($event);
1934
1935         Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1936
1937         return $result;
1938     }
1939
1940     public function setContainerOwners()
1941     {
1942         $select = $this->_getSelect('id')
1943             ->where('type = ?', Tinebase_Model_Container::TYPE_PERSONAL)
1944             ->where('owner_id is null or owner_id = ?', '');
1945
1946         $stmt = $this->_db->query($select);
1947         $containers = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
1948
1949         $count = 0;
1950         foreach ($containers as $container) {
1951             $id = $container['id'];
1952             $grants = $this->getGrantsOfContainer($id, /* ignore acl */ true);
1953             foreach ($grants as $grant) {
1954                 if ($grant->adminGrant && $grant->account_type == 'user') {
1955                     if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1956                         . ' Set owner for container id ' . $id . ': ' .  $grant->account_id);
1957                     $where  = array(
1958                         $this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $id),
1959                     );
1960
1961                     $this->_db->update($this->_tablePrefix . $this->_tableName, array('owner_id' => $grant->account_id), $where);
1962
1963                     $count++;
1964                 }
1965             }
1966         }
1967
1968         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1969             . ' Set owner for ' . $count . ' containers.');
1970     }
1971
1972     /**
1973      * apply modification logs from a replication master locally
1974      *
1975      * @param Tinebase_Model_ModificationLog $_modification
1976      */
1977     public function applyReplicationModificationLog(Tinebase_Model_ModificationLog $_modification)
1978     {
1979
1980         switch ($_modification->change_type) {
1981             case Tinebase_Timemachine_ModificationLog::CREATED:
1982                 $diff = new Tinebase_Record_Diff(json_decode($_modification->new_value, true));
1983                 $model = $_modification->record_type;
1984                 $record = new $model($diff->diff);
1985                 $this->addContainer($record, null, true);
1986                 break;
1987
1988             case Tinebase_Timemachine_ModificationLog::UPDATED:
1989                 $diff = new Tinebase_Record_Diff(json_decode($_modification->new_value, true));
1990                 if (isset($diff->diff['account_grants'])) {
1991                     $this->setGrants($_modification->record_id, new Tinebase_Record_RecordSet('Tinebase_Model_Grants', $diff->diff['account_grants']), true, false);
1992                 } else {
1993                     $record = $this->get($_modification->record_id, true);
1994                     $record->applyDiff($diff);
1995                     $this->update($record, true);
1996                 }
1997                 break;
1998
1999             case Tinebase_Timemachine_ModificationLog::DELETED:
2000                 $this->deleteContainer($_modification->record_id, true);
2001                 break;
2002
2003             default:
2004                 throw new Tinebase_Exception('unknown Tinebase_Model_ModificationLog->change_type: ' . $_modification->change_type);
2005         }
2006     }
2007 }