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