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