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