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