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