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