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