01e2063543c90de8c2be30712142b5b9323be4e1
[tine20] / tine20 / Tinebase / Controller / Record / Abstract.php
1 <?php
2 /**
3  * Abstract record controller for Tine 2.0 applications
4  *
5  * @package     Tinebase
6  * @subpackage  Controller
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Philipp Schüle <p.schuele@metaways.de>
9  * @copyright   Copyright (c) 2007-2013 Metaways Infosystems GmbH (http://www.metaways.de)
10  *
11  * @todo        this should be splitted into smaller parts!
12  */
13
14 /**
15  * abstract record controller class for Tine 2.0 applications
16  *
17  * @package     Tinebase
18  * @subpackage  Controller
19  */
20 abstract class Tinebase_Controller_Record_Abstract
21     extends Tinebase_Controller_Abstract
22     implements Tinebase_Controller_Record_Interface, Tinebase_Controller_SearchInterface
23 {
24    /**
25      * application backend class
26      *
27      * @var Tinebase_Backend_Sql_Interface
28      */
29     protected $_backend;
30
31     /**
32      * Model name
33      *
34      * @var string
35      *
36      * @todo perhaps we can remove that and build model name from name of the class (replace 'Controller' with 'Model')
37      */
38     protected $_modelName;
39
40     /**
41      * check for container ACLs
42      *
43      * @var boolean
44      *
45      * @todo rename to containerACLChecks
46      */
47     protected $_doContainerACLChecks = TRUE;
48
49     /**
50      * do right checks - can be enabled/disabled by _setRightChecks
51      *
52      * @var boolean
53      */
54     protected $_doRightChecks = TRUE;
55
56     /**
57      * delete or just set is_delete=1 if record is going to be deleted
58      * - legacy code -> remove that when all backends/applications are using the history logging
59      *
60      * @var boolean
61      */
62     protected $_purgeRecords = TRUE;
63
64     /**
65      * omit mod log for this records
66      *
67      * @var boolean
68      */
69     protected $_omitModLog = FALSE;
70
71     /**
72      * resolve customfields in search()
73      *
74      * @var boolean
75      */
76     protected $_resolveCustomFields = FALSE;
77
78     /**
79      * send notifications?
80      *
81      * @var boolean
82      */
83     protected $_sendNotifications = FALSE;
84
85     /**
86      * if some of the relations should be deleted
87      *
88      * @var array
89      */
90     protected $_relatedObjectsToDelete = array();
91
92     /**
93      * record alarm field
94      *
95      * @var string
96      */
97     protected $_recordAlarmField = 'dtstart';
98
99     /**
100      * duplicate check fields / if this is NULL -> no duplicate check
101      *
102      * @var array
103      */
104     protected $_duplicateCheckFields = NULL;
105
106     /**
107      * holds new relation on update multiple
108      * @var array
109      */
110     protected $_newRelations = NULL;
111     
112     /**
113      * holds relations to remove on update multiple
114      * @var array
115      */
116     protected $_removeRelations = NULL;
117     
118     /**
119      * result of updateMultiple function
120      * 
121      * @var array
122      */
123     protected $_updateMultipleResult = array();
124
125     /**
126      * should each record be validated in updateMultiple 
127      * - FALSE: only the first record is validated with the incoming data
128      *
129      * @var boolean
130      */
131     protected $_updateMultipleValidateEachRecord = FALSE;
132
133     /**
134      * returns controller for records of given model
135      *
136      * @param string $_model
137      */
138     public static function getController($_model)
139     {
140         list($appName, $i, $modelName) = explode('_', $_model);
141         return Tinebase_Core::getApplicationInstance($appName, $modelName);
142     }
143
144     /*********** get / search / count **************/
145
146     /**
147      * get list of records
148      *
149      * @param Tinebase_Model_Filter_FilterGroup|optional $_filter
150      * @param Tinebase_Model_Pagination|optional $_pagination
151      * @param boolean $_getRelations
152      * @param boolean $_onlyIds
153      * @param string $_action for right/acl check
154      * @return Tinebase_Record_RecordSet|array
155      */
156     public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL, $_getRelations = FALSE, $_onlyIds = FALSE, $_action = 'get')
157     {
158         $this->_checkRight($_action);
159         $this->checkFilterACL($_filter, $_action);
160         $this->_addDefaultFilter($_filter);
161
162         $result = $this->_backend->search($_filter, $_pagination, $_onlyIds);
163
164         if (! $_onlyIds) {
165             if ($_getRelations) {
166                 $result->setByIndices('relations', Tinebase_Relations::getInstance()->getMultipleRelations($this->_modelName, $this->_getBackendType(), $result->getId()));
167             }
168             if ($this->resolveCustomfields()) {
169                 Tinebase_CustomField::getInstance()->resolveMultipleCustomfields($result);
170             }
171         }
172
173         return $result;
174     }
175     
176     /**
177      * you can define default filters here
178      * 
179      * @param Tinebase_Model_Filter_FilterGroup $_filter
180      */
181     protected function _addDefaultFilter(Tinebase_Model_Filter_FilterGroup $_filter = NULL)
182     {
183         
184     }
185
186     /**
187      * Gets total count of search with $_filter
188      *
189      * @param Tinebase_Model_Filter_FilterGroup $_filter
190      * @param string $_action for right/acl check
191      * @return int
192      */
193     public function searchCount(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
194     {
195         $this->checkFilterACL($_filter, $_action);
196
197         $count = $this->_backend->searchCount($_filter);
198
199         return $count;
200     }
201
202     /**
203      * set/get the sendNotifications state
204      *
205      * @param  boolean optional
206      * @return boolean
207      */
208     public function sendNotifications()
209     {
210         $value = (func_num_args() === 1) ? (bool) func_get_arg(0) : NULL;
211         return $this->_setBooleanMemberVar('_sendNotifications', $value);
212     }
213     
214     /**
215      * set/get a boolean member var
216      * 
217      * @param string $name
218      * @param boolean $value
219      * @return boolean
220      */
221     protected function _setBooleanMemberVar($name, $value = NULL)
222     {
223         $currValue = $this->{$name};
224         if ($value !== NULL) {
225             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Resetting ' . $name . ' to ' . (int) $value);
226             $this->{$name} = $value;
227         }
228         
229         return $currValue;
230     }
231
232     /**
233      * set/get purging of record when deleting
234      *
235      * @param  boolean optional
236      * @return boolean
237      */
238     public function purgeRecords()
239     {
240         $value = (func_num_args() === 1) ? (bool) func_get_arg(0) : NULL;
241         return $this->_setBooleanMemberVar('_purgeRecords', $value);
242     }
243
244     /**
245      * set/get checking ACL rights
246      *
247      * @param  boolean optional
248      * @return boolean
249      */
250     public function doContainerACLChecks()
251     {
252         $value = (func_num_args() === 1) ? (bool) func_get_arg(0) : NULL;
253         return $this->_setBooleanMemberVar('_doContainerACLChecks', $value);
254     }
255     
256     /**
257      * set/get resolving of customfields
258      *
259      * @param  boolean optional
260      * @return boolean
261      */
262     public function resolveCustomfields()
263     {
264         $value = (func_num_args() === 1) ? (bool) func_get_arg(0) : NULL;
265         $currentValue = ($this->_setBooleanMemberVar('_resolveCustomFields', $value) 
266             && Tinebase_CustomField::getInstance()->appHasCustomFields($this->_applicationName, $this->_modelName));
267         return $currentValue;
268     }
269
270     /**
271      * set/get modlog active
272      *
273      * @param  boolean optional
274      * @return boolean
275      */
276     public function modlogActive()
277     {
278         if (! $this->_backend) {
279             throw new Tinebase_Exception_NotFound('Backend not defined');
280         }
281
282         $currValue = $this->_backend->getModlogActive();
283         if (func_num_args() === 1) {
284             $paramValue = (bool) func_get_arg(0);
285             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Resetting modlog active to ' . (int) $paramValue);
286             $this->_backend->setModlogActive($paramValue);
287             $this->_omitModLog = ! $paramValue;
288         }
289
290         return $currValue;
291     }
292
293     /**
294      * get by id
295      *
296      * @param string $_id
297      * @param int $_containerId
298      * @return Tinebase_Record_Interface
299      * @throws Tinebase_Exception_AccessDenied
300      */
301     public function get($_id, $_containerId = NULL)
302     {
303         $this->_checkRight('get');
304         
305         if (! $_id) { // yes, we mean 0, null, false, ''
306             $record = new $this->_modelName(array(), true);
307             
308             if ($this->_doContainerACLChecks) {
309                 if ($_containerId === NULL) {
310                     $containers = Tinebase_Container::getInstance()->getPersonalContainer(Tinebase_Core::getUser(), $this->_applicationName, Tinebase_Core::getUser(), Tinebase_Model_Grants::GRANT_ADD);
311                     $record->container_id = $containers[0]->getId();
312                 } else {
313                     $record->container_id = $_containerId;
314                 }
315             }
316             
317         } else {
318             $record = $this->_backend->get($_id);
319             $this->_checkGrant($record, 'get');
320             $this->_getRelatedData($record);
321             
322             if ($record->has('notes')) {
323                 $record->notes = Tinebase_Notes::getInstance()->getNotesOfRecord($this->_modelName, $record->getId());
324             }
325         }
326         
327         return $record;
328     }
329     
330     /**
331      * check if record with given $id exists
332      * 
333      * @param string $id
334      * @return boolean
335      */
336     public function exists($id)
337     {
338         $this->_checkRight('get');
339         
340         try {
341             $record = $this->_backend->get($id);
342             $result = $this->_checkGrant($record, 'get', FALSE);
343         } catch (Tinebase_Exception_NotFound $tenf) {
344             $result = FALSE;
345         }
346         
347         return $result;
348     }
349     
350     /**
351      * add related data to record
352      * 
353      * @param Tinebase_Record_Interface $record
354      */
355     protected function _getRelatedData($record)
356     {
357         if ($record->has('tags')) {
358             Tinebase_Tags::getInstance()->getTagsOfRecord($record);
359         }
360         if ($record->has('relations')) {
361             $record->relations = Tinebase_Relations::getInstance()->getRelations($this->_modelName, $this->_getBackendType(), $record->getId());
362         }
363         if ($record->has('alarms')) {
364             $this->getAlarms($record);
365         }
366         if ($this->resolveCustomfields()) {
367             Tinebase_CustomField::getInstance()->resolveRecordCustomFields($record);
368         }
369         if ($record->has('attachments')) {
370             Tinebase_FileSystem_RecordAttachments::getInstance()->getRecordAttachments($record);
371         }
372     }
373
374     /**
375      * Returns a set of records identified by their id's
376      *
377      * @param   array $_ids       array of record identifiers
378      * @param   bool  $_ignoreACL don't check acl grants
379      * @return  Tinebase_Record_RecordSet of $this->_modelName
380      */
381     public function getMultiple($_ids, $_ignoreACL = FALSE)
382     {
383         $this->_checkRight('get');
384
385         // get all allowed containers and add them to getMultiple query
386         $containerIds = ($this->_doContainerACLChecks && $_ignoreACL !== TRUE)
387            ? Tinebase_Container::getInstance()->getContainerByACL(
388                Tinebase_Core::getUser(),
389                $this->_applicationName,
390                Tinebase_Model_Grants::GRANT_READ,
391                TRUE)
392            : NULL;
393         $records = $this->_backend->getMultiple($_ids, $containerIds);
394
395         if ($this->resolveCustomfields()) {
396             Tinebase_CustomField::getInstance()->resolveMultipleCustomfields($records);
397         }
398
399         return $records;
400     }
401
402     /**
403      * Gets all entries
404      *
405      * @param string $_orderBy Order result by
406      * @param string $_orderDirection Order direction - allowed are ASC and DESC
407      * @throws Tinebase_Exception_InvalidArgument
408      * @return Tinebase_Record_RecordSet
409      */
410     public function getAll($_orderBy = 'id', $_orderDirection = 'ASC')
411     {
412         $this->_checkRight('get');
413
414         $records = $this->_backend->getAll($_orderBy, $_orderDirection);
415
416         if ($this->resolveCustomfields()) {
417             Tinebase_CustomField::getInstance()->resolveMultipleCustomfields($records);
418         }
419
420         return $records;
421     }
422
423     /*************** add / update / delete / move *****************/
424
425     /**
426      * add one record
427      *
428      * @param   Tinebase_Record_Interface $_record
429      * @param   boolean $_duplicateCheck
430      * @return  Tinebase_Record_Interface
431      * @throws  Tinebase_Exception_AccessDenied
432      */
433     public function create(Tinebase_Record_Interface $_record, $_duplicateCheck = TRUE)
434     {
435         $this->_checkRight('create');
436
437         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' '
438             . print_r($_record->toArray(),true));
439         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
440             . ' Create new ' . $this->_modelName);
441
442         $db = (method_exists($this->_backend, 'getAdapter')) ? $this->_backend->getAdapter() : Tinebase_Core::getDb();
443         
444         try {
445             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
446
447             // add personal container id if container id is missing in record
448             if ($_record->has('container_id') && empty($_record->container_id)) {
449                 $containers = Tinebase_Container::getInstance()->getPersonalContainer(Tinebase_Core::getUser(), $this->_applicationName, Tinebase_Core::getUser(), Tinebase_Model_Grants::GRANT_ADD);
450                 $_record->container_id = $containers[0]->getId();
451             }
452
453             $_record->isValid(TRUE);
454
455             $this->_checkGrant($_record, 'create');
456
457             if ($_record->has('created_by')) {
458                 Tinebase_Timemachine_ModificationLog::setRecordMetaData($_record, 'create');
459             }
460
461             $this->_inspectBeforeCreate($_record);
462             if ($_duplicateCheck) {
463                 $this->_duplicateCheck($_record);
464             }
465             $createdRecord = $this->_backend->create($_record);
466             $this->_inspectAfterCreate($createdRecord, $_record);
467             $this->_setRelatedData($createdRecord, $_record);
468             $this->_setNotes($createdRecord, $_record);
469
470             if ($this->sendNotifications()) {
471                 $this->doSendNotifications($createdRecord, Tinebase_Core::getUser(), 'created');
472             }
473             
474             $this->_increaseContainerContentSequence($createdRecord, Tinebase_Model_ContainerContent::ACTION_CREATE);
475
476             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
477
478         } catch (Exception $e) {
479             $this->_handleRecordCreateOrUpdateException($e);
480         }
481
482         return $this->get($createdRecord);
483     }
484     
485     /**
486      * handle record exception
487      * 
488      * @param Exception $e
489      * @throws Exception
490      * 
491      * @todo invent hooking mechanism for database/backend independant exception handling (like lock timeouts)
492      */
493     protected function _handleRecordCreateOrUpdateException(Exception $e)
494     {
495         Tinebase_TransactionManager::getInstance()->rollBack();
496         
497         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' ' . $e->getMessage());
498         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $e->getTraceAsString());
499         
500         if ($e instanceof Zend_Db_Statement_Exception && preg_match('/Lock wait timeout exceeded/', $e->getMessage())) {
501             throw new Tinebase_Exception_Backend_Database_LockTimeout($e->getMessage());
502         }
503         
504         throw $e;
505     }
506     
507     /**
508      * inspect creation of one record (before create)
509      *
510      * @param   Tinebase_Record_Interface $_record
511      * @return  void
512      */
513     protected function _inspectBeforeCreate(Tinebase_Record_Interface $_record)
514     {
515
516     }
517
518     /**
519      * do duplicate check (before create)
520      *
521      * @param   Tinebase_Record_Interface $_record
522      * @return  void
523      * @throws Tinebase_Exception_Duplicate
524      */
525     protected function _duplicateCheck(Tinebase_Record_Interface $_record)
526     {
527         $duplicateFilter = $this->_getDuplicateFilter($_record);
528
529         if ($duplicateFilter === NULL) {
530             return;
531         }
532
533         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
534             ' Doing duplicate check.');
535
536         $duplicates = $this->search($duplicateFilter, new Tinebase_Model_Pagination(array('limit' => 5)));
537
538         if (count($duplicates) > 0) {
539             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
540                 ' Found ' . count($duplicates) . ' duplicate(s).');
541
542             $ted = new Tinebase_Exception_Duplicate('Duplicate record(s) found');
543             $ted->setModelName($this->_modelName);
544             $ted->setData($duplicates);
545             $ted->setClientRecord($_record);
546             throw $ted;
547         } else {
548             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
549                 ' No duplicates found.');
550         }
551     }
552
553     /**
554      * get duplicate filter
555      *
556      * @param Tinebase_Record_Interface $_record
557      * @return Tinebase_Model_Filter_FilterGroup|NULL
558      */
559     protected function _getDuplicateFilter(Tinebase_Record_Interface $_record)
560     {
561         if (empty($this->_duplicateCheckFields)) {
562             return NULL;
563         }
564
565         $filters = array();
566         foreach ($this->_duplicateCheckFields as $group) {
567             $addFilter = array();
568             foreach ($group as $field) {
569                 if (! empty($_record->{$field})) {
570                     $addFilter[] = array('field' => $field, 'operator' => 'equals', 'value' => $_record->{$field});
571                 }
572             }
573             if (! empty($addFilter)) {
574                 $filters[] = array('condition' => 'AND', 'filters' => $addFilter);
575             }
576         }
577
578         if (empty($filters)) {
579             return NULL;
580         }
581
582         $filterClass = $this->_modelName . 'Filter';
583         $filterData = (count($filters) > 1) ? array(array('condition' => 'OR', 'filters' => $filters)) : $filters;
584
585         // exclude own record if it has an id
586         $recordId = $_record->getId();
587         if (! empty($recordId)) {
588             $filterData[] = array('field' => 'id', 'operator' => 'notin', 'value' => array($recordId));
589         }
590         
591         $filter = new $filterClass($filterData);
592         
593         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($filter->toArray(), TRUE));
594         
595         return $filter;
596     }
597
598     /**
599      * inspect creation of one record (after create)
600      *
601      * @param   Tinebase_Record_Interface $_createdRecord
602      * @param   Tinebase_Record_Interface $_record
603      * @return  void
604      */
605     protected function _inspectAfterCreate($_createdRecord, Tinebase_Record_Interface $_record)
606     {
607     }
608
609     /**
610      * increase container content sequence
611      * 
612      * @param Tinebase_Record_Interface $_record
613      * @param string $action
614      */
615     protected function _increaseContainerContentSequence(Tinebase_Record_Interface $record, $action = NULL)
616     {
617         if ($record->has('container_id')) {
618             Tinebase_Container::getInstance()->increaseContentSequence($record->container_id, $action, $record->getId());
619         }
620     }
621     
622     /**
623      * update one record
624      *
625      * @param   Tinebase_Record_Interface $_record
626      * @param   boolean $_duplicateCheck
627      * @return  Tinebase_Record_Interface
628      * @throws  Tinebase_Exception_AccessDenied
629      * 
630      * @todo    fix duplicate check on update / merge needs to remove the changed record / ux discussion
631      */
632     public function update(Tinebase_Record_Interface $_record, $_duplicateCheck = TRUE)
633     {
634         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' '
635             . ' Record to update: ' . print_r($_record->toArray(), TRUE));
636         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
637             . ' Update ' . $this->_modelName);
638
639         $db = (method_exists($this->_backend, 'getAdapter')) ? $this->_backend->getAdapter() : Tinebase_Core::getDb();
640         
641         try {
642             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
643
644             $_record->isValid(TRUE);
645             $currentRecord = $this->get($_record->getId());
646             
647             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
648                 . ' Current record: ' . print_r($currentRecord->toArray(), TRUE));
649
650             $this->_updateACLCheck($_record, $currentRecord);
651             $this->_concurrencyManagement($_record, $currentRecord);
652             $this->_inspectBeforeUpdate($_record, $currentRecord);
653             
654             // NOTE removed the duplicate check because we can not remove the changed record yet
655 //             if ($_duplicateCheck) {
656 //                 $this->_duplicateCheck($_record);
657 //             }
658             
659             $updatedRecord = $this->_backend->update($_record);
660             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
661                 . ' Updated record: ' . print_r($updatedRecord->toArray(), TRUE));
662             
663             $this->_inspectAfterUpdate($updatedRecord, $_record, $currentRecord);
664             $updatedRecordWithRelatedData = $this->_setRelatedData($updatedRecord, $_record, TRUE);
665             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
666                 . ' Updated record with related data: ' . print_r($updatedRecordWithRelatedData->toArray(), TRUE));
667             
668             $currentMods = $this->_writeModLog($updatedRecordWithRelatedData, $currentRecord);
669             $this->_setNotes($updatedRecordWithRelatedData, $_record, Tinebase_Model_Note::SYSTEM_NOTE_NAME_CHANGED, $currentMods);
670             
671             if ($this->_sendNotifications && count($currentMods) > 0) {
672                 $this->doSendNotifications($updatedRecordWithRelatedData, Tinebase_Core::getUser(), 'changed', $currentRecord);
673             }
674             
675             if ($_record->has('container_id') && $currentRecord->container_id !== $updatedRecord->container_id) {
676                 $this->_increaseContainerContentSequence($currentRecord, Tinebase_Model_ContainerContent::ACTION_DELETE);
677                 $this->_increaseContainerContentSequence($updatedRecord, Tinebase_Model_ContainerContent::ACTION_CREATE);
678             } else {
679                 $this->_increaseContainerContentSequence($updatedRecord, Tinebase_Model_ContainerContent::ACTION_UPDATE);
680             }
681
682             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
683
684         } catch (Exception $e) {
685             $this->_handleRecordCreateOrUpdateException($e);
686         }
687         return $this->get($updatedRecord->getId());
688     }
689     
690     /**
691      * do ACL check for update record
692      * 
693      * @param Tinebase_Record_Interface $_record
694      * @param Tinebase_Record_Interface $_currentRecord
695      */
696     protected function _updateACLCheck($_record, $_currentRecord)
697     {
698         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
699             . ' Doing ACL check ...');
700         
701         if ($_currentRecord->has('container_id') && $_currentRecord->container_id != $_record->container_id) {
702             $this->_checkGrant($_record, 'create');
703             $this->_checkRight('create');
704             // NOTE: It's not yet clear if we have to demand delete grants here or also edit grants would be fine
705             $this->_checkGrant($_currentRecord, 'delete');
706             $this->_checkRight('delete');
707         } else {
708             $this->_checkGrant($_record, 'update', TRUE, 'No permission to update record.', $_currentRecord);
709             $this->_checkRight('update');
710         }
711     }
712     
713     /**
714      * concurrency management & history log
715      * 
716      * @param Tinebase_Record_Interface $_record
717      * @param Tinebase_Record_Interface $_currentRecord
718      */
719     protected function _concurrencyManagement($_record, $_currentRecord)
720     {
721         if (! $_record->has('created_by')) {
722             return NULL;
723         }
724         
725         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
726             . ' Doing concurrency check ...');
727
728         $modLog = Tinebase_Timemachine_ModificationLog::getInstance();
729         $modLog->manageConcurrentUpdates($_record, $_currentRecord);
730         $modLog->setRecordMetaData($_record, 'update', $_currentRecord);
731     }
732     
733     /**
734      * get backend type
735      * 
736      * @return string
737      */
738     protected function _getBackendType()
739     {
740         $type = (method_exists( $this->_backend, 'getType')) ? $this->_backend->getType() : 'Sql';
741         return $type;
742     }
743
744     /**
745      * write modlog
746      * 
747      * @param Tinebase_Record_Interface $_newRecord
748      * @param Tinebase_Record_Interface $_oldRecord
749      * @return NULL|Tinebase_Record_RecordSet
750      */
751     protected function _writeModLog($_newRecord, $_oldRecord)
752     {
753         if (! $_newRecord->has('created_by') || $this->_omitModLog === TRUE) {
754             return NULL;
755         }
756
757         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
758             . ' Writing modlog for ' . get_class($_newRecord));
759     
760         $currentMods = Tinebase_Timemachine_ModificationLog::getInstance()->writeModLog($_newRecord, $_oldRecord, $this->_modelName, $this->_getBackendType(), $_newRecord->getId());
761         
762         return $currentMods;
763     }
764     
765     /**
766      * set relations / tags / alarms
767      * 
768      * @param   Tinebase_Record_Interface $updatedRecord   the just updated record
769      * @param   Tinebase_Record_Interface $record          the update record
770      * @param   boolean $returnUpdatedRelatedData
771      * @return  Tinebase_Record_Interface
772      */
773     protected function _setRelatedData($updatedRecord, $record, $returnUpdatedRelatedData = FALSE)
774     {
775         if ($record->has('relations') && isset($record->relations) && is_array($record->relations)) {
776             $type = $this->_getBackendType();
777             Tinebase_Relations::getInstance()->setRelations($this->_modelName, $type, $updatedRecord->getId(), $record->relations);
778         }
779         if ($record->has('tags') && isset($record->tags) && (is_array($record->tags) || $record->tags instanceof Tinebase_Record_RecordSet)) {
780             $updatedRecord->tags = $record->tags;
781             Tinebase_Tags::getInstance()->setTagsOfRecord($updatedRecord);
782         }
783         if ($record->has('alarms') && isset($record->alarms)) {
784             $this->_saveAlarms($record);
785         }
786         if ($record->has('attachments') && isset($record->attachments)) {
787             $updatedRecord->attachments = $record->attachments;
788             Tinebase_FileSystem_RecordAttachments::getInstance()->setRecordAttachments($updatedRecord);
789         }
790         
791         if ($returnUpdatedRelatedData) {
792             $this->_getRelatedData($updatedRecord);
793         }
794         
795         return $updatedRecord;
796     }
797
798     /**
799      * set notes
800      * 
801      * @param   Tinebase_Record_Interface $_updatedRecord   the just updated record
802      * @param   Tinebase_Record_Interface $_record          the update record
803      * @param   string $_systemNoteType
804      * @param   Tinebase_Record_RecordSet $_currentMods
805      */
806     protected function _setNotes($_updatedRecord, $_record, $_systemNoteType = Tinebase_Model_Note::SYSTEM_NOTE_NAME_CREATED, $_currentMods = NULL)
807     {
808         if (! $_record->has('notes')) {
809             return;
810         }
811
812         if (isset($_record->notes) && is_array($_record->notes)) {
813             $_updatedRecord->notes = $_record->notes;
814             Tinebase_Notes::getInstance()->setNotesOfRecord($_updatedRecord);
815         }
816         Tinebase_Notes::getInstance()->addSystemNote($_updatedRecord, Tinebase_Core::getUser(), $_systemNoteType, $_currentMods);
817     }
818     
819     /**
820      * inspect update of one record (before update)
821      *
822      * @param   Tinebase_Record_Interface $_record      the update record
823      * @param   Tinebase_Record_Interface $_oldRecord   the current persistent record
824      * @return  void
825      */
826     protected function _inspectBeforeUpdate($_record, $_oldRecord)
827     {
828     }
829
830     /**
831      * inspect update of one record (after update)
832      *
833      * @param   Tinebase_Record_Interface $updatedRecord   the just updated record
834      * @param   Tinebase_Record_Interface $record          the update record
835      * @param   Tinebase_Record_Interface $currentRecord   the current record (before update)
836      * @return  void
837      */
838     protected function _inspectAfterUpdate($updatedRecord, $record, $currentRecord)
839     {
840     }
841
842     /**
843      * update modlog / metadata / add systemnote for multiple records defined by filter
844      * 
845      * NOTE: this should be done in a transaction because of the concurrency handling as
846      *  we want the same seq in the record and in the modlog
847      * 
848      * @param Tinebase_Model_Filter_FilterGroup|array $_filterOrIds
849      * @param array $_oldData
850      * @param array $_newData
851      */
852     public function concurrencyManagementAndModlogMultiple($_filterOrIds, $_oldData, $_newData)
853     {
854         $ids = ($_filterOrIds instanceof Tinebase_Model_Filter_FilterGroup) ? $this->search($_filterOrIds, NULL, FALSE, TRUE, 'update') : $_filterOrIds;
855         if (! is_array($ids) || count($ids) === 0) {
856             return;
857         }
858         
859         if ($this->_omitModLog !== TRUE) {
860             $recordSeqs = $this->_backend->getPropertyByIds($ids, 'seq');
861             
862             list($currentAccountId, $currentTime) = Tinebase_Timemachine_ModificationLog::getCurrentAccountIdAndTime();
863             $updateMetaData = array(
864                 'last_modified_by'   => $currentAccountId,
865                 'last_modified_time' => $currentTime,
866                 'seq'                => new Zend_Db_Expr('seq + 1'),
867                 'recordSeqs'         => $recordSeqs, // is not written to DB yet
868             );
869         } else {
870             $updateMetaData = array();
871         }
872         
873         $this->_backend->updateMultiple($ids, $updateMetaData);
874         
875         if ($this->_omitModLog !== TRUE && is_object(Tinebase_Core::getUser())) {
876             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
877                 . ' Writing modlog for ' . count($ids) . ' records ...');
878             
879             $currentMods = Tinebase_Timemachine_ModificationLog::getInstance()->writeModLogMultiple($ids, $_oldData, $_newData, $this->_modelName, $this->_getBackendType(), $updateMetaData);
880             Tinebase_Notes::getInstance()->addMultipleModificationSystemNotes($currentMods, $currentAccountId);
881         }
882     }
883     
884     /**
885      * handles relations on update multiple
886      * @param string $key
887      * @param string $value
888      * @throws Tinebase_Exception_Record_DefinitionFailure
889      */
890     protected function _handleRelations($key, $value)
891     {
892         $model = new $this->_modelName;
893         $relConfig = $model::getRelatableConfig();
894         unset($model);
895         $getRelations = true;
896         preg_match('/%(.+)-((.+)_Model_(.+))/', $key, $a);
897         if(count($a) < 4) {
898             throw new Tinebase_Exception_Record_DefinitionFailure('The relation to delete/set is not configured properly!');
899         } 
900         // TODO: check config from foreign side
901         // $relConfig = $a[2]::getRelatableConfig();
902
903         $constrainsConfig = false;
904         foreach($relConfig as $config) {
905             if($config['relatedApp'] == $a[3] && $config['relatedModel'] == $a[4] && array_key_exists('config', $config) && is_array($config['config'])) {
906                 foreach($config['config'] as $constrain) {
907                     if($constrain['type'] == $a[1]) {
908                         $constrainsConfig = $constrain;
909                         break 2; 
910                     }
911                 }
912             }
913         }
914
915         if(!$constrainsConfig) {
916             throw new Tinebase_Exception_Record_DefinitionFailure('No relation definition could be found for this model!');
917         }
918
919         $rel = array(
920             'own_model' => $this->_modelName,
921             'own_backend' => 'Sql',
922             'own_degree' =>array_key_exists('sibling', $constrainsConfig) ? $constrainsConfig['sibling'] : 'sibling',
923             'related_model' => $a[2],
924             'related_backend' => 'Sql',
925             'type' => array_key_exists('type', $constrainsConfig) ? $constrainsConfig['type'] : '-',
926             'remark' => array_key_exists('defaultRemark', $constrainsConfig) ? $constrainsConfig['defaultRemark'] : ' '
927         );
928         
929         if(empty($value)) { // delete relations in iterator
930             if(!$this->_removeRelations) $this->removeRelations = array();
931             $this->_removeRelations[] = $rel;
932         } else { // create relations in iterator
933             if(! $this->_newRelations) $this->_newRelations = array();
934             $rel['related_id'] = $value;
935             $this->_newRelations[] = $rel;
936         }
937     }
938     /**
939      * update multiple records
940      *
941      * @param   Tinebase_Model_Filter_FilterGroup $_filter
942      * @param   array $_data
943      * @return  integer number of updated records
944      * 
945      * @todo add param $_returnFullResults (if false, do not return updated records in 'results')
946      */
947     public function updateMultiple($_filter, $_data)
948     {
949         $this->_checkRight('update');
950         $this->checkFilterACL($_filter, 'update');
951         $getRelations = false;
952         
953         $this->_newRelations = NULL;
954         $this->_removeRelations = NULL;
955         
956         foreach($_data as $key => $value) {
957             if(stristr($key,'#')) {
958                 $_data['customfields'][substr($key,1)] = $value;
959                 unset($_data[$key]);
960             }
961             if(stristr($key, '%')) {
962                 $getRelations = true;
963                 $this->_handleRelations($key, $value);
964                 unset($_data[$key]);
965             }
966         }
967
968         $this->_updateMultipleResult = array(
969             'results'           => new Tinebase_Record_RecordSet($this->_modelName),
970             'exceptions'        => new Tinebase_Record_RecordSet('Tinebase_Model_UpdateMultipleException'),
971             'totalcount'        => 0,
972             'failcount'         => 0,
973         );
974         // TODO: idProperty should be callable statically
975         $model = new $this->_modelName();
976         $idProperty = $model->getIdProperty();
977         unset($model);
978         
979         $iterator = new Tinebase_Record_Iterator(array(
980             'iteratable' => $this,
981             'controller' => $this,
982             'filter'     => $_filter,
983             'options'    => array('idProperty' => $idProperty, 'getRelations' => $getRelations),
984             'function'   => 'processUpdateMultipleIteration',
985         ));
986         $result = $iterator->iterate($_data);
987     
988         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Updated ' . $this->_updateMultipleResult['totalcount'] . ' records.');
989         
990         return $this->_updateMultipleResult;
991     }
992     
993     /**
994      * iterate relations
995      * 
996      * @param Tinebase_Record_Abstract $currentRecord
997      * @return array
998      */
999     protected function _iterateRelations($currentRecord)
1000     {
1001         if(! $currentRecord->relations || get_class($currentRecord->relations) != 'Tinebase_Record_RecordSet') {
1002             $currentRecord->relations = new Tinebase_Record_RecordSet('Tinebase_Model_Relation');
1003         }
1004         
1005         // handle relations to remove
1006         if($this->_removeRelations) {
1007             if($currentRecord->relations->count()) {
1008                 foreach($this->_removeRelations as $remRelation) {
1009                     $removeRelations = $currentRecord->relations->filter('type', $remRelation['type']);
1010                     $removeRelations = $removeRelations->filter('related_model', $remRelation['related_model']);
1011                     $removeRelations = $removeRelations->filter('own_degree', $remRelation['own_degree']);
1012                     $currentRecord->relations->removeRecords($removeRelations);
1013                 }
1014             }
1015         }
1016         
1017         // handle new relations
1018         if($this->_newRelations) {
1019             $removeRelations = NULL;
1020             foreach($this->_newRelations as $newRelation) {
1021                 $removeRelations = $currentRecord->relations->filter('type', $newRelation['type']);
1022                 $removeRelations = $removeRelations->filter('related_model', $newRelation['related_model']);
1023                 $removeRelations = $removeRelations->filter('own_degree', $newRelation['own_degree']);
1024                 $already = $removeRelations->filter('related_id', $newRelation['related_id']);
1025                 if($already->count() > 0) {
1026                     $removeRelations = NULL;
1027                 } else {
1028                     $newRelation['own_id'] = $currentRecord->getId();
1029                     $rel = new Tinebase_Model_Relation();
1030                     $rel->setFromArray($newRelation);
1031                     if($removeRelations) $currentRecord->relations->removeRecords($removeRelations);
1032                     $currentRecord->relations->addRecord($rel);
1033                 }
1034             }
1035         }
1036         return $currentRecord->relations->toArray();
1037     }
1038     
1039     /**
1040     * update multiple records in an iteration
1041     * @see Tinebase_Record_Iterator / self::updateMultiple()
1042     *
1043     * @param Tinebase_Record_RecordSet $_records
1044     * @param array $_data
1045     */
1046     public function processUpdateMultipleIteration($_records, $_data)
1047     {
1048         if (count($_records) === 0) {
1049             return;
1050         }
1051         $bypassFilters = FALSE;
1052         foreach ($_records as $currentRecord) {
1053             $oldRecordArray = $currentRecord->toArray();
1054             unset($oldRecordArray['relations']);
1055             
1056             $data = array_merge($oldRecordArray, $_data);
1057             
1058             if($this->_newRelations || $this->_removeRelations) {
1059                 $data['relations'] = $this->_iterateRelations($currentRecord);
1060             }
1061             try {
1062                 $record = new $this->_modelName($data, $bypassFilters);
1063                 $updatedRecord = $this->update($record, FALSE);
1064                 
1065                 $this->_updateMultipleResult['results']->addRecord($updatedRecord);
1066                 $this->_updateMultipleResult['totalcount'] ++;
1067                 
1068             } catch (Tinebase_Exception_Record_Validation $e) {
1069                 if ($this->_updateMultipleValidateEachRecord === FALSE) {
1070                     throw $e;
1071                 }
1072                 $this->_updateMultipleResult['exceptions']->addRecord(new Tinebase_Model_UpdateMultipleException(array(
1073                     'id'         => $currentRecord->getId(),
1074                     'exception'  => $e,
1075                     'record'     => $currentRecord,
1076                     'code'       => $e->getCode(),
1077                     'message'    => $e->getMessage()
1078                 )));
1079                 $this->_updateMultipleResult['failcount'] ++;
1080             }
1081             if ($this->_updateMultipleValidateEachRecord === FALSE) {
1082                 // only validate the first record
1083                 $bypassFilters = TRUE;
1084             }
1085         }
1086     }
1087     
1088     /**
1089      * Deletes a set of records.
1090      *
1091      * If one of the records could not be deleted, no record is deleted
1092      *
1093      * @param   array $_ids array of record identifiers
1094      * @return  Tinebase_Record_RecordSet
1095      * @throws Tinebase_Exception_NotFound|Tinebase_Exception
1096      */
1097     public function delete($_ids)
1098     {
1099         if ($_ids instanceof $this->_modelName) {
1100             $_ids = (array)$_ids->getId();
1101         }
1102
1103         $ids = $this->_inspectDelete((array) $_ids);
1104
1105         $records = $this->_backend->getMultiple((array)$ids);
1106         if (count((array)$ids) != count($records)) {
1107             Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Only ' . count($records) . ' of ' . count((array)$ids) . ' records exist.');
1108         }
1109         
1110         if (empty($records)) {
1111             return $records;
1112         }
1113         
1114         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1115             . ' Deleting ' . count($records) . ' records ...');
1116
1117         try {
1118             $db = $this->_backend->getAdapter();
1119             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
1120             $this->_checkRight('delete');
1121
1122             foreach ($records as $record) {
1123                 $this->_deleteRecord($record);
1124             }
1125
1126             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1127
1128             // send notifications
1129             if ($this->sendNotifications()) {
1130                 foreach ($records as $record) {
1131                     $this->doSendNotifications($record, Tinebase_Core::getUser(), 'deleted');
1132                 }
1133             }
1134
1135         } catch (Exception $e) {
1136             Tinebase_TransactionManager::getInstance()->rollBack();
1137             Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' ' . print_r($e->getMessage(), true));
1138             throw $e;
1139         }
1140
1141         // returns deleted records
1142         return $records;
1143     }
1144
1145     /**
1146      * delete records by filter
1147      *
1148      * @param Tinebase_Model_Filter_FilterGroup $_filter
1149      * @return  Tinebase_Record_RecordSet
1150      */
1151     public function deleteByFilter(Tinebase_Model_Filter_FilterGroup $_filter)
1152     {
1153         $oldMaxExcecutionTime = ini_get('max_execution_time');
1154
1155         Tinebase_Core::setExecutionLifeTime(300); // 5 minutes
1156
1157         $ids = $this->search($_filter, NULL, FALSE, TRUE);
1158         $deletedRecords = $this->delete($ids);
1159         
1160         // reset max execution time to old value
1161         Tinebase_Core::setExecutionLifeTime($oldMaxExcecutionTime);
1162
1163         return $deletedRecords;
1164     }
1165
1166     /**
1167      * inspects delete action
1168      *
1169      * @param array $_ids
1170      * @return array of ids to actually delete
1171      */
1172     protected function _inspectDelete(array $_ids)
1173     {
1174         return $_ids;
1175     }
1176
1177     /**
1178      * move records to new container / folder / whatever
1179      *
1180      * @param mixed $_records (can be record set, filter, array, string)
1181      * @param mixed $_target (string, container record, ...)
1182      * @return array
1183      */
1184     public function move($_records, $_target, $_containerProperty = 'container_id')
1185     {
1186         $records = $this->_convertToRecordSet($_records);
1187         $targetContainerId = ($_target instanceof Tinebase_Model_Container) ? $_target->getId() : $_target;
1188
1189         if ($this->_doContainerACLChecks) {
1190             // check add grant in target container
1191             if (! Tinebase_Core::getUser()->hasGrant($targetContainerId, Tinebase_Model_Grants::GRANT_ADD)) {
1192                 Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Permission denied to add records to container.');
1193                 throw new Tinebase_Exception_AccessDenied('You are not allowed to move records to this container');
1194             }
1195
1196             // check delete grant in source container
1197             $containerIdsWithDeleteGrant = Tinebase_Container::getInstance()->getContainerByACL(Tinebase_Core::getUser(), $this->_applicationName, Tinebase_Model_Grants::GRANT_DELETE, TRUE);
1198             foreach ($records as $index => $record) {
1199                 if (! in_array($record->{$_containerProperty}, $containerIdsWithDeleteGrant)) {
1200                     Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
1201                         . ' Permission denied to remove record ' . $record->getId() . ' from container ' . $record->{$_containerProperty}
1202                     );
1203                     unset($records[$index]);
1204                 }
1205             }
1206         }
1207
1208         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Moving ' . count($records) . ' ' . $this->_modelName . '(s) to container ' . $targetContainerId);
1209
1210         // move (update container id)
1211         $idsToMove = $records->getArrayOfIds();
1212         $filterClass = $this->_modelName . 'Filter';
1213         if (! class_exists($filterClass)) {
1214             throw new Tinebase_Exception_NotFound('Filter class ' . $filterClass . ' not found!');
1215         }
1216         $filter = new $filterClass(array(
1217             array('field' => 'id', 'operator' => 'in', 'value' => $idsToMove)
1218         ));
1219         $updateResult = $this->updateMultiple($filter, array(
1220             $_containerProperty => $targetContainerId
1221         ));
1222         
1223         return $idsToMove;
1224     }
1225
1226     /*********** helper funcs **************/
1227
1228     /**
1229      * delete one record
1230      *
1231      * @param Tinebase_Record_Interface $_record
1232      * @throws Tinebase_Exception_AccessDenied
1233      */
1234     protected function _deleteRecord(Tinebase_Record_Interface $_record)
1235     {
1236         $this->_checkGrant($_record, 'delete');
1237
1238         $this->_deleteLinkedObjects($_record);
1239
1240         if (! $this->_purgeRecords && $_record->has('created_by')) {
1241             Tinebase_Timemachine_ModificationLog::setRecordMetaData($_record, 'delete', $_record);
1242             $this->_backend->update($_record);
1243         } else {
1244             $this->_backend->delete($_record);
1245         }
1246         
1247         $this->_increaseContainerContentSequence($_record, Tinebase_Model_ContainerContent::ACTION_DELETE);
1248     }
1249
1250     /**
1251      * delete linked objects (notes, relations, ...) of record
1252      *
1253      * @param Tinebase_Record_Interface $_record
1254      */
1255     protected function _deleteLinkedObjects(Tinebase_Record_Interface $_record)
1256     {
1257         // delete notes & relations
1258         if ($_record->has('notes')) {
1259             Tinebase_Notes::getInstance()->deleteNotesOfRecord($this->_modelName, $this->_getBackendType(), $_record->getId());
1260         }
1261         if ($_record->has('relations')) {
1262             $relations = Tinebase_Relations::getInstance()->getRelations($this->_modelName, $this->_getBackendType(), $_record->getId());
1263             if (!empty($relations)) {
1264                 // remove relations
1265                 Tinebase_Relations::getInstance()->setRelations($this->_modelName, $this->_getBackendType(), $_record->getId(), array());
1266
1267                 // remove related objects
1268                 if (!empty($this->_relatedObjectsToDelete)) {
1269                     foreach ($relations as $relation) {
1270                         if (in_array($relation->related_model, $this->_relatedObjectsToDelete)) {
1271                             list($appName, $i, $itemName) = explode('_', $relation->related_model);
1272                             $appController = Tinebase_Core::getApplicationInstance($appName, $itemName);
1273                             $appController->delete($relation->related_id);
1274                         }
1275                     }
1276                 }
1277             }
1278         }
1279         if ($_record->has('attachments')) {
1280             Tinebase_FileSystem_RecordAttachments::getInstance()->deleteRecordAttachments($_record);
1281         }
1282     }
1283
1284     /**
1285      * check grant for action (CRUD)
1286      *
1287      * @param Tinebase_Record_Interface $_record
1288      * @param string $_action
1289      * @param boolean $_throw
1290      * @param string $_errorMessage
1291      * @param Tinebase_Record_Interface $_oldRecord
1292      * @return boolean
1293      * @throws Tinebase_Exception_AccessDenied
1294      *
1295      * @todo use this function in other create + update functions
1296      * @todo invent concept for simple adding of grants (plugins?)
1297      */
1298     protected function _checkGrant($_record, $_action, $_throw = TRUE, $_errorMessage = 'No Permission.', $_oldRecord = NULL)
1299     {
1300         if (   ! $this->_doContainerACLChecks
1301             || ! $_record->has('container_id')) {
1302             return TRUE;
1303         }
1304         
1305         if (! is_object(Tinebase_Core::getUser())) {
1306             throw new Tinebase_Exception_AccessDenied('User object required to check grants');
1307         }
1308         
1309         // admin grant includes all others
1310         if (Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_ADMIN)) {
1311             return TRUE;
1312         }
1313         
1314         $hasGrant = FALSE;
1315         
1316         switch ($_action) {
1317             case 'get':
1318                 $hasGrant = Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_READ);
1319                 break;
1320             case 'create':
1321                 $hasGrant = Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_ADD);
1322                 break;
1323             case 'update':
1324                 $hasGrant = Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_EDIT);
1325                 break;
1326             case 'delete':
1327                 $container = Tinebase_Container::getInstance()->getContainerById($_record->container_id);
1328                 $hasGrant = Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_DELETE);
1329                 break;
1330         }
1331
1332         if (!$hasGrant) {
1333             if ($_throw) {
1334                 throw new Tinebase_Exception_AccessDenied($_errorMessage);
1335             } else {
1336                 Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' No permissions to ' . $_action . ' in container ' . $_record->container_id);
1337             }
1338         }
1339
1340         return $hasGrant;
1341     }
1342
1343     /**
1344      * overwrite this function to check rights
1345      *
1346      * @param string $_action {get|create|update|delete}
1347      * @return void
1348      * @throws Tinebase_Exception_AccessDenied
1349      */
1350     protected function _checkRight($_action)
1351     {
1352         return;
1353     }
1354
1355     /**
1356      * Removes containers where current user has no access to
1357      *
1358      * @param Tinebase_Model_Filter_FilterGroup $_filter
1359      * @param string $_action get|update
1360      */
1361     public function checkFilterACL(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
1362     {
1363         if (! $this->_doContainerACLChecks) {
1364             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
1365                 . ' Container ACL disabled for ' . $_filter->getModelName() . '.');
1366             return TRUE;
1367         }
1368
1369         $aclFilters = $_filter->getAclFilters();
1370
1371         if (! $aclFilters) {
1372             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
1373                 . ' Force a standard containerFilter (specialNode = all) as ACL filter.');
1374             
1375             $containerFilter = $_filter->createFilter('container_id', 'specialNode', 'all', array('applicationName' => $_filter->getApplicationName()));
1376             $_filter->addFilter($containerFilter);
1377         }
1378
1379         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1380             . ' Setting filter grants for action ' . $_action);
1381         switch ($_action) {
1382             case 'get':
1383                 $_filter->setRequiredGrants(array(
1384                     Tinebase_Model_Grants::GRANT_READ,
1385                     Tinebase_Model_Grants::GRANT_ADMIN,
1386                 ));
1387                 break;
1388             case 'update':
1389                 $_filter->setRequiredGrants(array(
1390                     Tinebase_Model_Grants::GRANT_EDIT,
1391                     Tinebase_Model_Grants::GRANT_ADMIN,
1392                 ));
1393                 break;
1394             case 'export':
1395                 $_filter->setRequiredGrants(array(
1396                     Tinebase_Model_Grants::GRANT_EXPORT,
1397                     Tinebase_Model_Grants::GRANT_ADMIN,
1398                 ));
1399                 break;
1400             case 'sync':
1401                 $_filter->setRequiredGrants(array(
1402                     Tinebase_Model_Grants::GRANT_SYNC,
1403                     Tinebase_Model_Grants::GRANT_ADMIN,
1404                 ));
1405                 break;
1406             default:
1407                 throw new Tinebase_Exception_UnexpectedValue('Unknown action: ' . $_action);
1408         }
1409     }
1410
1411     /**
1412      * saves alarms of given record
1413      *
1414      * @param Tinebase_Record_Abstract $_record
1415      * @return void
1416      */
1417     protected function _saveAlarms(Tinebase_Record_Abstract $_record)
1418     {
1419         if (! $_record->alarms instanceof Tinebase_Record_RecordSet) {
1420             $_record->alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
1421         }
1422         $alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
1423
1424         // create / update alarms
1425         foreach ($_record->alarms as $alarm) {
1426             try {
1427                 $this->_inspectAlarmSet($_record, $alarm);
1428                 $alarms->addRecord($alarm);
1429             } catch (Tinebase_Exception_InvalidArgument $teia) {
1430                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' ' . $teia->getMessage());
1431             }
1432         }
1433
1434         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1435             . " About to save " . count($alarms) . " alarms for {$_record->id} ");
1436         $_record->alarms = $alarms;
1437
1438         Tinebase_Alarm::getInstance()->setAlarmsOfRecord($_record);
1439     }
1440
1441     /**
1442      * inspect alarm and set time
1443      *
1444      * @param Tinebase_Record_Abstract $_record
1445      * @param Tinebase_Model_Alarm $_alarm
1446      * @return void
1447      * @throws Tinebase_Exception_InvalidArgument
1448      */
1449     protected function _inspectAlarmSet(Tinebase_Record_Abstract $_record, Tinebase_Model_Alarm $_alarm)
1450     {
1451         if (! $_record->{$this->_recordAlarmField} instanceof DateTime) {
1452             throw new Tinebase_Exception_InvalidArgument('alarm reference time is not set');
1453         }
1454
1455         $_alarm->setTime($_record->{$this->_recordAlarmField});
1456     }
1457
1458     /**
1459      * get and resolve all alarms of given record(s)
1460      *
1461      * @param  Tinebase_Record_Interface|Tinebase_Record_RecordSet $_record
1462      */
1463     public function getAlarms($_record)
1464     {
1465         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " Resolving alarms and add them to record set.");
1466         
1467         $alarms = Tinebase_Alarm::getInstance()->getAlarmsOfRecord($this->_modelName, $_record);
1468         
1469         $records = $_record instanceof Tinebase_Record_RecordSet ? $_record : new Tinebase_Record_RecordSet($this->_modelName, array($_record));
1470
1471         foreach ($records as $record) {
1472             if (count($alarms) === 0) {
1473                 $record->alarms = $alarms;
1474                 continue;
1475             }
1476
1477             $record->alarms = $alarms->filter('record_id', $record->getId());
1478             // calc minutes_before
1479             if ($record->has($this->_recordAlarmField) && $record->{$this->_recordAlarmField} instanceof DateTime) {
1480                 $this->_inspectAlarmGet($record);
1481             }
1482         }
1483     }
1484
1485     /**
1486      * inspect alarms of record (all alarms minutes_before fields are set here by default)
1487      *
1488      * @param Tinebase_Record_Abstract $_record
1489      * @return void
1490      */
1491     protected function _inspectAlarmGet(Tinebase_Record_Abstract $_record)
1492     {
1493         $_record->alarms->setMinutesBefore($_record->{$this->_recordAlarmField});
1494     }
1495
1496     /**
1497      * delete alarms for records
1498      *
1499      * @param array $_recordIds
1500      */
1501     protected function _deleteAlarmsForIds($_recordIds)
1502     {
1503         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1504             . " Deleting alarms for records " . print_r($_recordIds, TRUE)
1505         );
1506
1507         Tinebase_Alarm::getInstance()->deleteAlarmsOfRecord($this->_modelName, $_recordIds);
1508     }
1509
1510     /**
1511      * enable / disable right checks
1512      *
1513      * @param boolean $_value
1514      * @return void
1515      */
1516     protected function _setRightChecks($_value)
1517     {
1518         $this->_doRightChecks = (bool) $_value;
1519     }
1520
1521     /**
1522      * convert input to recordset
1523      *
1524      * input can have the following datatypes:
1525      * - Tinebase_Model_Filter_FilterGroup
1526      * - Tinebase_Record_RecordSet
1527      * - Tinebase_Record_Abstract
1528      * - string (single id)
1529      * - array (multiple ids)
1530      *
1531      * @param mixed $_mixed
1532      * @param boolean $_refresh if this is TRUE, refresh the recordset by calling getMultiple
1533      * @param Tinebase_Model_Pagination $_pagination (only valid if $_mixed instanceof Tinebase_Model_Filter_FilterGroup)
1534      * @return Tinebase_Record_RecordSet
1535      */
1536     protected function _convertToRecordSet($_mixed, $_refresh = FALSE, Tinebase_Model_Pagination $_pagination = NULL)
1537     {
1538         if ($_mixed instanceof Tinebase_Model_Filter_FilterGroup) {
1539             // FILTER (Tinebase_Model_Filter_FilterGroup)
1540             $result = $this->search($_mixed, $_pagination);
1541         } elseif ($_mixed instanceof Tinebase_Record_RecordSet) {
1542             // RECORDSET (Tinebase_Record_RecordSet)
1543             $result = ($_refresh) ? $this->_backend->getMultiple($_mixed->getArrayOfIds()) : $_mixed;
1544         } elseif ($_mixed instanceof Tinebase_Record_Abstract) {
1545             // RECORD (Tinebase_Record_Abstract)
1546             if ($_refresh) {
1547                 $result = $this->_backend->getMultiple($_mixed->getId());
1548             } else {
1549                 $result = new Tinebase_Record_RecordSet(get_class($_mixed), array($_mixed));
1550             }
1551         } elseif (is_string($_mixed) || is_array($_mixed)) {
1552             // SINGLE ID or ARRAY OF IDS
1553             $result = $this->_backend->getMultiple($_mixed);
1554         } else {
1555             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1556                 . ' Could not convert input param to RecordSet: Unsupported type: ' . gettype($_mixed));
1557             $result = new Tinebase_Record_RecordSet($this->_modelName);
1558         }
1559
1560         return $result;
1561     }
1562     
1563
1564     /**
1565      * creates dependent records after creating the parent record
1566      *
1567      * @param Tinebase_Record_Interface $_createdRecord
1568      * @param Tinebase_Record_Interface $_record
1569      * @param string $_property
1570      * @param array $_fieldConfig
1571      */
1572     protected function _createDependentRecords(Tinebase_Record_Interface $_createdRecord, Tinebase_Record_Interface $_record, $_property, $_fieldConfig)
1573     {
1574         if (! array_key_exists('dependentRecords', $_fieldConfig) || ! $_fieldConfig['dependentRecords']) {
1575             return;
1576         }
1577         
1578         if ($_record->has($_property) && $_record->{$_property}) {
1579             $recordClassName = $_fieldConfig['recordClassName'];
1580             $new = new Tinebase_Record_RecordSet($recordClassName);
1581             $ccn = $_fieldConfig['controllerClassName'];
1582             $controller = $ccn::getInstance();
1583     
1584             // legacy - should be already done in frontent json - remove if all record properties are record sets before getting to controller
1585             if (is_array($_record->{$_property})) {
1586                 $rs = new Tinebase_Record_RecordSet($recordClassName);
1587                 foreach ($_record->{$_property} as $recordArray) {
1588                     $rec = new $recordClassName(array(),true);
1589                     $rec->setFromJsonInUsersTimezone($recordArray);
1590                     $rs->addRecord($rec);
1591                 }
1592                 $_record->{$_property} = $rs;
1593             }
1594             // legacy end
1595             
1596             foreach ($_record->{$_property} as $record) {
1597                 $record->{$_fieldConfig['refIdField']} = $_createdRecord->getId();
1598                 $new->add($controller->create($record));
1599             }
1600     
1601             $_createdRecord->{$_property} = $new->toArray();
1602         }
1603     }
1604     
1605     /**
1606      * updates dependent records on update the parent record
1607      *
1608      * @param Tinebase_Record_Interface $_record
1609      * @param Tinebase_Record_Interface $_oldRecord
1610      * @param string $_property
1611      * @param array $_fieldConfig
1612      */
1613     protected function _updateDependentRecords(Tinebase_Record_Interface $_record, Tinebase_Record_Interface $_oldRecord, $_property, $_fieldConfig)
1614     {
1615         if (! array_key_exists('dependentRecords', $_fieldConfig) || ! $_fieldConfig['dependentRecords']) {
1616             return;
1617         }
1618     
1619         if ($_record->has($_property)) {
1620     
1621             $ccn = $_fieldConfig['controllerClassName'];
1622             $controller = $ccn::getInstance();
1623             $recordClassName = $_fieldConfig['recordClassName'];
1624             $filterClassName = $_fieldConfig['filterClassName'];
1625             $existing = new Tinebase_Record_RecordSet($recordClassName);
1626     
1627             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
1628                 Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_record->{$_property}, TRUE));
1629             }
1630             
1631             if (! empty($_record->{$_property}) && $_record->{$_property}) {
1632                 
1633                 // legacy - should be already done in frontent json - remove if all record properties are record sets before getting to controller
1634                 if (is_array($_record->{$_property})) {
1635                     $rs = new Tinebase_Record_RecordSet($recordClassName);
1636                     foreach ($_record->{$_property} as $recordArray) {
1637                         $rec = new $recordClassName(array(),true);
1638                         $rec->setFromJsonInUsersTimezone($recordArray);
1639                         $rs->addRecord($rec);
1640                     }
1641                     $_record->{$_property} = $rs;
1642                 }
1643                 // legacy end
1644                 
1645                 foreach ($_record->{$_property} as $record) {
1646                     $record->{$_fieldConfig['refIdField']} = $_oldRecord->getId();
1647                     // update record if ID exists and has a length of 40
1648                     if ($record->id && strlen($record->id) == 40) {
1649                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
1650                             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . 'Updating contract ' . $record->id);
1651                         }
1652                         $updatedRecord = $controller->update($record);
1653                         $existing->addRecord($updatedRecord);
1654                         // create if ID does not exist or has not a length of 40
1655                     } else {
1656                         $record->id = NULL;
1657                         $existing->addRecord($controller->create($record));
1658                     }
1659                 }
1660             }
1661     
1662             $filter = new $filterClassName(isset($_fieldConfig['addFilters']) ? $_fieldConfig['addFilters'] : array(), 'AND');
1663             $filter->addFilter(new Tinebase_Model_Filter_Text($_fieldConfig['refIdField'], 'equals', $_record->getId()));
1664             $filter->addFilter(new Tinebase_Model_Filter_Id('id', 'notin', $existing->id));
1665     
1666             $deleteContracts = $controller->search($filter);
1667     
1668             $controller->delete($deleteContracts->id);
1669             $_record->{$_property} = $existing->toArray();
1670         }
1671     }
1672 }