bddfb1c17fdd151b47b87706d2b390a5f655c73a
[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             $this->_deleteLinkedRelations($_record);
1587         }
1588
1589         if ($_record->has('attachments') && Tinebase_Core::isFilesystemAvailable()) {
1590             Tinebase_FileSystem_RecordAttachments::getInstance()->deleteRecordAttachments($_record);
1591         }
1592     }
1593     
1594     /**
1595      * delete linked relations
1596      * 
1597      * @param Tinebase_Record_Interface $_record
1598      * 
1599      * TODO check if this needs to be done, as we might already deleting this "from the other side"
1600      */
1601     protected function _deleteLinkedRelations(Tinebase_Record_Interface $_record)
1602     {
1603         $relations = Tinebase_Relations::getInstance()->getRelations($this->_modelName, $this->_getBackendType(), $_record->getId());
1604         if (empty($relations)) {
1605             return;
1606         }
1607
1608         // remove relations
1609         Tinebase_Relations::getInstance()->setRelations($this->_modelName, $this->_getBackendType(), $_record->getId(), array());
1610
1611         if (empty($this->_relatedObjectsToDelete)) {
1612             return;
1613         }
1614         
1615         // remove related objects
1616         Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Deleting all '
1617                 . implode(',', $this->_relatedObjectsToDelete) . ' relations.');
1618
1619         foreach ($relations as $relation) {
1620             if (in_array($relation->related_model, $this->_relatedObjectsToDelete)) {
1621                 list($appName, $i, $itemName) = explode('_', $relation->related_model);
1622                 $appController = Tinebase_Core::getApplicationInstance($appName, $itemName);
1623
1624                 try {
1625                     $appController->delete($relation->related_id);
1626                 } catch (Exception $e) {
1627                     Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Error deleting: ' . $e->getMessage());
1628                 }
1629             }
1630         }
1631     }
1632
1633     /**
1634      * check grant for action (CRUD)
1635      *
1636      * @param Tinebase_Record_Interface $_record
1637      * @param string $_action
1638      * @param boolean $_throw
1639      * @param string $_errorMessage
1640      * @param Tinebase_Record_Interface $_oldRecord
1641      * @return boolean
1642      * @throws Tinebase_Exception_AccessDenied
1643      *
1644      * @todo use this function in other create + update functions
1645      * @todo invent concept for simple adding of grants (plugins?)
1646      */
1647     protected function _checkGrant($_record, $_action, $_throw = TRUE, $_errorMessage = 'No Permission.', $_oldRecord = NULL)
1648     {
1649         if (   ! $this->_doContainerACLChecks
1650             || ! $_record->has('container_id')) {
1651             return TRUE;
1652         }
1653         
1654         if (! is_object(Tinebase_Core::getUser())) {
1655             throw new Tinebase_Exception_AccessDenied('User object required to check grants');
1656         }
1657         
1658         // admin grant includes all others
1659         if (Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_ADMIN)) {
1660             return TRUE;
1661         }
1662         
1663         $hasGrant = FALSE;
1664         
1665         switch ($_action) {
1666             case 'get':
1667                 $hasGrant = Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_READ);
1668                 break;
1669             case 'create':
1670                 $hasGrant = Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_ADD);
1671                 break;
1672             case 'update':
1673                 $hasGrant = Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_EDIT);
1674                 break;
1675             case 'delete':
1676                 $container = Tinebase_Container::getInstance()->getContainerById($_record->container_id);
1677                 $hasGrant = Tinebase_Core::getUser()->hasGrant($_record->container_id, Tinebase_Model_Grants::GRANT_DELETE);
1678                 break;
1679         }
1680
1681         if (! $hasGrant) {
1682             Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' No permissions to ' . $_action . ' in container ' . $_record->container_id);
1683             if ($_throw) {
1684                 throw new Tinebase_Exception_AccessDenied($_errorMessage);
1685             }
1686         }
1687         
1688         return $hasGrant;
1689     }
1690
1691     /**
1692      * overwrite this function to check rights
1693      *
1694      * @param string $_action {get|create|update|delete}
1695      * @return void
1696      * @throws Tinebase_Exception_AccessDenied
1697      */
1698     protected function _checkRight($_action)
1699     {
1700         return;
1701     }
1702
1703     /**
1704      * Removes containers where current user has no access to
1705      *
1706      * @param Tinebase_Model_Filter_FilterGroup $_filter
1707      * @param string $_action get|update
1708      */
1709     public function checkFilterACL(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
1710     {
1711         if (! $this->_doContainerACLChecks) {
1712             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
1713                 . ' Container ACL disabled for ' . $_filter->getModelName() . '.');
1714             return TRUE;
1715         }
1716
1717         $aclFilters = $_filter->getAclFilters();
1718
1719         if (! $aclFilters) {
1720             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1721                 . ' Force a standard containerFilter (specialNode = all) as ACL filter.');
1722             
1723             $containerFilter = $_filter->createFilter('container_id', 'specialNode', 'all', array('applicationName' => $_filter->getApplicationName()));
1724             $_filter->addFilter($containerFilter);
1725         }
1726
1727         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
1728             . ' Setting filter grants for action ' . $_action);
1729         switch ($_action) {
1730             case 'get':
1731                 $_filter->setRequiredGrants(array(
1732                     Tinebase_Model_Grants::GRANT_READ,
1733                     Tinebase_Model_Grants::GRANT_ADMIN,
1734                 ));
1735                 break;
1736             case 'update':
1737                 $_filter->setRequiredGrants(array(
1738                     Tinebase_Model_Grants::GRANT_EDIT,
1739                     Tinebase_Model_Grants::GRANT_ADMIN,
1740                 ));
1741                 break;
1742             case 'export':
1743                 $_filter->setRequiredGrants(array(
1744                     Tinebase_Model_Grants::GRANT_EXPORT,
1745                     Tinebase_Model_Grants::GRANT_ADMIN,
1746                 ));
1747                 break;
1748             case 'sync':
1749                 $_filter->setRequiredGrants(array(
1750                     Tinebase_Model_Grants::GRANT_SYNC,
1751                     Tinebase_Model_Grants::GRANT_ADMIN,
1752                 ));
1753                 break;
1754             default:
1755                 throw new Tinebase_Exception_UnexpectedValue('Unknown action: ' . $_action);
1756         }
1757     }
1758
1759     /**
1760      * saves alarms of given record
1761      *
1762      * @param Tinebase_Record_Abstract $_record
1763      * @return void
1764      */
1765     protected function _saveAlarms(Tinebase_Record_Abstract $_record)
1766     {
1767         if (! $_record->alarms instanceof Tinebase_Record_RecordSet) {
1768             $_record->alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
1769         }
1770         $alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
1771
1772         // create / update alarms
1773         foreach ($_record->alarms as $alarm) {
1774             try {
1775                 $this->_inspectAlarmSet($_record, $alarm);
1776                 $alarms->addRecord($alarm);
1777             } catch (Tinebase_Exception_InvalidArgument $teia) {
1778                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' ' . $teia->getMessage());
1779             }
1780         }
1781
1782         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1783             . " About to save " . count($alarms) . " alarms for {$_record->id} ");
1784         $_record->alarms = $alarms;
1785
1786         Tinebase_Alarm::getInstance()->setAlarmsOfRecord($_record);
1787     }
1788
1789     /**
1790      * inspect alarm and set time
1791      *
1792      * @param Tinebase_Record_Abstract $_record
1793      * @param Tinebase_Model_Alarm $_alarm
1794      * @return void
1795      * @throws Tinebase_Exception_InvalidArgument
1796      */
1797     protected function _inspectAlarmSet(Tinebase_Record_Abstract $_record, Tinebase_Model_Alarm $_alarm)
1798     {
1799         if (! $_record->{$this->_recordAlarmField} instanceof DateTime) {
1800             throw new Tinebase_Exception_InvalidArgument('alarm reference time is not set');
1801         }
1802
1803         $_alarm->setTime($_record->{$this->_recordAlarmField});
1804     }
1805
1806     /**
1807      * get and resolve all alarms of given record(s)
1808      *
1809      * @param  Tinebase_Record_Interface|Tinebase_Record_RecordSet $_record
1810      */
1811     public function getAlarms($_record)
1812     {
1813         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " Resolving alarms and add them to record set.");
1814         
1815         $records = $_record instanceof Tinebase_Record_RecordSet ? $_record : new Tinebase_Record_RecordSet($this->_modelName, array($_record));
1816
1817         $alarms = Tinebase_Alarm::getInstance()->getAlarmsOfRecord($this->_modelName, $records);
1818         
1819         foreach ($alarms as $alarm) {
1820             $record = $records->getById($alarm->record_id);
1821             
1822             if (!isset($record->alarms)) {
1823                 $record->alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
1824             }
1825             
1826             if (!$record->alarms->getById($alarm->getId())) {
1827                 $record->alarms->addRecord($alarm);
1828             }
1829         }
1830         
1831         foreach ($records as $record) {
1832             if (!isset($record->alarms)) {
1833                 $record->alarms = new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
1834             } else {
1835                 // calc minutes_before
1836                 if ($record->has($this->_recordAlarmField) && $record->{$this->_recordAlarmField} instanceof DateTime) {
1837                     $this->_inspectAlarmGet($record);
1838                 }
1839             }
1840         }
1841     }
1842
1843     /**
1844      * inspect alarms of record (all alarms minutes_before fields are set here by default)
1845      *
1846      * @param Tinebase_Record_Abstract $_record
1847      * @return void
1848      */
1849     protected function _inspectAlarmGet(Tinebase_Record_Abstract $_record)
1850     {
1851         $_record->alarms->setMinutesBefore($_record->{$this->_recordAlarmField});
1852     }
1853
1854     /**
1855      * delete alarms for records
1856      *
1857      * @param array $_recordIds
1858      */
1859     protected function _deleteAlarmsForIds($_recordIds)
1860     {
1861         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
1862             . " Deleting alarms for records " . print_r($_recordIds, TRUE)
1863         );
1864
1865         Tinebase_Alarm::getInstance()->deleteAlarmsOfRecord($this->_modelName, $_recordIds);
1866     }
1867
1868     /**
1869      * convert input to recordset
1870      *
1871      * input can have the following datatypes:
1872      * - Tinebase_Model_Filter_FilterGroup
1873      * - Tinebase_Record_RecordSet
1874      * - Tinebase_Record_Abstract
1875      * - string (single id)
1876      * - array (multiple ids)
1877      *
1878      * @param mixed $_mixed
1879      * @param boolean $_refresh if this is TRUE, refresh the recordset by calling getMultiple
1880      * @param Tinebase_Model_Pagination $_pagination (only valid if $_mixed instanceof Tinebase_Model_Filter_FilterGroup)
1881      * @return Tinebase_Record_RecordSet
1882      */
1883     protected function _convertToRecordSet($_mixed, $_refresh = FALSE, Tinebase_Model_Pagination $_pagination = NULL)
1884     {
1885         if ($_mixed instanceof Tinebase_Model_Filter_FilterGroup) {
1886             // FILTER (Tinebase_Model_Filter_FilterGroup)
1887             $result = $this->search($_mixed, $_pagination);
1888         } elseif ($_mixed instanceof Tinebase_Record_RecordSet) {
1889             // RECORDSET (Tinebase_Record_RecordSet)
1890             $result = ($_refresh) ? $this->_backend->getMultiple($_mixed->getArrayOfIds()) : $_mixed;
1891         } elseif ($_mixed instanceof Tinebase_Record_Abstract) {
1892             // RECORD (Tinebase_Record_Abstract)
1893             if ($_refresh) {
1894                 $result = $this->_backend->getMultiple($_mixed->getId());
1895             } else {
1896                 $result = new Tinebase_Record_RecordSet(get_class($_mixed), array($_mixed));
1897             }
1898         } elseif (is_string($_mixed) || is_array($_mixed)) {
1899             // SINGLE ID or ARRAY OF IDS
1900             $result = $this->_backend->getMultiple($_mixed);
1901         } else {
1902             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1903                 . ' Could not convert input param to RecordSet: Unsupported type: ' . gettype($_mixed));
1904             $result = new Tinebase_Record_RecordSet($this->_modelName);
1905         }
1906
1907         return $result;
1908     }
1909     
1910
1911     /**
1912      * creates dependent records after creating the parent record
1913      *
1914      * @param Tinebase_Record_Interface $_createdRecord
1915      * @param Tinebase_Record_Interface $_record
1916      * @param string $_property
1917      * @param array $_fieldConfig
1918      */
1919     protected function _createDependentRecords(Tinebase_Record_Interface $_createdRecord, Tinebase_Record_Interface $_record, $_property, $_fieldConfig)
1920     {
1921         if (! (isset($_fieldConfig['dependentRecords']) || array_key_exists('dependentRecords', $_fieldConfig)) || ! $_fieldConfig['dependentRecords']) {
1922             return;
1923         }
1924         
1925         if ($_record->has($_property) && $_record->{$_property}) {
1926             $recordClassName = $_fieldConfig['recordClassName'];
1927             $new = new Tinebase_Record_RecordSet($recordClassName);
1928             $ccn = $_fieldConfig['controllerClassName'];
1929             $controller = $ccn::getInstance();
1930     
1931             // legacy - should be already done in frontend json - remove if all record properties are record sets before getting to controller
1932             if (is_array($_record->{$_property})) {
1933                 $rs = new Tinebase_Record_RecordSet($recordClassName);
1934                 foreach ($_record->{$_property} as $recordArray) {
1935                     $rec = new $recordClassName(array(),true);
1936                     $rec->setFromJsonInUsersTimezone($recordArray);
1937                     
1938                     if (strlen($rec->getId()) < 40) {
1939                         $rec->{$rec->getIdProperty()} = Tinebase_Record_Abstract::generateUID();
1940                     }
1941                     
1942                     $rs->addRecord($rec);
1943                 }
1944                 $_record->{$_property} = $rs;
1945             } else {
1946                 foreach ($_record->{$_property} as $rec) {
1947                     if (strlen($rec->getId()) < 40) {
1948                         $rec->{$rec->getIdProperty()} = Tinebase_Record_Abstract::generateUID();
1949                     }
1950                 }
1951             }
1952             // legacy end
1953
1954             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
1955                 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__. ' Creating ' . $_record->{$_property}->count() . ' dependent records on property ' . $_property . ' for ' . $this->_applicationName . ' ' . $this->_modelName);
1956             }
1957             
1958             foreach ($_record->{$_property} as $record) {
1959                 $record->{$_fieldConfig['refIdField']} = $_createdRecord->getId();
1960                 if (! $record->getId() || strlen($record->getId()) != 40) {
1961                     $record->{$record->getIdProperty()} = NULL;
1962                 }
1963                 $new->addRecord($controller->create($record));
1964             }
1965     
1966             $_createdRecord->{$_property} = $new->toArray();
1967         }
1968     }
1969     
1970     /**
1971      * updates dependent records on update the parent record
1972      *
1973      * @param Tinebase_Record_Interface $_record
1974      * @param Tinebase_Record_Interface $_oldRecord
1975      * @param string $_property
1976      * @param array $_fieldConfig
1977      */
1978     protected function _updateDependentRecords(Tinebase_Record_Interface $_record, Tinebase_Record_Interface $_oldRecord, $_property, $_fieldConfig)
1979     {
1980         if (! (isset($_fieldConfig['dependentRecords']) || array_key_exists('dependentRecords', $_fieldConfig)) || ! $_fieldConfig['dependentRecords']) {
1981             return;
1982         }
1983         
1984         if (! isset ($_fieldConfig['refIdField'])) {
1985             throw new Tinebase_Exception_Record_DefinitionFailure('If a record is dependent, a refIdField has to be defined!');
1986         }
1987         
1988         // don't handle dependent records on property if it is set to null or doesn't exist.
1989         if (($_record->{$_property} === NULL) || (! $_record->has($_property))) {
1990             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
1991                 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__. ' Skip updating dependent records (got NULL) on property ' . $_property . ' for ' . $this->_applicationName . ' ' . $this->_modelName . ' with id = "' . $_record->getId() . '"');
1992             }
1993             return;
1994         }
1995         
1996         $ccn = $_fieldConfig['controllerClassName'];
1997         $controller = $ccn::getInstance();
1998         $recordClassName = $_fieldConfig['recordClassName'];
1999         $filterClassName = $_fieldConfig['filterClassName'];
2000         $existing = new Tinebase_Record_RecordSet($recordClassName);
2001         
2002         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
2003             . ' ' . print_r($_record->{$_property}, TRUE));
2004         
2005         if (! empty($_record->{$_property}) && $_record->{$_property}) {
2006             
2007             // legacy - should be already done in frontend json - remove if all record properties are record sets before getting to controller
2008             if (is_array($_record->{$_property})) {
2009                 $rs = new Tinebase_Record_RecordSet($recordClassName);
2010                 foreach ($_record->{$_property} as $recordArray) {
2011                     $rec = new $recordClassName(array(),true);
2012                     $rec->setFromJsonInUsersTimezone($recordArray);
2013                     $rs->addRecord($rec);
2014                 }
2015                 $_record->{$_property} = $rs;
2016             }
2017             
2018             $idProperty = $_record->{$_property}->getFirstRecord()->getIdProperty();
2019             
2020             // legacy end
2021             $oldFilter = new $filterClassName(array(array('field' => $idProperty, 'operator' => 'in', 'value' => $_record->{$_property}->getId())));
2022             $oldRecords = $controller->search($oldFilter);
2023             
2024             foreach ($_record->{$_property} as $record) {
2025                 
2026                 $record->{$_fieldConfig['refIdField']} = $_record->getId();
2027                 
2028                 // update record if ID exists and has a length of 40 (it has a length of 10 if it is a timestamp)
2029                 if ($record->getId() && strlen($record->getId()) == 40) {
2030                     
2031                     // do not try to update if the record hasn't changed
2032                     $oldRecord = $oldRecords->getById($record->getId());
2033                     
2034                     if ($oldRecord && ! empty($oldRecord->diff($record)->diff)) {
2035                         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
2036                             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__. ' Updating dependent record with id = "' . $record->getId() . '" on property ' . $_property . ' for ' . $this->_applicationName . ' ' . $this->_modelName);
2037                         }
2038                         $existing->addRecord($controller->update($record));
2039                     } else {
2040                         $existing->addRecord($record);
2041                     }
2042                     // create if is not existing already
2043                 } else {
2044                     // try to find if it already exists (with corrupted id)
2045                     if ($record->getId() == NULL) {
2046                         $crc = $controller->create($record);
2047                         $existing->addRecord($crc);
2048                         
2049                         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
2050                             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__. ' Creating dependent record with id = "' . $crc->getId() . '" on property ' . $_property . ' for ' . $this->_applicationName . ' ' . $this->_modelName);
2051                         }
2052                     } else {
2053                         
2054                         try {
2055                             
2056                             $prevRecord = $controller->get($record->getId());
2057     
2058                             if (! empty($prevRecord->diff($record)->diff)) {
2059                                 $existing->addRecord($controller->update($record));
2060                             } else {
2061                                 $existing->addRecord($record);
2062                             }
2063                             
2064                         } catch (Tinebase_Exception_NotFound $e) {
2065                             $record->id = NULL;
2066                             $crc = $controller->create($record);
2067                             $existing->addRecord($crc);
2068                             
2069                             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
2070                                 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__. ' Creating dependent record with id = "' . $crc->getId() . '" on property ' . $_property . ' for ' . $this->_applicationName . ' ' . $this->_modelName);
2071                             }
2072                         }
2073                     }
2074                 }
2075             }
2076         }
2077
2078         $filter = new $filterClassName(isset($_fieldConfig['addFilters']) ? $_fieldConfig['addFilters'] : array(), 'AND');
2079         $filter->addFilter(new Tinebase_Model_Filter_Text($_fieldConfig['refIdField'], 'equals', $_record->getId()));
2080         
2081         // an empty array will remove all records on this property
2082         if (! empty($_record->{$_property})) {
2083             $filter->addFilter(new Tinebase_Model_Filter_Id('id', 'notin', $existing->getId()));
2084         }
2085
2086         $deleteIds = $controller->search($filter, NULL, FALSE, TRUE);
2087         
2088         if (! empty($deleteIds)) {
2089             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
2090                 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__. ' Deleting dependent records with id = "' . print_r($deleteIds, 1) . '" on property ' . $_property . ' for ' . $this->_applicationName . ' ' . $this->_modelName);
2091             }
2092             $controller->delete($deleteIds);
2093         }
2094         $_record->{$_property} = $existing->toArray();
2095     }
2096     
2097     protected function _createDependentRecord($record, $_record, $_fieldConfig, $controller, $recordSet)
2098     {
2099         
2100     }
2101 }