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