65487ff23178951ec84364b51527bc289221f9ac
[tine20] / tine20 / Tinebase / Notes.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Tinebase
6  * @subpackage  Notes
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2008 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Philipp Schuele <p.schuele@metaways.de>
10  * 
11  * @todo        delete notes completely or just set the is_deleted flag?
12  */
13
14 /**
15  * Class for handling notes
16  * 
17  * @package     Tinebase
18  * @subpackage  Notes 
19  */
20 class Tinebase_Notes implements Tinebase_Backend_Sql_Interface 
21 {
22     /**
23      * @var Zend_Db_Adapter_Pdo_Mysql
24      */
25     protected $_db;
26
27     /**
28      * @var Tinebase_Db_Table
29      */
30     protected $_notesTable;
31     
32     /**
33      * @var Tinebase_Db_Table
34      */
35     protected $_noteTypesTable;
36     
37     /**
38      * default record backend
39      */
40     const DEFAULT_RECORD_BACKEND = 'Sql';
41     
42     /**
43      * number of notes per record for activities panel
44      * (NOT the tab panel)
45      */
46     const NUMBER_RECORD_NOTES = 8;
47
48     /**
49      * max length of note text
50      * 
51      * @var integer
52      */
53     const MAX_NOTE_LENGTH = 10000;
54     
55     /**
56      * don't clone. Use the singleton.
57      */
58     private function __clone()
59     {
60         
61     }
62
63     /**
64      * holds the instance of the singleton
65      *
66      * @var Tinebase_Notes
67      */
68     private static $_instance = NULL;
69         
70     /**
71      * the singleton pattern
72      *
73      * @return Tinebase_Notes
74      */
75     public static function getInstance() 
76     {
77         if (self::$_instance === NULL) {
78             self::$_instance = new Tinebase_Notes;
79         }
80         
81         return self::$_instance;
82     }
83
84     /**
85      * the private constructor
86      *
87      */
88     private function __construct()
89     {
90
91         $this->_db = Tinebase_Core::getDb();
92         
93         $this->_notesTable = new Tinebase_Db_Table(array(
94             'name' => SQL_TABLE_PREFIX . 'notes',
95             'primary' => 'id'
96         ));
97         
98         $this->_noteTypesTable = new Tinebase_Db_Table(array(
99             'name' => SQL_TABLE_PREFIX . 'note_types',
100             'primary' => 'id'
101         ));
102     }
103     
104     /************************** sql backend interface ************************/
105     
106     /**
107      * get table name
108      *
109      * @return string
110      */
111     public function getTableName()
112     {
113         return 'notes';
114     }
115     
116     /**
117      * get table prefix
118      *
119      * @return string
120      */
121     public function getTablePrefix()
122     {
123         return $this->_db->table_prefix;
124     }
125     
126     /**
127      * get db adapter
128      *
129      * @return Zend_Db_Adapter_Abstract
130      */
131     public function getAdapter()
132     {
133         return $this->_db;
134     }
135     
136     /**
137      * returns the db schema
138      * 
139      * @return array
140      */
141     public function getSchema()
142     {
143         return Tinebase_Db_Table::getTableDescriptionFromCache(SQL_TABLE_PREFIX . 'notes', $this->_db);
144     }
145     
146     /************************** get notes ************************/
147
148     /**
149      * search for notes
150      *
151      * @param Tinebase_Model_NoteFilter $_filter
152      * @param Tinebase_Model_Pagination $_pagination
153      * @return Tinebase_Record_RecordSet subtype Tinebase_Model_Note
154      */
155     public function searchNotes(Tinebase_Model_NoteFilter $_filter, Tinebase_Model_Pagination $_pagination = NULL)
156     {
157         $select = $this->_db->select()
158             ->from(array('notes' => SQL_TABLE_PREFIX . 'notes'));
159         
160         Tinebase_Backend_Sql_Filter_FilterGroup::appendFilters($select, $_filter, $this);
161         if ($_pagination !== NULL) {
162             $_pagination->appendPaginationSql($select);
163         }
164         
165         //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $select->__toString());
166         
167         $rows = $this->_db->fetchAssoc($select);
168         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Note', $rows, true);
169
170         return $result;
171     }
172     
173     /**
174      * count notes
175      *
176      * @param Tinebase_Model_NoteFilter $_filter
177      * @return int notes count
178      */
179     public function searchNotesCount(Tinebase_Model_NoteFilter $_filter)
180     {
181         $select = $this->_db->select()
182             ->from(array('notes' => SQL_TABLE_PREFIX . 'notes'), array('count' => 'COUNT(' . $this->_db->quoteIdentifier('id') . ')'));
183         
184         Tinebase_Backend_Sql_Filter_FilterGroup::appendFilters($select, $_filter, $this);
185         //$_filter->appendFilterSql($select);
186         
187         $result = $this->_db->fetchOne($select);
188         return $result;
189     }
190     
191     /**
192      * get a single note
193      *
194      * @param   string $_noteId
195      * @return  Tinebase_Model_Note
196      * @throws  Tinebase_Exception_NotFound
197      */
198     public function getNote($_noteId)
199     {
200         $row = $this->_notesTable->fetchRow($this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', (string) $_noteId));
201         
202         if (!$row) {
203             throw new Tinebase_Exception_NotFound('Note not found.');
204         }
205         
206         return new Tinebase_Model_Note($row->toArray());
207     }
208     
209     /**
210      * get all notes of a given record (calls searchNotes)
211      * 
212      * @param  string $_model     model of record
213      * @param  string $_id        id of record
214      * @param  string $_backend   backend of record
215      * @param  boolean $_onlyNonSystemNotes get only non-system notes per default
216      * @return Tinebase_Record_RecordSet of Tinebase_Model_Note
217      */
218     public function getNotesOfRecord($_model, $_id, $_backend = 'Sql', $_onlyNonSystemNotes = TRUE)
219     {
220         $backend = ucfirst(strtolower($_backend));
221
222         $filter = $this->_getNotesFilter($_id, $_model, $backend, $_onlyNonSystemNotes);
223         
224         $pagination = new Tinebase_Model_Pagination(array(
225             'limit' => Tinebase_Notes::NUMBER_RECORD_NOTES,
226             'sort'  => 'creation_time',
227             'dir'   => 'DESC'
228         ));
229         
230         $result = $this->searchNotes($filter, $pagination);
231             
232         return $result;
233     }
234     
235     /**
236      * get all notes of all given records (calls searchNotes)
237      * 
238      * @param  Tinebase_Record_RecordSet  $_records       the recordSet
239      * @param  string                     $_notesProperty  the property in the record where the notes are in (defaults: 'notes')
240      * @param  string                     $_backend   backend of record
241      * @return void
242      */
243     public function getMultipleNotesOfRecords($_records, $_notesProperty = 'notes', $_backend = 'Sql', $_onlyNonSystemNotes = TRUE)
244     {
245         if (count($_records) == 0) {
246             return;
247         }
248         
249         $modelName = $_records->getRecordClassName();
250         $filter = $this->_getNotesFilter($_records->getArrayOfIds(), $modelName, $_backend, $_onlyNonSystemNotes);
251         
252         // search and add index
253         $notesOfRecords = $this->searchNotes($filter);
254         $notesOfRecords->addIndices(array('record_id'));
255         
256         // add notes to records
257         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Getting ' . count($notesOfRecords) . ' notes for ' . count($_records) . ' records.');
258         foreach($_records as $record) {
259             //$record->notes = Tinebase_Notes::getInstance()->getNotesOfRecord($modelName, $record->getId(), $_backend);
260             $record->{$_notesProperty} = $notesOfRecords->filter('record_id', $record->getId());
261         }
262     }
263     
264     /************************** set / add / delete notes ************************/
265     
266     /**
267      * sets notes of a record
268      * 
269      * @param Tinebase_Record_Abstract  $_record            the record object
270      * @param string                    $_backend           backend (default: 'Sql')
271      * @param string                    $_notesProperty     the property in the record where the tags are in (default: 'notes')
272      * 
273      * @todo add update notes ?
274      */
275     public function setNotesOfRecord($_record, $_backend = 'Sql', $_notesProperty = 'notes')
276     {
277         $model = get_class($_record);
278         $backend = ucfirst(strtolower($_backend));
279         
280         //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_record->toArray(), TRUE));
281         
282         $currentNotesIds = $this->getNotesOfRecord($model, $_record->getId(), $backend)->getArrayOfIds();
283         $notes = $_record->$_notesProperty;
284         
285         if ($notes instanceOf Tinebase_Record_RecordSet) {
286             $notesToSet = $notes;
287         } else {
288             if (count($notes) > 0 && $notes[0] instanceOf Tinebase_Record_Abstract) {
289                 // array of notes records given
290                 $notesToSet = new Tinebase_Record_RecordSet('Tinebase_Model_Note', $notes);
291             } else {
292                 // array of arrays given
293                 $notesToSet = new Tinebase_Record_RecordSet('Tinebase_Model_Note');
294                 foreach($notes as $noteData) {
295                     if (!empty($noteData)) {
296                         $noteArray = (!is_array($noteData)) ? array('note' => $noteData) : $noteData;
297                         if (!isset($noteArray['note_type_id'])) {
298                             // get default note type
299                             $defaultNote = $this->getNoteTypeByName('note');
300                             $noteArray['note_type_id'] = $defaultNote->getId();
301                         }
302                         try {
303                             $note = new Tinebase_Model_Note($noteArray);
304                             $notesToSet->addRecord($note);
305                             
306                         } catch (Tinebase_Exception_Record_Validation $terv) {
307                             // discard invalid notes here
308                             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ 
309                                 . ' Note is invalid! '
310                                 . $terv->getMessage()
311                                 //. print_r($noteArray, TRUE)
312                             );
313                         }
314                     }
315                 }
316                 
317             }
318         }
319         
320         //$toAttach = array_diff($notesToSet->getArrayOfIds(), $currentNotesIds);
321         $toDetach = array_diff($currentNotesIds, $notesToSet->getArrayOfIds());
322
323         // delete detached/deleted notes
324         $this->deleteNotes($toDetach);
325         
326         // add new notes
327         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Adding ' . count($notesToSet) . ' note(s) to record.');
328         foreach ($notesToSet as $note) {
329             //if (in_array($note->getId(), $toAttach)) {
330             if (!$note->getId()) {
331                 $note->record_model = $model;
332                 $note->record_backend = $backend;
333                 $note->record_id = $_record->getId();
334                 $this->addNote($note);
335             }
336         }
337     }
338     
339     /**
340      * add new note
341      *
342      * @param Tinebase_Model_Note $_note
343      */
344     public function addNote(Tinebase_Model_Note $_note)
345     {
346         if (!$_note->getId()) {
347             $id = $_note->generateUID();
348             $_note->setId($id);
349         }
350
351         Tinebase_Timemachine_ModificationLog::getInstance()->setRecordMetaData($_note, 'create');
352         
353         $data = $_note->toArray(FALSE, FALSE);
354         
355         $this->_notesTable->insert($data);
356     }
357
358     /**
359      * add new system note
360      *
361      * @param Tinebase_Record_Abstract|string $_record
362      * @param string|Tinebase_Mode_User $_userId
363      * @param string $_type (created|changed)
364      * @param Tinebase_Record_RecordSet|string $_mods (Tinebase_Model_ModificationLog)
365      * @param string $_backend   backend of record
366      * @return Tinebase_Model_Note|boolean
367      * 
368      * @todo get field translations from application?
369      * @todo attach modlog record (id) to note instead of saving an ugly string
370      */
371     public function addSystemNote($_record, $_userId = NULL, $_type = Tinebase_Model_Note::SYSTEM_NOTE_NAME_CREATED, $_mods = NULL, $_backend = 'Sql', $_modelName = NULL)
372     {
373         if (empty($_mods) && $_type === Tinebase_Model_Note::SYSTEM_NOTE_NAME_CHANGED) {
374             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ .' Nothing changed -> do not add "changed" note.');
375             return FALSE;
376         }
377         
378         $id = ($_record instanceof Tinebase_Record_Abstract) ? $_record->getId() : $_record;
379         $modelName = ($_modelName !== NULL) ? $_modelName : (($_record instanceof Tinebase_Record_Abstract) ? get_class($_record) : 'unknown');
380         if (($_userId === NULL)) {
381             $_userId = Tinebase_Core::getUser();
382         }
383         $user = ($_userId instanceof Tinebase_Model_User) ? $_userId : Tinebase_User::getInstance()->getUserById($_userId);
384         
385         $translate = Tinebase_Translation::getTranslation('Tinebase');
386         $noteText = $translate->_($_type) . ' ' . $translate->_('by') . ' ' . $user->accountDisplayName;
387         
388         if ($_mods !== NULL) {
389             if ($_mods instanceof Tinebase_Record_RecordSet && count($_mods) > 0) {
390                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
391                     .' mods to log: ' . print_r($_mods->toArray(), TRUE));
392                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
393                     .' Adding "' . $_type . '" system note note to record (id ' . $id . ')');
394                 
395                 $noteText .= ' | ' .$translate->_('Changed fields:');
396                 foreach ($_mods as $mod) {
397                     $noteText .= ' ' . $translate->_($mod->modified_attribute) .' (' . $this->_getSystemNoteChangeText($mod) . ')';
398                 }
399             } else if (is_string($_mods)) {
400                 $noteText = $_mods;
401             }
402         }
403         
404         $noteType = $this->getNoteTypeByName($_type);
405         $note = new Tinebase_Model_Note(array(
406             'note_type_id'      => $noteType->getId(),
407             'note'              => substr($noteText, 0, self::MAX_NOTE_LENGTH),
408             'record_model'      => $modelName,
409             'record_backend'    => ucfirst(strtolower($_backend)),
410             'record_id'         => $id,
411         ));
412         
413         return $this->addNote($note);
414     }
415     
416     /**
417      * get system note change text
418      * 
419      * @param Tinebase_Model_ModificationLog $modification
420      * @return string
421      */
422     protected function _getSystemNoteChangeText(Tinebase_Model_ModificationLog $modification)
423     {
424         // check if $modification->new_value is json string and record set diff
425         // @see 0008546: When edit event, history show "code" ...
426         if (is_json($modification->new_value)) {
427             $newValueArray = Zend_Json::decode($modification->new_value);
428             if ((isset($newValueArray['model']) || array_key_exists('model', $newValueArray)) && (isset($newValueArray['added']) || array_key_exists('added', $newValueArray))) {
429                 $diff = new Tinebase_Record_RecordSetDiff($newValueArray);
430                 
431                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
432                     .' fetching translated text for diff: ' . print_r($diff->toArray(), true));
433                 
434                 return $diff->getTranslatedDiffText();
435             }
436         }
437         
438         return $modification->old_value . ' -> ' . $modification->new_value;
439     }
440     
441     /**
442      * add multiple modification system nodes
443      * 
444      * @param Tinebase_Record_RecordSet $_mods
445      * @param string $_userId
446      */
447     public function addMultipleModificationSystemNotes($_mods, $_userId)
448     {
449         $_mods->addIndices(array('record_id'));
450         foreach ($_mods->record_id as $recordId) {
451             $modsOfRecord = $_mods->filter('record_id', $recordId);
452             $this->addSystemNote($recordId, $_userId, Tinebase_Model_Note::SYSTEM_NOTE_NAME_CHANGED, $modsOfRecord);
453         }
454     }
455     
456     /**
457      * delete notes
458      *
459      * @param array $_noteIds
460      */
461     public function deleteNotes(array $_noteIds)
462     {
463         if (!empty($_noteIds)) {
464             $where = array($this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' IN (?)', $_noteIds));
465             $this->_notesTable->delete($where);
466         }
467     }
468
469     /**
470      * delete notes
471      *
472      * @param  string $_model     model of record
473      * @param  string $_backend   backend of record
474      * @param  string $_id        id of record
475      */
476     public function deleteNotesOfRecord($_model, $_backend, $_id)
477     {
478         $backend = ucfirst(strtolower($_backend));
479         
480         $notes = $this->getNotesOfRecord($_model, $_id, $backend);
481         
482         $this->deleteNotes($notes->getArrayOfIdsAsString());
483     }
484     
485     /**
486      * get note filter
487      * 
488      * @param string|array $_id
489      * @param string $_model
490      * @param string $_backend
491      * @param boolean|optional $onlyNonSystemNotes
492      * @return Tinebase_Model_NoteFilter
493      */
494     protected function _getNotesFilter($_id, $_model, $_backend, $_onlyNonSystemNotes = TRUE)
495     {
496         $backend = ucfirst(strtolower($_backend));
497         
498         $filter = new Tinebase_Model_NoteFilter(array(
499             array(
500                 'field' => 'record_model',
501                 'operator' => 'equals',
502                 'value' => $_model
503             ),
504             array(
505                 'field' => 'record_backend',
506                 'operator' => 'equals',
507                 'value' => $backend
508             ),
509             array(
510                 'field' => 'record_id',
511                 'operator' => 'in',
512                 'value' => (array) $_id
513             ),
514             array(
515                 'field' => 'note_type_id',
516                 'operator' => 'in',
517                 'value' => $this->getNoteTypes($_onlyNonSystemNotes)->getArrayOfIdsAsString()
518             )
519         ));
520         
521         return $filter;
522     }
523     
524     /************************** note types *******************/
525     
526     /**
527      * get all note types
528      *
529      * @param boolean|optional $onlyNonSystemNotes
530      * @return Tinebase_Record_RecordSet of Tinebase_Model_NoteType
531      */
532     public function getNoteTypes($onlyNonSystemNotes = FALSE)
533     {
534         $types = new Tinebase_Record_RecordSet('Tinebase_Model_NoteType');
535         foreach ($this->_noteTypesTable->fetchAll() as $type) {
536             if (!$onlyNonSystemNotes || $type->is_user_type) {
537                 $types->addRecord(new Tinebase_Model_NoteType($type->toArray(), true));
538             }
539         }
540         return $types;
541     }
542
543     /**
544      * get note type by name
545      *
546      * @param string $_name
547      * @return Tinebase_Model_NoteType
548      * @throws  Tinebase_Exception_NotFound
549      */
550     public function getNoteTypeByName($_name)
551     {
552         $row = $this->_noteTypesTable->fetchRow($this->_db->quoteInto($this->_db->quoteIdentifier('name') . ' = ?', $_name));
553         
554         if (!$row) {
555             throw new Tinebase_Exception_NotFound('Note type not found.');
556         }
557         
558         return new Tinebase_Model_NoteType($row->toArray());
559     }
560     
561     /**
562      * add new note type
563      *
564      * @param Tinebase_Model_NoteType $_noteType
565      */
566     public function addNoteType(Tinebase_Model_NoteType $_noteType)
567     {
568         if (!$_noteType->getId()) {
569             $id = $_noteType->generateUID();
570             $_noteType->setId($id);
571         }
572         
573         $data = $_noteType->toArray();
574
575         $this->_noteTypesTable->insert($data);
576     }
577
578     /**
579      * update note type
580      *
581      * @param Tinebase_Model_NoteType $_noteType
582      */
583     public function updateNoteType(Tinebase_Model_NoteType $_noteType)
584     {
585         $data = $_noteType->toArray();
586
587         $where  = array(
588             $this->_noteTypesTable->getAdapter()->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $_noteType->getId()),
589         );
590         
591         $this->_noteTypesTable->update($data, $where);
592     }
593     
594     /**
595      * delete note type
596      *
597      * @param integer $_noteTypeId
598      */
599     public function deleteNoteType($_noteTypeId)
600     {
601         $this->_noteTypesTable->delete($this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $_noteTypeId));
602     }
603     
604 }