9604: notes are visible even if they have is_deleted = 1
[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      * @param boolean $ignoreACL
154      * @return Tinebase_Record_RecordSet subtype Tinebase_Model_Note
155      */
156     public function searchNotes(Tinebase_Model_NoteFilter $_filter, Tinebase_Model_Pagination $_pagination = NULL, $ignoreACL = true)
157     {
158         $select = $this->_db->select()
159             ->from(array('notes' => SQL_TABLE_PREFIX . 'notes'))
160             ->where($this->_db->quoteIdentifier('is_deleted') . ' = 0');
161         
162         if (! $ignoreACL) {
163             $this->_checkFilterACL($_filter);
164         }
165         
166         Tinebase_Backend_Sql_Filter_FilterGroup::appendFilters($select, $_filter, $this);
167         if ($_pagination !== NULL) {
168             $_pagination->appendPaginationSql($select);
169         }
170         
171         $rows = $this->_db->fetchAssoc($select);
172         $result = new Tinebase_Record_RecordSet('Tinebase_Model_Note', $rows, true);
173
174         return $result;
175     }
176     
177     /**
178      * checks acl of filter
179      * 
180      * @param Tinebase_Model_NoteFilter $noteFilter
181      * @throws Tinebase_Exception_AccessDenied
182      */
183     protected function _checkFilterACL(Tinebase_Model_NoteFilter $noteFilter)
184     {
185         $recordModelFilter = $noteFilter->getFilter('record_model');
186         if (empty($recordModelFilter)) {
187             throw new Tinebase_Exception_AccessDenied('record model filter required');
188         }
189         
190         $recordIdFilter = $noteFilter->getFilter('record_id');
191         if (empty($recordIdFilter) || $recordIdFilter->getOperator() !== 'equals') {
192             throw new Tinebase_Exception_AccessDenied('record id filter required or wrong operator');
193         }
194         
195         $recordModel = $recordModelFilter->getValue();
196         if (! is_string($recordModel)) {
197             throw new Tinebase_Exception_AccessDenied('no explicit record model set in filter');
198         }
199         
200         try {
201             $record = Tinebase_Core::getApplicationInstance($recordModel)->get($recordIdFilter->getValue());
202         } catch (Tinebase_Exception_AccessDenied $tead) {
203             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Do not fetch record notes because user has no read grant for container');
204             $recordIdFilter->setValue('');
205         }
206     }
207     
208     /**
209      * count notes
210      *
211      * @param Tinebase_Model_NoteFilter $_filter
212      * @param boolean $ignoreACL
213      * @return int notes count
214      */
215     public function searchNotesCount(Tinebase_Model_NoteFilter $_filter, $ignoreACL = true)
216     {
217         $select = $this->_db->select()
218             ->from(array('notes' => SQL_TABLE_PREFIX . 'notes'), array('count' => 'COUNT(' . $this->_db->quoteIdentifier('id') . ')'))
219             ->where($this->_db->quoteIdentifier('is_deleted') . ' = 0');
220         
221         if (! $ignoreACL) {
222             $this->_checkFilterACL($_filter);
223         }
224         
225         Tinebase_Backend_Sql_Filter_FilterGroup::appendFilters($select, $_filter, $this);
226         
227         $result = $this->_db->fetchOne($select);
228         return $result;
229     }
230     
231     /**
232      * get a single note
233      *
234      * @param   string $_noteId
235      * @return  Tinebase_Model_Note
236      * @throws  Tinebase_Exception_NotFound
237      */
238     public function getNote($_noteId)
239     {
240         $row = $this->_notesTable->fetchRow($this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', (string) $_noteId));
241         
242         if (!$row) {
243             throw new Tinebase_Exception_NotFound('Note not found.');
244         }
245         
246         return new Tinebase_Model_Note($row->toArray());
247     }
248     
249     /**
250      * get all notes of a given record (calls searchNotes)
251      * 
252      * @param  string $_model     model of record
253      * @param  string $_id        id of record
254      * @param  string $_backend   backend of record
255      * @param  boolean $_onlyNonSystemNotes get only non-system notes per default
256      * @return Tinebase_Record_RecordSet of Tinebase_Model_Note
257      */
258     public function getNotesOfRecord($_model, $_id, $_backend = 'Sql', $_onlyNonSystemNotes = TRUE)
259     {
260         $backend = ucfirst(strtolower($_backend));
261
262         $filter = $this->_getNotesFilter($_id, $_model, $backend, $_onlyNonSystemNotes);
263         
264         $pagination = new Tinebase_Model_Pagination(array(
265             'limit' => Tinebase_Notes::NUMBER_RECORD_NOTES,
266             'sort'  => 'creation_time',
267             'dir'   => 'DESC'
268         ));
269         
270         $result = $this->searchNotes($filter, $pagination);
271             
272         return $result;
273     }
274     
275     /**
276      * get all notes of all given records (calls searchNotes)
277      * 
278      * @param  Tinebase_Record_RecordSet  $_records       the recordSet
279      * @param  string                     $_notesProperty  the property in the record where the notes are in (defaults: 'notes')
280      * @param  string                     $_backend   backend of record
281      * @return void
282      */
283     public function getMultipleNotesOfRecords($_records, $_notesProperty = 'notes', $_backend = 'Sql', $_onlyNonSystemNotes = TRUE)
284     {
285         if (count($_records) == 0) {
286             return;
287         }
288         
289         $modelName = $_records->getRecordClassName();
290         $filter = $this->_getNotesFilter($_records->getArrayOfIds(), $modelName, $_backend, $_onlyNonSystemNotes);
291         
292         // search and add index
293         $notesOfRecords = $this->searchNotes($filter);
294         $notesOfRecords->addIndices(array('record_id'));
295         
296         // add notes to records
297         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Getting ' . count($notesOfRecords) . ' notes for ' . count($_records) . ' records.');
298         foreach($_records as $record) {
299             //$record->notes = Tinebase_Notes::getInstance()->getNotesOfRecord($modelName, $record->getId(), $_backend);
300             $record->{$_notesProperty} = $notesOfRecords->filter('record_id', $record->getId());
301         }
302     }
303     
304     /************************** set / add / delete notes ************************/
305     
306     /**
307      * sets notes of a record
308      * 
309      * @param Tinebase_Record_Abstract  $_record            the record object
310      * @param string                    $_backend           backend (default: 'Sql')
311      * @param string                    $_notesProperty     the property in the record where the tags are in (default: 'notes')
312      * 
313      * @todo add update notes ?
314      */
315     public function setNotesOfRecord($_record, $_backend = 'Sql', $_notesProperty = 'notes')
316     {
317         $model = get_class($_record);
318         $backend = ucfirst(strtolower($_backend));
319         
320         //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_record->toArray(), TRUE));
321         
322         $currentNotesIds = $this->getNotesOfRecord($model, $_record->getId(), $backend)->getArrayOfIds();
323         $notes = $_record->$_notesProperty;
324         
325         if ($notes instanceOf Tinebase_Record_RecordSet) {
326             $notesToSet = $notes;
327         } else {
328             if (count($notes) > 0 && $notes[0] instanceOf Tinebase_Record_Abstract) {
329                 // array of notes records given
330                 $notesToSet = new Tinebase_Record_RecordSet('Tinebase_Model_Note', $notes);
331             } else {
332                 // array of arrays given
333                 $notesToSet = new Tinebase_Record_RecordSet('Tinebase_Model_Note');
334                 foreach($notes as $noteData) {
335                     if (!empty($noteData)) {
336                         $noteArray = (!is_array($noteData)) ? array('note' => $noteData) : $noteData;
337                         if (!isset($noteArray['note_type_id'])) {
338                             // get default note type
339                             $defaultNote = $this->getNoteTypeByName('note');
340                             $noteArray['note_type_id'] = $defaultNote->getId();
341                         }
342                         try {
343                             $note = new Tinebase_Model_Note($noteArray);
344                             $notesToSet->addRecord($note);
345                             
346                         } catch (Tinebase_Exception_Record_Validation $terv) {
347                             // discard invalid notes here
348                             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ 
349                                 . ' Note is invalid! '
350                                 . $terv->getMessage()
351                                 //. print_r($noteArray, TRUE)
352                             );
353                         }
354                     }
355                 }
356                 
357             }
358         }
359         
360         //$toAttach = array_diff($notesToSet->getArrayOfIds(), $currentNotesIds);
361         $toDetach = array_diff($currentNotesIds, $notesToSet->getArrayOfIds());
362
363         // delete detached/deleted notes
364         $this->deleteNotes($toDetach);
365         
366         // add new notes
367         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Adding ' . count($notesToSet) . ' note(s) to record.');
368         foreach ($notesToSet as $note) {
369             //if (in_array($note->getId(), $toAttach)) {
370             if (!$note->getId()) {
371                 $note->record_model = $model;
372                 $note->record_backend = $backend;
373                 $note->record_id = $_record->getId();
374                 $this->addNote($note);
375             }
376         }
377     }
378     
379     /**
380      * add new note
381      *
382      * @param Tinebase_Model_Note $_note
383      */
384     public function addNote(Tinebase_Model_Note $_note)
385     {
386         if (!$_note->getId()) {
387             $id = $_note->generateUID();
388             $_note->setId($id);
389         }
390
391         Tinebase_Timemachine_ModificationLog::getInstance()->setRecordMetaData($_note, 'create');
392         
393         $data = $_note->toArray(FALSE, FALSE);
394         
395         $this->_notesTable->insert($data);
396     }
397
398     /**
399      * add new system note
400      *
401      * @param Tinebase_Record_Abstract|string $_record
402      * @param string|Tinebase_Mode_User $_userId
403      * @param string $_type (created|changed)
404      * @param Tinebase_Record_RecordSet|string $_mods (Tinebase_Model_ModificationLog)
405      * @param string $_backend   backend of record
406      * @return Tinebase_Model_Note|boolean
407      * 
408      * @todo get field translations from application?
409      * @todo attach modlog record (id) to note instead of saving an ugly string
410      */
411     public function addSystemNote($_record, $_userId = NULL, $_type = Tinebase_Model_Note::SYSTEM_NOTE_NAME_CREATED, $_mods = NULL, $_backend = 'Sql', $_modelName = NULL)
412     {
413         if (empty($_mods) && $_type === Tinebase_Model_Note::SYSTEM_NOTE_NAME_CHANGED) {
414             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ .' Nothing changed -> do not add "changed" note.');
415             return FALSE;
416         }
417         
418         $id = ($_record instanceof Tinebase_Record_Abstract) ? $_record->getId() : $_record;
419         $modelName = ($_modelName !== NULL) ? $_modelName : (($_record instanceof Tinebase_Record_Abstract) ? get_class($_record) : 'unknown');
420         if (($_userId === NULL)) {
421             $_userId = Tinebase_Core::getUser();
422         }
423         $user = ($_userId instanceof Tinebase_Model_User) ? $_userId : Tinebase_User::getInstance()->getUserById($_userId);
424         
425         $translate = Tinebase_Translation::getTranslation('Tinebase');
426         $noteText = $translate->_($_type) . ' ' . $translate->_('by') . ' ' . $user->accountDisplayName;
427         
428         if ($_mods !== NULL) {
429             if ($_mods instanceof Tinebase_Record_RecordSet && count($_mods) > 0) {
430                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
431                     .' mods to log: ' . print_r($_mods->toArray(), TRUE));
432                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
433                     .' Adding "' . $_type . '" system note note to record (id ' . $id . ')');
434                 
435                 $noteText .= ' | ' .$translate->_('Changed fields:');
436                 foreach ($_mods as $mod) {
437                     $noteText .= ' ' . $translate->_($mod->modified_attribute) .' (' . $this->_getSystemNoteChangeText($mod) . ')';
438                 }
439             } else if (is_string($_mods)) {
440                 $noteText = $_mods;
441             }
442         }
443         
444         $noteType = $this->getNoteTypeByName($_type);
445         $note = new Tinebase_Model_Note(array(
446             'note_type_id'      => $noteType->getId(),
447             'note'              => substr($noteText, 0, self::MAX_NOTE_LENGTH),
448             'record_model'      => $modelName,
449             'record_backend'    => ucfirst(strtolower($_backend)),
450             'record_id'         => $id,
451         ));
452         
453         return $this->addNote($note);
454     }
455     
456     /**
457      * get system note change text
458      * 
459      * @param Tinebase_Model_ModificationLog $modification
460      * @return string
461      */
462     protected function _getSystemNoteChangeText(Tinebase_Model_ModificationLog $modification)
463     {
464         // check if $modification->new_value is json string and record set diff
465         // @see 0008546: When edit event, history show "code" ...
466         if (is_json($modification->new_value)) {
467             $newValueArray = Zend_Json::decode($modification->new_value);
468             if ((isset($newValueArray['model']) || array_key_exists('model', $newValueArray)) && (isset($newValueArray['added']) || array_key_exists('added', $newValueArray))) {
469                 $diff = new Tinebase_Record_RecordSetDiff($newValueArray);
470                 
471                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
472                     .' fetching translated text for diff: ' . print_r($diff->toArray(), true));
473                 
474                 return $diff->getTranslatedDiffText();
475             }
476         }
477         
478         return $modification->old_value . ' -> ' . $modification->new_value;
479     }
480     
481     /**
482      * add multiple modification system nodes
483      * 
484      * @param Tinebase_Record_RecordSet $_mods
485      * @param string $_userId
486      * @param string $modelName
487      */
488     public function addMultipleModificationSystemNotes($_mods, $_userId, $modelName = null)
489     {
490         $_mods->addIndices(array('record_id'));
491         foreach ($_mods->record_id as $recordId) {
492             $modsOfRecord = $_mods->filter('record_id', $recordId);
493             $this->addSystemNote($recordId, $_userId, Tinebase_Model_Note::SYSTEM_NOTE_NAME_CHANGED, $modsOfRecord, 'Sql', $modelName);
494         }
495     }
496     
497     /**
498      * delete notes
499      *
500      * @param array $_noteIds
501      */
502     public function deleteNotes(array $_noteIds)
503     {
504         if (!empty($_noteIds)) {
505             $where = array($this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' IN (?)', $_noteIds));
506             $this->_notesTable->delete($where);
507         }
508     }
509
510     /**
511      * delete notes
512      *
513      * @param  string $_model     model of record
514      * @param  string $_backend   backend of record
515      * @param  string $_id        id of record
516      */
517     public function deleteNotesOfRecord($_model, $_backend, $_id)
518     {
519         $backend = ucfirst(strtolower($_backend));
520         
521         $notes = $this->getNotesOfRecord($_model, $_id, $backend);
522         
523         $this->deleteNotes($notes->getArrayOfIdsAsString());
524     }
525     
526     /**
527      * get note filter
528      * 
529      * @param string|array $_id
530      * @param string $_model
531      * @param string $_backend
532      * @param boolean|optional $onlyNonSystemNotes
533      * @return Tinebase_Model_NoteFilter
534      */
535     protected function _getNotesFilter($_id, $_model, $_backend, $_onlyNonSystemNotes = TRUE)
536     {
537         $backend = ucfirst(strtolower($_backend));
538         
539         $filter = new Tinebase_Model_NoteFilter(array(
540             array(
541                 'field' => 'record_model',
542                 'operator' => 'equals',
543                 'value' => $_model
544             ),
545             array(
546                 'field' => 'record_backend',
547                 'operator' => 'equals',
548                 'value' => $backend
549             ),
550             array(
551                 'field' => 'record_id',
552                 'operator' => 'in',
553                 'value' => (array) $_id
554             ),
555             array(
556                 'field' => 'note_type_id',
557                 'operator' => 'in',
558                 'value' => $this->getNoteTypes($_onlyNonSystemNotes)->getArrayOfIdsAsString()
559             )
560         ));
561         
562         return $filter;
563     }
564     
565     /************************** note types *******************/
566     
567     /**
568      * get all note types
569      *
570      * @param boolean|optional $onlyNonSystemNotes
571      * @return Tinebase_Record_RecordSet of Tinebase_Model_NoteType
572      */
573     public function getNoteTypes($onlyNonSystemNotes = FALSE)
574     {
575         $types = new Tinebase_Record_RecordSet('Tinebase_Model_NoteType');
576         foreach ($this->_noteTypesTable->fetchAll() as $type) {
577             if (!$onlyNonSystemNotes || $type->is_user_type) {
578                 $types->addRecord(new Tinebase_Model_NoteType($type->toArray(), true));
579             }
580         }
581         return $types;
582     }
583
584     /**
585      * get note type by name
586      *
587      * @param string $_name
588      * @return Tinebase_Model_NoteType
589      * @throws  Tinebase_Exception_NotFound
590      */
591     public function getNoteTypeByName($_name)
592     {
593         $row = $this->_noteTypesTable->fetchRow($this->_db->quoteInto($this->_db->quoteIdentifier('name') . ' = ?', $_name));
594         
595         if (!$row) {
596             throw new Tinebase_Exception_NotFound('Note type not found.');
597         }
598         
599         return new Tinebase_Model_NoteType($row->toArray());
600     }
601     
602     /**
603      * add new note type
604      *
605      * @param Tinebase_Model_NoteType $_noteType
606      */
607     public function addNoteType(Tinebase_Model_NoteType $_noteType)
608     {
609         if (!$_noteType->getId()) {
610             $id = $_noteType->generateUID();
611             $_noteType->setId($id);
612         }
613         
614         $data = $_noteType->toArray();
615
616         $this->_noteTypesTable->insert($data);
617     }
618
619     /**
620      * update note type
621      *
622      * @param Tinebase_Model_NoteType $_noteType
623      */
624     public function updateNoteType(Tinebase_Model_NoteType $_noteType)
625     {
626         $data = $_noteType->toArray();
627
628         $where  = array(
629             $this->_noteTypesTable->getAdapter()->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $_noteType->getId()),
630         );
631         
632         $this->_noteTypesTable->update($data, $where);
633     }
634     
635     /**
636      * delete note type
637      *
638      * @param integer $_noteTypeId
639      */
640     public function deleteNoteType($_noteTypeId)
641     {
642         $this->_noteTypesTable->delete($this->_db->quoteInto($this->_db->quoteIdentifier('id') . ' = ?', $_noteTypeId));
643     }
644     
645 }