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