6aaac622b492eea030c65324ac6c2eb4bda6fbe8
[tine20] / tine20 / Tinebase / Timemachine / ModificationLog.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Tinebase
6  * @subpackage  Timemachine 
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Cornelius Weiss <c.weiss@metaways.de>
9  * @copyright   Copyright (c) 2007-2017 Metaways Infosystems GmbH (http://www.metaways.de)
10  *
11  */
12
13 /**
14  * ModificationLog tracks and supplies the logging of modifications on a field 
15  * basis of records. It's an generic approach which could be usesed by any 
16  * application. Besides, providing a logbook, the real power of ModificationLog 
17  * depends the combination with the Timemachine.
18  * 
19  * ModificationLog logges differences of complete fields. This is in contrast to
20  * changetracking of other products which have sub field resolution. As in
21  * general, the sub field approach offers most felxibility, the complete field 
22  * solution is an adequate compromise for usage and performace.
23  * 
24  * ModificationLog is used by Tinebase_Timemachine_Abstract. If an application
25  * backened extends Tinebase_Timemachine_Abstract, it MUST use 
26  * Tinebase_Timemachine_ModificationLog to track modifications
27  * 
28  * NOTE: Maximum time resolution is one second. If there are more than one
29  * modifications in a second, they are distinguished by the accounts which made
30  * the modifications and a autoincement key of the underlaying database table.
31  * NOTE: Timespans are allways defined, with the beginning point excluded and
32  * the end point included. Mathematical: (_from, _until]
33  * 
34  * @package Tinebase
35  * @subpackage Timemachine
36  * 
37  * @todo Add registry for logbook starttime and methods to throw away logbook 
38  *       entries. Throw exceptions when times are requested which are not in the 
39  *       log anymore!
40  * @todo refactor this to use generic sql backend + remove Tinebase_Db_Table usage
41  */
42 class Tinebase_Timemachine_ModificationLog implements Tinebase_Controller_Interface
43 {
44     const CREATED = 'created';
45     const DELETED = 'deleted';
46     const UPDATED = 'updated';
47
48     /**
49      * Tablename SQL_TABLE_PREFIX . timemachine_modificationlog
50      *
51      * @var string
52      */
53     protected $_tablename = 'timemachine_modlog';
54     
55     /**
56      * Holds table instance for timemachine_history table
57      *
58      * @var Tinebase_Db_Table
59      */
60     protected $_table = NULL;
61     
62     /**
63      * holds names of meta properties in record
64      * 
65      * @var array
66      * 
67      * @see 0007494: add changes in notes to modlog/history
68      */
69     protected $_metaProperties = array(
70         'created_by',
71         'creation_time',
72         'last_modified_by',
73         'last_modified_time',
74         //do NOT add is_deleted!
75         //'is_deleted',
76         'deleted_time',
77         'deleted_by',
78         'seq',
79     );
80     
81     /**
82      * the sql backend
83      * 
84      * @var Tinebase_Backend_Sql
85      */
86     protected $_backend;
87     
88     /**
89      * holds the instance of the singleton
90      *
91      * @var Tinebase_Timemachine_ModificationLog
92      */
93     private static $instance = NULL;
94
95     /**
96      * holds the applicationId of the current context temporarily.
97      *
98      * @var string
99      */
100     protected $_applicationId = NULL;
101
102     /**
103      * if set, all newly created modlogs will have this external instance id. this is used during applying replication logs
104      *
105      * @var string
106      */
107     protected $_externalInstanceId = NULL;
108     
109     /**
110      * the singleton pattern
111      *
112      * @return Tinebase_Timemachine_ModificationLog
113      */
114     public static function getInstance() 
115     {
116         if (self::$instance === NULL) {
117             self::$instance = new Tinebase_Timemachine_ModificationLog();
118         }
119         
120         return self::$instance;
121     }
122     
123     /**
124      * the constructor
125      *
126      */
127     private function __construct()
128     {
129         $this->_tablename = SQL_TABLE_PREFIX . $this->_tablename;
130         
131         $this->_table = new Tinebase_Db_Table(array('name' => $this->_tablename));
132         $this->_table->setRowClass('Tinebase_Model_ModificationLog');
133         
134         $this->_backend = new Tinebase_Backend_Sql(array(
135             'modelName' => 'Tinebase_Model_ModificationLog',
136             'tableName' => 'timemachine_modlog',
137         ));
138     }
139
140     /**
141      * clean timemachine_modlog for records that have been pruned (not deleted!)
142      *
143      * TODO if replication is on, we need to keep the "deleted" / "pruned" message in the modlog
144      */
145     public function clean()
146     {
147         $filter = new Tinebase_Model_Filter_FilterGroup();
148         $pagination = new Tinebase_Model_Pagination();
149         $pagination->limit = 10000;
150         $pagination->sort = 'id';
151
152         $totalCount = 0;
153
154         while ( ($recordSet = $this->_backend->search($filter, $pagination)) && $recordSet->count() > 0 ) {
155             $filter = new Tinebase_Model_Filter_FilterGroup();
156             $pagination->start += $pagination->limit;
157             $models = array();
158
159             foreach($recordSet as $modlog) {
160                 $models[$modlog->record_type][$modlog->record_id][] = $modlog->id;
161             }
162
163             foreach($models as $model => &$ids) {
164
165                 if ('Tinebase_Model_Tree_Node' === $model) {
166                     continue;
167                 }
168
169                 $app = null;
170                 $appNotFound = false;
171
172                 try {
173                     $app = Tinebase_Core::getApplicationInstance($model, '', true);
174                 } catch (Tinebase_Exception_NotFound $tenf) {
175                     $appNotFound = true;
176                 }
177
178                 if (!$appNotFound) {
179
180                     if ($app instanceof Tinebase_Container)
181                     {
182                         $backend = $app;
183
184                     } else {
185                         if (!$app instanceof Tinebase_Controller_Record_Abstract) {
186                             if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
187                                 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' model: ' . $model . ' controller: ' . get_class($app) . ' not an instance of Tinebase_Controller_Record_Abstract');
188                             continue;
189                         }
190
191                         $backend = $app->getBackend();
192                     }
193
194                     if (!$backend instanceof Tinebase_Backend_Interface) {
195                         if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
196                             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' model: ' . $model . ' backend: ' . get_class($backend) . ' not an instance of Tinebase_Backend_Interface');
197                         continue;
198                     }
199                     /** @var Tinebase_Record_Abstract $record */
200                     $record = new $model(null, true);
201
202                     /** @var Tinebase_Model_Filter_FilterGroup $idFilter */
203                     $idFilter = Tinebase_Model_Filter_FilterGroup::getFilterForModel(
204                         $model,
205                         array(),
206                         '',
207                         array('ignoreAcl' => true)
208                     );
209                     $idFilter->addFilter(new Tinebase_Model_Filter_Id(array(
210                         'field' => $record->getIdProperty(), 'operator' => 'in', 'value' => array_keys($ids)
211                     )));
212
213
214                     // to work around Tinebase_Container, we just send one more true parameter, will be ignored by all real backends, only taken into account by Tinebase_Container
215                     $existingIds = $backend->search($idFilter, null, true, true);
216
217                     if (!is_array($existingIds)) {
218                         throw new Exception('search for model: ' . $model . ' returned not an array!');
219                     }
220                     foreach ($existingIds as $id) {
221                         unset($ids[$id]);
222                     }
223                 }
224
225                 if ( count($ids) > 0 ) {
226                     $toDelete = array();
227                     foreach ($ids as $idArrays) {
228                         foreach ($idArrays as $id) {
229                             $toDelete[$id] = true;
230                         }
231                     }
232
233                     $toDelete = array_keys($toDelete);
234
235                     $this->_backend->delete($toDelete);
236                     $totalCount += count($toDelete);
237                 }
238             }
239         }
240
241         if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
242             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' deleted ' . $totalCount . ' modlogs records');
243
244         return $totalCount;
245     }
246     
247     /**
248      * Returns modification of a given record in a given timespan
249      * 
250      * @param string $_application application of given identifier  
251      * @param string $_id identifier to retrieve modification log for
252      * @param string $_type 
253      * @param string $_backend 
254      * @param Tinebase_DateTime $_from beginning point of timespan, excluding point itself
255      * @param Tinebase_DateTime $_until end point of timespan, including point itself
256      * @param int $_modifierId optional
257      * @return Tinebase_Record_RecordSet RecordSet of Tinebase_Model_ModificationLog
258      * 
259      * @todo use backend search() + Tinebase_Model_ModificationLogFilter
260      */
261     public function getModifications($_application, $_id, $_type = NULL, $_backend = 'Sql', Tinebase_DateTime $_from = NULL, Tinebase_DateTime $_until = NULL,  $_modifierId = NULL)
262     {
263         $id = ($_id instanceof Tinebase_Record_Interface) ? $_id->getId() : $_id;
264         $application = Tinebase_Application::getInstance()->getApplicationByName($_application);
265         
266         $isoDef = 'Y-m-d\TH:i:s';
267         
268         $db = $this->_table->getAdapter();
269         $select = $db->select()
270             ->from($this->_tablename)
271             ->order('instance_seq ASC')
272             ->where($db->quoteInto($db->quoteIdentifier('application_id') . ' = ?', $application->id))
273             ->where($db->quoteInto($db->quoteIdentifier('record_id') . ' = ?', $id));
274         
275         if ($_from) {
276             $select->where($db->quoteInto($db->quoteIdentifier('modification_time') . ' > ?', $_from->toString($isoDef)));
277         }
278         
279         if ($_until) {
280             $select->where($db->quoteInto($db->quoteIdentifier('modification_time') . ' <= ?', $_until->toString($isoDef)));
281         }
282         
283         if ($_type) {
284             $select->where($db->quoteInto($db->quoteIdentifier('record_type') . ' LIKE ?', $_type));
285         }
286         
287         if ($_backend) {
288             $select->where($db->quoteInto($db->quoteIdentifier('record_backend') . ' LIKE ?', $_backend));
289         }
290         
291         if ($_modifierId) {
292             $select->where($db->quoteInto($db->quoteIdentifier('modification_account') . ' = ?', $_modifierId));
293         }
294        
295         $stmt = $db->query($select);
296         $resultArray = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
297        
298         $modifications = new Tinebase_Record_RecordSet('Tinebase_Model_ModificationLog', $resultArray);
299         return $modifications;
300     }
301
302     /**
303      * get modifications by seq
304      *
305      * @param string $applicationId
306      * @param Tinebase_Record_Interface $newRecord
307      * @param integer $currentSeq
308      * @return Tinebase_Record_RecordSet RecordSet of Tinebase_Model_ModificationLog
309      */
310     public function getModificationsBySeq($applicationId, Tinebase_Record_Interface $newRecord, $currentSeq)
311     {
312         $filter = new Tinebase_Model_ModificationLogFilter(array(
313             array('field' => 'seq',            'operator' => 'greater', 'value' => $newRecord->seq),
314             array('field' => 'seq',            'operator' => 'less',    'value' => $currentSeq + 1),
315             array('field' => 'record_type',    'operator' => 'equals',  'value' => get_class($newRecord)),
316             array('field' => 'record_id',      'operator' => 'equals',  'value' => $newRecord->getId()),
317             array('field' => 'application_id', 'operator' => 'equals',  'value' => $applicationId),
318         ));
319         $paging = new Tinebase_Model_Pagination(array(
320             'sort' => 'seq'
321         ));
322         
323         return $this->_backend->search($filter, $paging);
324     }
325
326     /**
327      * get modifications for replication (instance_id == TinebaseId) by instance seq
328      *
329      * @param integer $currentSeq
330      * @return Tinebase_Record_RecordSet RecordSet of Tinebase_Model_ModificationLog
331      */
332     public function getReplicationModificationsByInstanceSeq($currentSeq, $limit = 100)
333     {
334         $filter = new Tinebase_Model_ModificationLogFilter(array(
335             array('field' => 'instance_id',  'operator' => 'equals',  'value' => Tinebase_Core::getTinebaseId()),
336             array('field' => 'instance_seq', 'operator' => 'greater', 'value' => $currentSeq)
337         ));
338         $paging = new Tinebase_Model_Pagination(array(
339             'limit' => $limit,
340             'sort'  => 'instance_seq'
341         ));
342
343         return $this->_backend->search($filter, $paging);
344     }
345
346     /**
347      * @return int
348      */
349     public function getMaxInstanceSeq()
350     {
351         $db = $this->_table->getAdapter();
352         $select = $db->select()
353             ->from($this->_tablename, new Zend_Db_Expr('MAX(' . $db->quoteIdentifier('instance_seq') . ')'))
354             ->where($db->quoteInto($db->quoteIdentifier('instance_id') . ' = ?', Tinebase_Core::getTinebaseId()));
355
356         $stmt = $db->query($select);
357         $resultArray = $stmt->fetchAll(Zend_Db::FETCH_NUM);
358
359         if (count($resultArray) === 0) {
360             return 0;
361         }
362
363         return intval($resultArray[0][0]);
364     }
365     
366     /**
367      * Computes effective difference from a set of modifications
368      *
369      * TODO check this claim re modified_from
370      * TODO activate and rewrite test
371      *
372      * If a attribute got changed more than once, the returned diff has all
373      * properties of the last change to the attribute, besides the 
374      * 'modified_from', which holds the modified_from of the first change.
375      * 
376      * @param Tinebase_Record_RecordSet $modifications
377      * @return Tinebase_Record_Diff differences
378      */
379     public function computeDiff(Tinebase_Record_RecordSet $modifications)
380     {
381         $diff = array();
382         $oldData = array();
383         /** @var Tinebase_Model_ModificationLog $modification */
384         foreach ($modifications as $modification) {
385             $modified_attribute = $modification->modified_attribute;
386
387             // legacy code
388             if (!empty($modified_attribute)) {
389                 if (!array_key_exists($modified_attribute, $diff)) {
390                     $oldData[$modified_attribute] = $modification->old_value;
391                 }
392                 $diff[$modified_attribute] = $modification->new_value;
393
394             // new modificationlog implementation
395             } else {
396                 $tmpDiff = new Tinebase_Record_Diff(json_decode($modification->new_value, true));
397                 if (is_array($tmpDiff->diff)) {
398                     foreach ($tmpDiff->diff as $key => $value) {
399                         if (!array_key_exists($key, $diff)) {
400                             $oldData[$key] = $tmpDiff->oldData[$key];
401                         }
402                         $diff[$key] = $value;
403                     }
404                 }
405             }
406         }
407         $result = new Tinebase_Record_Diff();
408         $result->diff = $diff;
409         $result->oldData = $oldData;
410         return $result;
411     }
412     
413     /**
414      * Returns a single logbook entry identified by an logbook identifier
415      * 
416      * @param   string $_id
417      * @return  Tinebase_Model_ModificationLog
418      * @throws  Tinebase_Exception_NotFound
419      *
420     public function getModification($_id)
421     {
422         $db = $this->_table->getAdapter();
423         $stmt = $db->query($db->select()
424            ->from($this->_tablename)
425            ->where($this->_table->getAdapter()->quoteInto($db->quoteIdentifier('id') . ' = ?', $_id))
426         );
427         $RawLogEntry = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
428         
429         if (empty($RawLogEntry)) {
430             throw new Tinebase_Exception_NotFound("Modification Log with id: $_id not found!");
431         }
432         return new Tinebase_Model_ModificationLog($RawLogEntry[0], true);
433     }*/
434
435     /**
436      * Saves a logbook record
437      *
438      * @param Tinebase_Model_ModificationLog $modification
439      * @return string id
440      * @throws Tinebase_Exception_Record_Validation
441      * @throws Tinebase_Timemachine_Exception_ConcurrencyConflict
442      * @throws Zend_Db_Statement_Exception
443      */
444     public function setModification(Tinebase_Model_ModificationLog $modification)
445     {
446         $modification->isValid(TRUE);
447         
448         $id = $modification->generateUID();
449         $modification->setId($id);
450         $modification->convertDates = true;
451
452         // mainly if we are applying replication modlogs on the slave, we set the masters instance id here
453         if (null !== $this->_externalInstanceId) {
454             $modification->instance_id = $this->_externalInstanceId;
455         }
456
457         $modificationArray = $modification->toArray();
458         if (is_array($modificationArray['new_value'])) {
459             throw new Tinebase_Exception_Record_Validation("New value is an array! \n" . print_r($modificationArray['new_value'], true));
460         }
461         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
462             . " Inserting modlog: " . print_r($modificationArray, TRUE));
463         try {
464             $this->_table->insert($modificationArray);
465         } catch (Zend_Db_Statement_Exception $zdse) {
466             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
467                 $zdse->getMessage() . ' ' . print_r($modification->toArray(), TRUE));
468             
469             // check if unique key constraint failed
470             $filter = new Tinebase_Model_ModificationLogFilter(array(
471                 array('field' => 'seq',                'operator' => 'equals',  'value' => $modification->seq),
472                 array('field' => 'record_type',        'operator' => 'equals',  'value' => $modification->record_type),
473                 array('field' => 'record_id',          'operator' => 'equals',  'value' => $modification->record_id),
474                 array('field' => 'modified_attribute', 'operator' => 'equals',  'value' => $modification->modified_attribute),
475             ));
476             $result = $this->_backend->search($filter);
477             if (count($result) > 0) {
478                 throw new Tinebase_Timemachine_Exception_ConcurrencyConflict('Seq ' . $modification->seq . ' for record ' . $modification->record_id . ' already exists');
479             } else {
480                 throw $zdse;
481             }
482         }
483         
484         return $id;
485     }
486     
487     /**
488      * merges changes made to local storage on concurrent updates into the new record 
489      *
490      * @param string $applicationId
491      * @param  Tinebase_Record_Interface $newRecord record from user data
492      * @param  Tinebase_Record_Interface $curRecord record from storage
493      * @return Tinebase_Record_Diff with resolved concurrent updates
494      * @throws Tinebase_Timemachine_Exception_ConcurrencyConflict
495      */
496     public function manageConcurrentUpdates($applicationId, Tinebase_Record_Interface $newRecord, Tinebase_Record_Interface $curRecord)
497     {
498         if (! $newRecord->has('seq')) {
499             /** @noinspection PhpDeprecationInspection */
500             return $this->manageConcurrentUpdatesByTimestamp($newRecord, $curRecord, get_class($newRecord), 'Sql', $newRecord->getId());
501         }
502
503         $this->_applicationId = $applicationId;
504
505         if ($curRecord->seq != $newRecord->seq) {
506             
507             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
508                 " Concurrent updates: current record last updated '" .
509                 ($curRecord->last_modified_time instanceof DateTime ? $curRecord->last_modified_time : 'unknown') .
510                 "' where record to be updated was last updated '" .
511                 ($newRecord->last_modified_time instanceof DateTime ? $newRecord->last_modified_time : 
512                     ($curRecord->creation_time instanceof DateTime ? $curRecord->creation_time : 'unknown')) .
513                 "' / current sequence: " . $curRecord->seq . " - new record sequence: " . $newRecord->seq);
514             
515             $loggedMods = $this->getModificationsBySeq($applicationId, $newRecord, $curRecord->seq)->filter('change_type', Tinebase_Timemachine_ModificationLog::UPDATED);
516             
517             // effective modifications made to the record after current user got his record
518             $diff = $this->computeDiff($loggedMods);
519             
520             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
521                 " During the concurrent update, the following changes have been made: " .
522                 print_r($diff->toArray(),true));
523             
524             $this->_resolveDiff($diff, $newRecord);
525
526             return $diff;
527             
528         } else {
529             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . " No concurrent updates.");
530         }
531         
532         return null;
533     }
534     
535     /**
536      * we loop over the diff! -> changes over fields which have no diff in storage are not in the loop!
537      *
538      * @param Tinebase_Record_Diff $diff
539      * @param Tinebase_Record_Interface $newRecord
540      */
541     protected function _resolveDiff(Tinebase_Record_Diff $diff, Tinebase_Record_Interface $newRecord)
542     {
543         if (!is_array($diff->diff)) {
544             // nothing to do
545             return;
546         }
547
548         $diffArray = $diff->diff;
549         /** @var Tinebase_Record_Abstract $newRecord */
550         $newRecord->_convertISO8601ToDateTime($diffArray);
551
552         foreach ($diffArray as $key => $value) {
553             $newUserValue = isset($newRecord->$key) ? Tinebase_Helper::normalizeLineBreaks($newRecord->$key) : NULL;
554             
555             if (isset($newRecord->$key) && $newUserValue == Tinebase_Helper::normalizeLineBreaks($value)) {
556                 //$this->_resolveScalarSameValue($newRecord, $diff);
557                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
558                     . " User updated to same value for field '" . $key . "', nothing to do.");
559             
560             } else if (! isset($newRecord[$key]) || $newUserValue == Tinebase_Helper::normalizeLineBreaks($diff->oldData[$key])) {
561                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
562                     . ' Merge current value into update data, as it was not changed in update request.');
563                 if ($newRecord->has($key)) {
564                     $newRecord->$key = $value;
565                 } else {
566                     if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
567                         . ' It seems that the attribute ' . $key . ' no longer exists in this record. Skipping ...');
568                 }
569             
570             } else if ($newRecord[$key] instanceof Tinebase_Record_RecordSet) {
571                 $this->_resolveRecordSetMergeUpdate($newRecord, $key, $value);
572             
573             } else {
574                 $this->_nonResolvableConflict($newUserValue, $key, $diff);
575             }
576         }
577     }
578     
579     /**
580      * Update to same value, nothing to do
581      * 
582      * @param Tinebase_Record_Interface $newRecord
583      * @param Tinebase_Record_Diff $diff
584      *
585     protected function _resolveScalarSameValue(Tinebase_Record_Interface $newRecord, Tinebase_Record_Diff $diff)
586     {
587         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
588             . " User updated to same value for field '" . $diff->modified_attribute . "', nothing to do.");
589     }*/
590
591     /**
592      * Merge current value into update data, as it was not changed in update request
593      * 
594      * @param Tinebase_Record_Interface $newRecord
595      * @param Tinebase_Record_Diff $diff
596      *
597     protected function _resolveScalarMergeUpdate(Tinebase_Record_Interface $newRecord, Tinebase_Record_Diff $diff)
598     {
599         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
600             . ' Merge current value into update data, as it was not changed in update request.');
601         if ($newRecord->has($diff->modified_attribute)) {
602             $newRecord[$diff->modified_attribute] = $diff->new_value;
603         } else {
604             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
605                 . ' It seems that the attribute ' . $diff->modified_attribute . ' no longer exists in this record. Skipping ...');
606         }
607     } */
608
609     /**
610      * record set diff resolving
611      *
612      * @param Tinebase_Record_Interface $newRecord
613      * @param string $attribute
614      * @param string $newValue
615      * @throws Tinebase_Timemachine_Exception_ConcurrencyConflict
616      */
617     protected function _resolveRecordSetMergeUpdate(Tinebase_Record_Interface $newRecord, $attribute, $newValue)
618     {
619         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
620             . " Try to merge record set changes of record attribute " . $attribute);
621         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
622             . ' New record: ' . print_r($newRecord->toArray(), TRUE));
623         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
624             . ' Mod log: ' . print_r($newValue, TRUE));
625
626         $concurrentChangeDiff = new Tinebase_Record_RecordSetDiff($newValue);
627         
628         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
629             . ' RecordSet diff: ' . print_r($concurrentChangeDiff->toArray(), TRUE));
630         
631         foreach ($concurrentChangeDiff->added as $added) {
632             /** @var Tinebase_Record_Abstract $addedRecord */
633             $addedRecord = new $concurrentChangeDiff->model($added);
634             if (! $newRecord->$attribute->getById($addedRecord->getId())) {
635                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
636                     . " Adding recently added record " . $addedRecord->getId());
637                 $newRecord->$attribute->addRecord($addedRecord);
638             }
639         }
640         
641         foreach ($concurrentChangeDiff->removed as $removed) {
642             /** @var Tinebase_Record_Abstract $removedRecord */
643             $removedRecord = new $concurrentChangeDiff->model($removed);
644             /** @var Tinebase_Record_Abstract $recordToRemove */
645             $recordToRemove = $newRecord->$attribute->getById($removedRecord->getId());
646             if ($recordToRemove) {
647                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
648                     . " Removing record " . $recordToRemove->getId());
649                 $newRecord->$attribute->removeRecord($recordToRemove);
650             }
651         }
652         
653         foreach ($concurrentChangeDiff->modified as $modified) {
654             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
655                 . ' modified diff: ' . print_r($modified, TRUE));
656
657             /** @var Tinebase_Record_Abstract $modifiedRecord */
658             $modifiedRecord = new $concurrentChangeDiff->model(array_merge(array('id' => $modified['id']), $modified['diff']), TRUE);
659             /** @var Tinebase_Record_Abstract $newRecordsRecord */
660             $newRecordsRecord = $newRecord->$attribute->getById($modifiedRecord->getId());
661             if ($newRecordsRecord && ($newRecordsRecord->has('seq') || $newRecordsRecord->has('last_modified_time'))) {
662                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
663                     . ' Managing updates for ' . get_class($newRecordsRecord) . ' record ' . $newRecordsRecord->getId());
664                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
665                     . ' new record: ' . print_r($newRecordsRecord->toArray(), TRUE));
666                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
667                     . ' modified record: ' . print_r($modifiedRecord->toArray(), TRUE));
668
669                 if (null === $this->_applicationId) {
670                     throw new Tinebase_Exception_UnexpectedValue('application_id needs to be set here');
671                 }
672                 $this->manageConcurrentUpdates($this->_applicationId, $newRecordsRecord, $modifiedRecord);
673             } else {
674                 throw new Tinebase_Timemachine_Exception_ConcurrencyConflict('concurrency conflict - modified record changes could not be merged!');
675             }
676         }
677         
678         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
679             . ' New record after merge: ' . print_r($newRecord->toArray(), TRUE));
680     }
681     
682     /**
683      * Non resolvable concurrency conflict detected
684      * 
685      * @param string $newUserValue
686      * @param string $attribute
687      * @param Tinebase_Record_Diff $diff
688      * @throws Tinebase_Timemachine_Exception_ConcurrencyConflict
689      */
690     protected function _nonResolvableConflict($newUserValue, $attribute, Tinebase_Record_Diff $diff)
691     {
692         if (Tinebase_Core::isLogLevel(Zend_Log::ERR)) Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ 
693             . " Non resolvable conflict for field '" . $attribute . "'!");
694         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
695             . ' New user value: ' . var_export($newUserValue, TRUE)
696             . ' New diff value: ' . var_export($diff->diff[$attribute], TRUE)
697             . ' Old diff value: ' . var_export($diff->oldData[$attribute], TRUE));
698         
699         throw new Tinebase_Timemachine_Exception_ConcurrencyConflict('concurrency conflict!');
700     }
701     
702     /**
703      * merges changes made to local storage on concurrent updates into the new record 
704      * 
705      * @param  Tinebase_Record_Interface $_newRecord record from user data
706      * @param  Tinebase_Record_Interface $_curRecord record from storage
707      * @param  string $_model
708      * @param  string $_backend
709      * @param  string $_id
710      * @return Tinebase_Record_Diff with resolved concurrent updates
711      * @throws Tinebase_Timemachine_Exception_ConcurrencyConflict
712      * 
713      * @deprecated this should be removed when all records have seq(uence)
714      */
715     public function manageConcurrentUpdatesByTimestamp(Tinebase_Record_Interface $_newRecord, Tinebase_Record_Interface $_curRecord, $_model, $_backend, $_id)
716     {
717         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
718             . ' Calling deprecated method. Model ' . $_model . ' should get a seq property.');
719         
720         list($appName) = explode('_', $_model);
721         
722         // handle concurrent updates on unmodified records
723         if (! $_newRecord->last_modified_time instanceof DateTime) {
724             if ($_curRecord->creation_time instanceof DateTime) {
725                 $_newRecord->last_modified_time = clone $_curRecord->creation_time;
726             } else {
727                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ 
728                     . ' Something went wrong! No creation_time was set in current record: ' 
729                     . print_r($_curRecord->toArray(), TRUE)
730                 );
731                 return null;
732             }
733         }
734         
735         if ($_curRecord->last_modified_time instanceof DateTime && !$_curRecord->last_modified_time->equals($_newRecord->last_modified_time)) {
736
737             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " concurrent updates: current record last updated '" .
738                 $_curRecord->last_modified_time . "' where record to be updated was last updated '" . $_newRecord->last_modified_time . "'");
739             
740             $loggedMods = $this->getModifications($appName, $_id,
741                 $_model, $_backend, $_newRecord->last_modified_time, $_curRecord->last_modified_time)->filter('change_type', Tinebase_Timemachine_ModificationLog::UPDATED);
742             // effective modifications made to the record after current user got his record
743             $diff = $this->computeDiff($loggedMods);
744
745             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " during the concurrent update, the following changes have been made: " .
746                 print_r($diff->toArray(),true));
747
748             $this->_resolveDiff($diff, $_newRecord);
749
750             return $diff;
751         }
752         
753         return null;
754     }
755     
756     /**
757      * computes changes of records and writes them to the logbook
758      * 
759      * NOTE: expects last_modified_by and last_modified_time to be set
760      * properly in the $_newRecord
761      * 
762      * @param  Tinebase_Record_Interface $_newRecord record from user data
763      * @param  Tinebase_Record_Interface $_curRecord record from storage
764      * @param  string $_model
765      * @param  string $_backend
766      * @param  string $_id
767      * @return Tinebase_Record_RecordSet RecordSet of Tinebase_Model_ModificationLog
768      */
769     public function writeModLog($_newRecord, $_curRecord, $_model, $_backend, $_id)
770     {
771         $modifications = new Tinebase_Record_RecordSet('Tinebase_Model_ModificationLog');
772         if (null !== $_curRecord && null !== $_newRecord) {
773             $diff = $_curRecord->diff($_newRecord, array_merge($this->_metaProperties, $_newRecord->getModlogOmitFields()));
774             $notNullRecord = $_newRecord;
775         } else {
776             if (null !== $_newRecord) {
777                 $notNullRecord = $_newRecord;
778                 $diffProp = 'diff';
779             } else {
780                 $notNullRecord = $_curRecord;
781                 $diffProp = 'oldData';
782             }
783             $diffData = $notNullRecord->toArray();
784
785             foreach (array_merge($this->_metaProperties, $notNullRecord->getModlogOmitFields()) as $omit) {
786                 if (isset($diffData[$omit])) {
787                     unset($diffData[$omit]);
788                 }
789             }
790
791             $diff = new Tinebase_Record_Diff(array($diffProp => $diffData));
792         }
793
794         if (! $diff->isEmpty()) {
795             $updateMetaData = array('seq' => ($notNullRecord->has('seq')) ? $notNullRecord->seq : 0);
796             $last_modified_time = $notNullRecord->last_modified_time;
797             if (!empty($last_modified_time)) {
798                 $updateMetaData['last_modified_time'] = $last_modified_time;
799             }
800             $last_modified_by   = $notNullRecord->last_modified_by;
801             if (!empty($last_modified_by)) {
802                 $updateMetaData['last_modified_by'] = $last_modified_by;
803             }
804             $commonModLog = $this->_getCommonModlog($_model, $_backend, $updateMetaData, $_id);
805             $commonModLog->new_value = json_encode($diff->toArray());
806             if (null === $_newRecord) {
807                 $commonModLog->change_type = self::DELETED;
808             } elseif(null === $_curRecord) {
809                 $commonModLog->change_type = self::CREATED;
810             } else {
811                 $commonModLog->change_type = self::UPDATED;
812             }
813
814             if(true === $notNullRecord->isReplicable()) {
815                 $commonModLog->instance_id = Tinebase_Core::getTinebaseId();
816             }
817
818             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) {
819                 Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
820                     . ' Diffs: ' . print_r($diff->diff, TRUE));
821                 Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
822                     . ' CurRecord: ' . ($_curRecord!==null?print_r($_curRecord->toArray(), TRUE):'null'));
823                 Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
824                     . ' NewRecord: ' . ($_newRecord!==null?print_r($_newRecord->toArray(), TRUE):'null'));
825                 Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
826                     . ' Common modlog: ' . print_r($commonModLog->toArray(), TRUE));
827             }
828
829             $this->setModification($commonModLog);
830
831             $modifications->addRecord($commonModLog);
832         }
833
834         return $modifications;
835
836         /** old
837
838         $this->_loopModifications($diffs, $commonModLog, $modifications, $_curRecord->toArray(), $_curRecord->getModlogOmitFields());
839         
840         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
841             . ' Logged ' . count($modifications) . ' modifications.');
842         
843         return $modifications; */
844     }
845     
846     /**
847      * creates a common modlog record
848      * 
849      * @param string $_model
850      * @param string $_backend
851      * @param array $_updateMetaData
852      * @param string $_recordId
853      * @return Tinebase_Model_ModificationLog
854      */
855     protected function _getCommonModlog($_model, $_backend, $_updateMetaData = array(), $_recordId = NULL)
856     {
857         if (empty($_updateMetaData) || ! isset($_updateMetaData['last_modified_by']) ||  ! isset($_updateMetaData['last_modified_time'])) {
858             list($currentAccountId, $currentTime) = Tinebase_Timemachine_ModificationLog::getCurrentAccountIdAndTime();
859         } else {
860             $currentAccountId = $_updateMetaData['last_modified_by'];
861             $currentTime      = $_updateMetaData['last_modified_time'];
862         }
863
864         $client = Tinebase_Core::get('serverclassname');
865         if (isset($_SERVER['HTTP_USER_AGENT'])) {
866             $client .= ' - ' . $_SERVER['HTTP_USER_AGENT'];
867         } else {
868             $client .= ' - no http user agent present';
869         }
870         
871         list($appName/*, $i, $modelName*/) = explode('_', $_model);
872         $commonModLogEntry = new Tinebase_Model_ModificationLog(array(
873             'application_id'       => Tinebase_Application::getInstance()->getApplicationByName($appName)->getId(),
874             'record_id'            => $_recordId,
875             'record_type'          => $_model,
876             'record_backend'       => $_backend,
877             'modification_time'    => $currentTime,
878             'modification_account' => $currentAccountId,
879             'seq'                  => (isset($_updateMetaData['seq'])) ? $_updateMetaData['seq'] : 0,
880             'client'               => $client
881         ), TRUE);
882         
883         return $commonModLogEntry;
884     }
885
886     /**
887      * write modlog for multiple records
888      *
889      * @param array $_ids
890      * @param $_currentData
891      * @param array $_newData
892      * @param string $_model
893      * @param string $_backend
894      * @param array $updateMetaData
895      * @return Tinebase_Record_RecordSet RecordSet of Tinebase_Model_ModificationLog
896      * @throws Tinebase_Exception_NotImplemented
897      * @internal param array $_oldData
898      *
899      * TODO instance id is never set in this code path! => thus replication doesn't work here!
900      */
901     public function writeModLogMultiple($_ids, $_currentData, $_newData, $_model, $_backend, $updateMetaData = array())
902     {
903         //return new Tinebase_Record_RecordSet('Tinebase_Model_ModificationLog');
904
905         //throw new Tinebase_Exception_NotImplemented('fix it');
906
907         $commonModLog = $this->_getCommonModlog($_model, $_backend, $updateMetaData);
908         
909         $modifications = new Tinebase_Record_RecordSet('Tinebase_Model_ModificationLog');
910         
911         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
912             . ' Writing modlog for ' . count($_ids) . ' records.');
913         
914         foreach ($_ids as $id) {
915             $modification = clone $commonModLog;
916
917             $modification->record_id = $id;
918             if (isset($updateMetaData['recordSeqs']) && (isset($updateMetaData['recordSeqs'][$id]) || array_key_exists($id, $updateMetaData['recordSeqs']))) {
919                 $modification->seq = (! empty($updateMetaData['recordSeqs'][$id])) ? $updateMetaData['recordSeqs'][$id] + 1 : 1;
920             }
921             $diff = new Tinebase_Record_Diff();
922             $diff->diff = $_newData;
923             $diff->oldData = $_currentData;
924             $modification->new_value = json_encode($diff->toArray());
925
926             $this->setModification($modification);
927             $modifications->addRecord($modification);
928             //$this->_loopModifications($_newData, $commonModLog, $modifications, $_currentData);
929         }
930         
931         return $modifications;
932     }
933     
934     /**
935      * undo modlog records defined by filter
936      * 
937      * @param Tinebase_Model_ModificationLogFilter $filter
938      * @param boolean $overwrite should changes made after the detected change be overwritten?
939      * @param boolean $dryrun
940      * @param string  $attribute limit undo to this attribute
941      * @return array
942      * 
943      * @todo use iterator?
944      * @todo return updated records/exceptions?
945      * @todo create result model / should be used in Tinebase_Controller_Record_Abstract::updateMultiple, too
946      * @todo use transaction with rollback for dryrun?
947      * @todo allow to undo tags/customfields/...
948      * @todo add interactive mode
949      */
950     public function undo(Tinebase_Model_ModificationLogFilter $filter, $overwrite = FALSE, $dryrun = FALSE, $attribute = null)
951     {
952         /* TODO fix this !*/
953         $notUndoableFields = array('tags', 'customfields', 'relations');
954         
955         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
956             ' Filter: ' . print_r($filter->toArray(), TRUE). ' attribute: ' . $attribute);
957         
958         $modlogRecords = $this->_backend->search($filter, new Tinebase_Model_Pagination(array(
959             'sort' => 'instance_seq',
960             'dir'  => 'DESC'
961         )));
962         
963         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
964             ' Found ' . count($modlogRecords) . ' modlog records matching the filter.');
965         
966         $updateCount = 0;
967         $failCount = 0;
968         $undoneModlogs = new Tinebase_Record_RecordSet('Tinebase_Model_ModificationLog');
969         $currentRecordType = NULL;
970         /** @var Tinebase_Controller_Record_Abstract $controller */
971         $controller = NULL;
972         $controllerCache = array();
973
974         /** @var Tinebase_Model_ModificationLog $modlog */
975         foreach ($modlogRecords as $modlog) {
976             if ($currentRecordType !== $modlog->record_type || ! isset($controller)) {
977                 $currentRecordType = $modlog->record_type;
978                 if (!isset($controllerCache[$modlog->record_type])) {
979                     $controller = Tinebase_Core::getApplicationInstance($modlog->record_type);
980                     $controllerCache[$modlog->record_type] = $controller;
981                 } else {
982                     $controller = $controllerCache[$modlog->record_type];
983                 }
984             }
985
986             if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
987                 ' Modlog: ' . print_r($modlog->toArray(), TRUE));
988
989
990             /* TODO $overwrite check in new code path! */
991
992             $modifiedAttribute = $modlog->modified_attribute;
993
994             try {
995
996                 if (empty($modifiedAttribute)) {
997                     // new handling using diff!
998
999                     $updateCount++;
1000
1001                     if (method_exists($controller, 'undoReplicationModificationLog')) {
1002                         $controller->undoReplicationModificationLog($modlog, $dryrun);
1003                     } else {
1004
1005                         if (Tinebase_Timemachine_ModificationLog::CREATED === $modlog->change_type) {
1006                             if (!$dryrun) {
1007                                 $controller->delete($modlog->record_id);
1008                             }
1009                         } elseif (Tinebase_Timemachine_ModificationLog::DELETED === $modlog->change_type) {
1010                             $diff = new Tinebase_Record_Diff(json_decode($modlog->new_value, true));
1011                             $model = $modlog->record_type;
1012                             $record = new $model($diff->oldData, true);
1013                             if (!$dryrun) {
1014                                 $controller->unDelete($record);
1015                             }
1016                         } else {
1017                             $record = $controller->get($modlog->record_id, null, true, true);
1018                             $diff = new Tinebase_Record_Diff(json_decode($modlog->new_value, true));
1019                             $record->undo($diff);
1020
1021                             if (!$dryrun) {
1022                                 $controller->update($record);
1023                             }
1024                         }
1025                     }
1026
1027                     // this is the legacy code for old data in existing installations
1028                 } else {
1029
1030                     $record = $controller->get($modlog->record_id);
1031
1032                     if (!in_array($modlog->modified_attribute, $notUndoableFields) && ($overwrite || $record->seq === $modlog->seq)) {
1033                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1034                             ' Reverting change id ' . $modlog->getId());
1035
1036                         $record->{$modlog->modified_attribute} = $modlog->old_value;
1037                         if (!$dryrun) {
1038                             $controller->update($record);
1039                         }
1040                         $updateCount++;
1041                         $undoneModlogs->addRecord($modlog);
1042                     } else {
1043                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1044                             ' Not reverting change of ' . $modlog->modified_attribute . ' of record ' . $modlog->record_id);
1045                     }
1046                 }
1047             } catch (Exception $e) {
1048                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' ' . $e);
1049                 $failCount++;
1050             }
1051         }
1052         
1053         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1054             ' Reverted ' . $updateCount . ' modlog changes.');
1055         
1056         return array(
1057             'totalcount'     => $updateCount,
1058             'failcount'      => $failCount,
1059             'undoneModlogs'  => $undoneModlogs,
1060 //             'exceptions' => NULL,
1061 //             'results'    => NULL,
1062         );
1063     }
1064     
1065     /**
1066      * sets record modification data and protects it from spoofing
1067      * 
1068      * @param   Tinebase_Record_Interface $_newRecord record from user data
1069      * @param   string                    $_action    one of {create|update|delete}
1070      * @param   Tinebase_Record_Interface $_curRecord record from storage
1071      * @throws  Tinebase_Exception_InvalidArgument
1072      */
1073     public static function setRecordMetaData(Tinebase_Record_Interface $_newRecord, $_action, Tinebase_Record_Interface $_curRecord = NULL)
1074     {
1075         // disable validation as this is slow and we are setting valid data here
1076         $bypassFilters = $_newRecord->bypassFilters;
1077         $_newRecord->bypassFilters = TRUE;
1078         
1079         list($currentAccountId, $currentTime) = self::getCurrentAccountIdAndTime();
1080         
1081         // spoofing protection
1082         $_newRecord->created_by         = $_curRecord ? $_curRecord->created_by : NULL;
1083         $_newRecord->creation_time      = $_curRecord ? $_curRecord->creation_time : NULL;
1084         $_newRecord->last_modified_by   = $_curRecord ? $_curRecord->last_modified_by : NULL;
1085         $_newRecord->last_modified_time = $_curRecord ? $_curRecord->last_modified_time : NULL;
1086         
1087         if ($_newRecord->has('is_deleted')) {
1088             $_newRecord->is_deleted     = $_curRecord ? $_curRecord->is_deleted : 0;
1089             $_newRecord->deleted_time   = $_curRecord ? $_curRecord->deleted_time : NULL;
1090             $_newRecord->deleted_by     = $_curRecord ? $_curRecord->deleted_by : NULL;
1091         }
1092         
1093         switch ($_action) {
1094             case 'create':
1095                 $_newRecord->created_by    = $currentAccountId;
1096                 $_newRecord->creation_time = $currentTime;
1097                 if ($_newRecord->has('seq')) {
1098                     $_newRecord->seq       = 1;
1099                 }
1100                 break;
1101             case 'update':
1102                 $_newRecord->last_modified_by   = $currentAccountId;
1103                 $_newRecord->last_modified_time = $currentTime;
1104                 self::increaseRecordSequence($_newRecord, $_curRecord);
1105                 break;
1106             case 'delete':
1107                 $_newRecord->deleted_by   = $currentAccountId;
1108                 $_newRecord->deleted_time = $currentTime;
1109                 $_newRecord->is_deleted   = true;
1110                 self::increaseRecordSequence($_newRecord, $_curRecord);
1111                 break;
1112             case 'undelete':
1113                 $_newRecord->deleted_by   = null;
1114                 $_newRecord->deleted_time = null;
1115                 $_newRecord->is_deleted   = 0;
1116                 self::increaseRecordSequence($_newRecord, $_curRecord);
1117                 break;
1118             default:
1119                 throw new Tinebase_Exception_InvalidArgument('Action must be one of {create|update|delete}.');
1120                 break;
1121         }
1122         
1123         $_newRecord->bypassFilters = $bypassFilters;
1124     }
1125     
1126     /**
1127      * increase record sequence
1128      * 
1129      * @param Tinebase_Record_Interface $newRecord
1130      * @param Tinebase_Record_Interface $curRecord
1131      */
1132     public static function increaseRecordSequence(Tinebase_Record_Interface $newRecord, Tinebase_Record_Interface $curRecord = NULL)
1133     {
1134         if (is_object($curRecord) && $curRecord->has('seq')) {
1135             $newRecord->seq = (int) $curRecord->seq +1;
1136             
1137             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1138                 ' Increasing seq of ' . get_class($newRecord) . ' with id ' . $newRecord->getId() .
1139                 ' from ' . ($newRecord->seq - 1) . ' to ' . $newRecord->seq);
1140         }
1141     }
1142     
1143     /**
1144      * returns current account id and time
1145      * 
1146      * @return array
1147      */
1148     public static function getCurrentAccountIdAndTime()
1149     {
1150         $currentAccount   = Tinebase_Core::getUser();
1151         $currentAccountId = $currentAccount instanceof Tinebase_Record_Interface ? $currentAccount->getId(): NULL;
1152         $currentTime      = new Tinebase_DateTime();
1153
1154         return array($currentAccountId, $currentTime);
1155     }
1156
1157     /**
1158      * removes modlog entries for that application
1159      *
1160      * @param Tinebase_Model_Application $_application
1161      *
1162      * @return void
1163      */
1164     public function removeApplication(Tinebase_Model_Application $_application)
1165     {
1166         $this->_backend->deleteByProperty($_application->getId(), 'application_id');
1167     }
1168
1169     public static function getModifiedAttributes(Tinebase_Record_RecordSet $modLogs)
1170     {
1171         $result = array();
1172
1173         /** @var Tinebase_Model_ModificationLog $modlog */
1174         foreach ($modLogs as $modlog) {
1175             $modAtrb = $modlog->modified_attribute;
1176             if (empty($modAtrb)) {
1177                 $diff = new Tinebase_Record_Diff(json_decode($modlog->new_value, true));
1178                 $result = array_merge($result, $diff->diff);
1179             } else {
1180                 $result[$modAtrb] = null;
1181             }
1182         }
1183
1184         return array_keys($result);
1185     }
1186
1187     public function fetchBlobFromMaster($hash)
1188     {
1189         $slaveConfiguration = Tinebase_Config::getInstance()->{Tinebase_Config::REPLICATION_SLAVE};
1190         $tine20Url = $slaveConfiguration->{Tinebase_Config::MASTER_URL};
1191         $tine20LoginName = $slaveConfiguration->{Tinebase_Config::MASTER_USERNAME};
1192         $tine20Password = $slaveConfiguration->{Tinebase_Config::MASTER_PASSWORD};
1193
1194         // check if we are a replication slave
1195         if (empty($tine20Url) || empty($tine20LoginName) || empty($tine20Password)) {
1196             return true;
1197         }
1198
1199         $tine20Service = new Zend_Service_Tine20($tine20Url);
1200
1201         $authResponse = $tine20Service->login($tine20LoginName, $tine20Password);
1202         if (!is_array($authResponse) || !isset($authResponse['success']) || $authResponse['success'] !== true) {
1203             throw new Tinebase_Exception_AccessDenied('login failed');
1204         }
1205         unset($authResponse);
1206
1207         $tinebaseProxy = $tine20Service->getProxy('Tinebase');
1208         /** @noinspection PhpUndefinedMethodInspection */
1209         $result = $tinebaseProxy->getBlob($hash);
1210
1211         $fileObject = new Tinebase_Model_Tree_FileObject(array('hash' => $hash), true);
1212         $path = $fileObject->getFilesystemPath();
1213         if (!is_dir(dirname($path))) {
1214             mkdir(dirname($path));
1215         }
1216         file_put_contents($path, $result);
1217     }
1218
1219     /**
1220      * @return bool
1221      * @throws Tinebase_Exception_AccessDenied
1222      */
1223     public function readModificationLogFromMaster()
1224     {
1225         $slaveConfiguration = Tinebase_Config::getInstance()->{Tinebase_Config::REPLICATION_SLAVE};
1226         $tine20Url = $slaveConfiguration->{Tinebase_Config::MASTER_URL};
1227         $tine20LoginName = $slaveConfiguration->{Tinebase_Config::MASTER_USERNAME};
1228         $tine20Password = $slaveConfiguration->{Tinebase_Config::MASTER_PASSWORD};
1229
1230         // check if we are a replication slave
1231         if (empty($tine20Url) || empty($tine20LoginName) || empty($tine20Password)) {
1232             return true;
1233         }
1234
1235         $result = Tinebase_Lock::aquireDBSessionLock(__FUNCTION__);
1236         if (false === $result) {
1237             // we are already running
1238             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ .
1239                 ' failed to aquire DB lock, it seems we are already running in a parallel process.');
1240             return true;
1241         }
1242         if (null === $result) {
1243             // DB backend does not suppport lock, no way we do replication without proper thread concurrency!
1244             Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ .
1245                 ' failed to aquire DB lock, the DB backend doesn\'t support locks. You should not run a replication on this type of DB backend!');
1246             return false;
1247         }
1248
1249         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1250             ' trying to connect to master host: ' . $tine20Url . ' with user: ' . $tine20LoginName);
1251
1252         $tine20Service = new Zend_Service_Tine20($tine20Url);
1253
1254         $authResponse = $tine20Service->login($tine20LoginName, $tine20Password);
1255         if (!is_array($authResponse) || !isset($authResponse['success']) || $authResponse['success'] !== true) {
1256             throw new Tinebase_Exception_AccessDenied('login failed');
1257         }
1258         unset($authResponse);
1259
1260         //get replication state:
1261         $state = Tinebase_Application::getInstance()->getApplicationByName('Tinebase')->state;
1262         if (!is_array($state) || !isset($state[Tinebase_Model_Application::STATE_REPLICATION_MASTER_ID])) {
1263             $masterReplicationId = 0;
1264         } else {
1265             $masterReplicationId = $state[Tinebase_Model_Application::STATE_REPLICATION_MASTER_ID];
1266         }
1267
1268         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1269             ' master replication id: ' . $masterReplicationId);
1270
1271         $tinebaseProxy = $tine20Service->getProxy('Tinebase');
1272         /** @noinspection PhpUndefinedMethodInspection */
1273         $result = $tinebaseProxy->getReplicationModificationLogs($masterReplicationId, 100);
1274         /* TODO make the amount above configurable  */
1275
1276         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
1277             ' received ' . count($result['results']) . ' modification logs');
1278
1279         // memory cleanup
1280         unset($tinebaseProxy);
1281         unset($tine20Service);
1282
1283         $modifications = new Tinebase_Record_RecordSet('Tinebase_Model_ModificationLog', $result['results']);
1284         unset($result);
1285
1286         return $this->applyReplicationModLogs($modifications);
1287     }
1288
1289     /**
1290      * apply modification logs from a replication master locally
1291      *
1292      * @param Tinebase_Record_RecordSet $modifications
1293      * @return boolean
1294      */
1295     public function applyReplicationModLogs(Tinebase_Record_RecordSet $modifications)
1296     {
1297         $currentRecordType = NULL;
1298         $controller = NULL;
1299         $controllerCache = array();
1300
1301         $transactionManager = Tinebase_TransactionManager::getInstance();
1302         $db = Tinebase_Core::getDb();
1303         $applicationController = Tinebase_Application::getInstance();
1304         /** @var Tinebase_Model_Application $tinebaseApplication */
1305         $tinebaseApplication = $applicationController->getApplicationByName('Tinebase');
1306
1307         /** @var Tinebase_Model_ModificationLog $modification */
1308         foreach($modifications as $modification)
1309         {
1310             $transactionId = $transactionManager->startTransaction($db);
1311
1312             $this->_externalInstanceId = $modification->instance_id;
1313
1314             try {
1315                 if ($currentRecordType !== $modification->record_type || !isset($controller)) {
1316                     $currentRecordType = $modification->record_type;
1317                     if (!isset($controllerCache[$modification->record_type])) {
1318                         $controller = Tinebase_Core::getApplicationInstance($modification->record_type);
1319                         $controllerCache[$modification->record_type] = $controller;
1320                     } else {
1321                         $controller = $controllerCache[$modification->record_type];
1322                     }
1323                 }
1324
1325                 if (method_exists($controller, 'applyReplicationModificationLog')) {
1326                     $controller->applyReplicationModificationLog($modification);
1327                 } else {
1328                     static::defaultApply($modification, $controller);
1329                 }
1330
1331                 $tinebaseApplication->xprops('state')[Tinebase_Model_Application::STATE_REPLICATION_MASTER_ID] =
1332                     $modification->instance_seq;
1333                 $tinebaseApplication = $applicationController->updateApplication($tinebaseApplication);
1334
1335                 $transactionManager->commitTransaction($transactionId);
1336
1337             } catch (Exception $e) {
1338                 $this->_externalInstanceId = null;
1339
1340                 Tinebase_Exception::log($e, false);
1341
1342                 $transactionManager->rollBack();
1343
1344                 // notify configured email addresses about replication failure
1345                 $config = Tinebase_Config::getInstance()->get(Tinebase_Config::REPLICATION_SLAVE);
1346                 if (is_array($config->{Tinebase_Config::ERROR_NOTIFICATION_LIST}) &&
1347                         count($config->{Tinebase_Config::ERROR_NOTIFICATION_LIST}) > 0) {
1348
1349                     $recipients = array();
1350                     foreach($config->{Tinebase_Config::ERROR_NOTIFICATION_LIST} as $recipient) {
1351                         $recipients[] = new Addressbook_Model_Contact(array('email' => $recipient), true);
1352                     }
1353
1354                     $plain = $e->getMessage() . PHP_EOL . PHP_EOL . $e->getTraceAsString();
1355
1356                     Tinebase_Notification::getInstance()->send(Tinebase_Core::getUser(), $recipients, 'replication client error', $plain);
1357                 }
1358
1359                 // must not happen, continuing pointless!
1360                 return false;
1361             }
1362         }
1363
1364         $this->_externalInstanceId = null;
1365
1366         return true;
1367     }
1368
1369     /**
1370      * @param Tinebase_Model_ModificationLog $_modification
1371      * @param Tinebase_Controller_Record_Abstract $_controller
1372      * @throws Tinebase_Exception
1373      */
1374     public static function defaultApply(Tinebase_Model_ModificationLog $_modification, $_controller)
1375     {
1376         switch ($_modification->change_type) {
1377             case Tinebase_Timemachine_ModificationLog::CREATED:
1378                 $diff = new Tinebase_Record_Diff(json_decode($_modification->new_value, true));
1379                 $model = $_modification->record_type;
1380                 $record = new $model($diff->diff);
1381                 $_controller->create($record);
1382                 break;
1383
1384             case Tinebase_Timemachine_ModificationLog::UPDATED:
1385                 $diff = new Tinebase_Record_Diff(json_decode($_modification->new_value, true));
1386                 $record = $_controller->get($_modification->record_id, NULL, true, true);
1387                 $record->applyDiff($diff);
1388                 $_controller->update($record);
1389                 break;
1390
1391             case Tinebase_Timemachine_ModificationLog::DELETED:
1392                 $_controller->delete($_modification->record_id);
1393                 break;
1394
1395             default:
1396                 throw new Tinebase_Exception('unknown Tinebase_Model_ModificationLog->change_type: ' . $_modification->change_type);
1397         }
1398     }
1399
1400     /**
1401      * @param int $count
1402      */
1403     public function increaseReplicationMasterId($count = 1)
1404     {
1405         $applicationController = Tinebase_Application::getInstance();
1406         $tinebase = $applicationController->getApplicationByName('Tinebase');
1407
1408         $state = $tinebase->xprops('state');
1409         if (!isset($state[Tinebase_Model_Application::STATE_REPLICATION_MASTER_ID])) {
1410             $state[Tinebase_Model_Application::STATE_REPLICATION_MASTER_ID] = 0;
1411         }
1412
1413         $state[Tinebase_Model_Application::STATE_REPLICATION_MASTER_ID] += intval($count);
1414
1415         $applicationController->updateApplication($tinebase);
1416     }
1417 }