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