Display tab-based line indentation in HTML
[grauphel.git] / lib / notestorage.php
1 <?php
2 /**
3  * Part of grauphel
4  *
5  * PHP version 5
6  *
7  * @category  Tools
8  * @package   Grauphel
9  * @author    Christian Weiske <cweiske@cweiske.de>
10  * @copyright 2014 Christian Weiske
11  * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL v3
12  * @link      http://cweiske.de/grauphel.htm
13  */
14 namespace OCA\Grauphel\Lib;
15
16 /**
17  * Flat file storage for notes
18  *
19  * @category  Tools
20  * @package   Grauphel
21  * @author    Christian Weiske <cweiske@cweiske.de>
22  * @copyright 2014 Christian Weiske
23  * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL v3
24  * @version   Release: @package_version@
25  * @link      http://cweiske.de/grauphel.htm
26  */
27 class NoteStorage
28 {
29     /**
30      * @var \OCP\IDBConnection
31      */
32     protected $db;
33
34     protected $urlGen;
35     protected $username;
36
37     public function __construct($urlGen)
38     {
39         $this->urlGen = $urlGen;
40         $this->db     = \OC::$server->getDatabaseConnection();
41     }
42
43     public function setUsername($username)
44     {
45         $this->username = $username;
46     }
47
48     /**
49      * Create a new sync data object for fresh users.
50      * Used by loadSyncData()
51      *
52      * @return SyncData New synchronization statistics
53      */
54     protected function getNewSyncData()
55     {
56         $syncdata = new SyncData();
57         $syncdata->initNew($this->username);
58         return $syncdata;
59     }
60
61     public function getTags()
62     {
63         $result = $this->db->executeQuery(
64             'SELECT `note_tags` FROM `*PREFIX*grauphel_notes`'
65             . ' WHERE note_user = ?',
66             array($this->username)
67         );
68
69         $tags = array();
70         while ($row = $result->fetch()) {
71             $tags = array_merge($tags, json_decode($row['note_tags']));
72         }
73         return array_unique($tags);
74     }
75
76     /**
77      * Updates the given $note object with data from $noteUpdate.
78      * Sets the last-sync-revision to $syncRevision
79      *
80      * @param object  $note         Original note object
81      * @param object  $noteUpdate   Update note object from a PUT to the API
82      * @param integer $syncRevision Current sync revision number
83      *
84      * @return void
85      */
86     public function update($note, $noteUpdate, $syncRevision)
87     {
88         static $updateFields = array(
89             'create-date',
90             'last-change-date',
91             'last-metadata-change-date',
92             'note-content',
93             'note-content-version',
94             'open-on-startup',
95             'pinned',
96             'tags',
97             'title',
98         );
99
100         $changed = array();
101         foreach ($updateFields as $field) {
102             $changed[$field] = false;
103             if (isset($noteUpdate->$field)) {
104                 if ($note->$field != $noteUpdate->$field) {
105                     $note->$field = $noteUpdate->$field;
106                     $changed[$field] = true;
107                 }
108             }
109         }
110
111         if (!isset($noteUpdate->{'last-change-date'})
112             && ($changed['title'] || $changed['note-content'])
113         ) {
114             //no idea how to get the microseconds in there
115             $note->{'last-change-date'} = date('c');
116         }
117
118         if (!isset($noteUpdate->{'last-metadata-change-date'})) {
119             //no idea how to get the microseconds in there
120             $note->{'last-metadata-change-date'} = date('c');
121         }
122
123         if (!isset($note->{'create-date'})) {
124             //no idea how to get the microseconds in there
125             $note->{'create-date'} = date('c');
126         }
127
128         if (isset($noteUpdate->{'node-content'})
129             && $note->{'note-content-version'} == 0
130         ) {
131             $note->{'note-content-version'} = 0.3;
132         }
133
134         $note->{'last-sync-revision'} = $syncRevision;
135     }
136
137     /**
138      * Loads synchronization data for the given user.
139      * Creates fresh sync data if there are none for the user.
140      *
141      * @return SyncData Synchronization statistics (revision, sync guid)
142      */
143     public function loadSyncData()
144     {
145         $row = $this->db->executeQuery(
146             'SELECT * FROM `*PREFIX*grauphel_syncdata`'
147             . ' WHERE `syncdata_user` = ?',
148             array($this->username)
149         )->fetch();
150
151         if ($row === false) {
152             $syncdata = $this->getNewSyncData();
153             $this->saveSyncData($syncdata);
154         } else {
155             $syncdata = new SyncData();
156             $syncdata->latestSyncRevision = (int) $row['syncdata_latest_sync_revision'];
157             $syncdata->currentSyncGuid    = $row['syncdata_current_sync_guid'];
158         }
159
160         return $syncdata;
161     }
162
163     /**
164      * Save synchronization data for the given user.
165      *
166      * @param SyncData $syncdata Synchronization data object
167      *
168      * @return void
169      */
170     public function saveSyncData(SyncData $syncdata)
171     {
172         $row = $this->db->executeQuery(
173             'SELECT * FROM `*PREFIX*grauphel_syncdata`'
174             . ' WHERE `syncdata_user` = ?',
175             array($this->username)
176         )->fetch();
177
178         if ($row === false) {
179             //INSERT
180             $sql = 'INSERT INTO `*PREFIX*grauphel_syncdata`'
181                 . '(`syncdata_user`, `syncdata_latest_sync_revision`, `syncdata_current_sync_guid`)'
182                 . ' VALUES(?, ?, ?)';
183             $params = array(
184                 $this->username,
185                 $syncdata->latestSyncRevision,
186                 $syncdata->currentSyncGuid
187             );
188         } else {
189             //UPDATE
190             $data = array(
191                 'syncdata_latest_sync_revision' => $syncdata->latestSyncRevision,
192                 'syncdata_current_sync_guid'    => $syncdata->currentSyncGuid,
193             );
194             $sql = 'UPDATE `*PREFIX*grauphel_syncdata` SET'
195                 . ' `' . implode('` = ?, `', array_keys($data)) . '` = ?'
196                 . ' WHERE `syncdata_user` = ?';
197             $params = array_values($data);
198             $params[] = $this->username;
199         }
200         $this->db->executeQuery($sql, $params);
201     }
202
203     /**
204      * Delete synchronization data for the given user.
205      *
206      * @param SyncData $syncdata Synchronization data object
207      *
208      * @return void
209      */
210     public function deleteSyncData()
211     {
212         $this->db->executeQuery(
213             'DELETE FROM `*PREFIX*grauphel_syncdata`'
214             . ' WHERE `syncdata_user` = ?',
215             array($this->username)
216         );
217     }
218
219     /**
220      * Load a note from the storage.
221      *
222      * @param string  $guid      Note identifier
223      * @param boolean $createNew Create a new note if it does not exist
224      *
225      * @return object Note object, NULL if !$createNew and note does not exist
226      */
227     public function load($guid, $createNew = true)
228     {
229         $row = $this->db->executeQuery(
230             'SELECT * FROM `*PREFIX*grauphel_notes`'
231             . ' WHERE `note_user` = ? AND `note_guid` = ?',
232             array($this->username, $guid)
233         )->fetch();
234
235         if ($row === false) {
236             if (!$createNew) {
237                 return null;
238             }
239             return (object) array(
240                 'guid' => $guid,
241
242                 'create-date'               => null,
243                 'last-change-date'          => null,
244                 'last-metadata-change-date' => null,
245
246                 'title'                => null,
247                 'note-content'         => null,
248                 'note-content-version' => 0.3,
249
250                 'open-on-startup' => false,
251                 'pinned'          => false,
252                 'tags'            => array(),
253             );
254         }
255
256         return $this->noteFromRow($row);
257     }
258
259     /**
260      * Load a GUID of a note by the note title.
261      *
262      * The note title is stored html-escaped in the database because we
263      * get it that way from tomboy. Thus we have to escape the search
264      * input, too.
265      *
266      * @param string $title Note title.
267      *
268      * @return string GUID, NULL if note could not be found
269      */
270     public function loadGuidByTitle($title)
271     {
272         $row = $this->db->executeQuery(
273             'SELECT note_guid FROM `*PREFIX*grauphel_notes`'
274             . ' WHERE `note_user` = ? AND `note_title` = ?',
275             array($this->username, htmlspecialchars($title))
276         )->fetch();
277
278         if ($row === false) {
279             return null;
280         }
281
282         return $row['note_guid'];
283     }
284
285     /**
286      * Search for a note
287      *
288      * @param array $keywords arrays of query strings within keys AND and NOT
289      *
290      * @return array Database rows with note_guid and note_title
291      */
292     public function search($keywordGroups)
293     {
294         if (!isset($keywordGroups['AND'])) {
295             $keywordGroups['AND'] = array();
296         }
297         if (!isset($keywordGroups['NOT'])) {
298             $keywordGroups['NOT'] = array();
299         }
300
301         $sqlTplAnd = ' AND (note_title ILIKE ? OR note_tags ILIKE ? OR note_content ILIKE ?)';
302         $sqlTplNot = ' AND NOT (note_title ILIKE ? OR note_tags ILIKE ? OR note_content ILIKE ?)';
303         $arData = array(
304             $this->username
305         );
306         foreach (array('AND', 'NOT') as $group) {
307             $keywords = $keywordGroups[$group];
308             foreach ($keywords as $keyword) {
309                 $arData[] = '%' . $keyword . '%';//title
310                 $arData[] = '%' . $keyword . '%';//tags
311                 $arData[] = '%' . $keyword . '%';//content
312             }
313         }
314
315         $result = $this->db->executeQuery(
316             'SELECT `note_guid`, `note_title`'
317             . ' FROM `*PREFIX*grauphel_notes`'
318             . ' WHERE note_user = ?'
319             . str_repeat($sqlTplAnd, count($keywordGroups['AND']))
320             . str_repeat($sqlTplNot, count($keywordGroups['NOT'])),
321             $arData
322         );
323
324         $notes = array();
325         while ($row = $result->fetch()) {
326             $notes[] = $row;
327         }
328         return $notes;
329     }
330
331     /**
332      * Save a note into storage.
333      *
334      * @param object $note Note to save
335      *
336      * @return void
337      */
338     public function save($note)
339     {
340         $row = $this->db->executeQuery(
341             'SELECT * FROM `*PREFIX*grauphel_notes`'
342             . ' WHERE `note_user` = ? AND `note_guid` = ?',
343             array($this->username, $note->guid)
344         )->fetch();
345
346         $data = $this->rowFromNote($note);
347         if ($row === false) {
348             //INSERT
349             $data['note_user'] = $this->username;
350             $sql = 'INSERT INTO `*PREFIX*grauphel_notes`'
351                 . ' (`' . implode('`, `', array_keys($data)) . '`)'
352                 . ' VALUES(' . implode(', ', array_fill(0, count($data), '?')) . ')';
353             $params = array_values($data);
354         } else {
355             //UPDATE
356             $sql = 'UPDATE `*PREFIX*grauphel_notes` SET '
357                 . '`' . implode('` = ?, `', array_keys($data)) . '` = ?'
358                 . ' WHERE `note_user` = ? AND `note_guid` = ?';
359             $params = array_values($data);
360             $params[] = $this->username;
361             $params[] = $note->guid;
362         }
363         $this->db->executeQuery($sql, $params);
364     }
365
366     /**
367      * Delete a note from storage.
368      *
369      * @param object $guid ID of the note
370      *
371      * @return void
372      */
373     public function delete($guid)
374     {
375         $this->db->executeQuery(
376             'DELETE FROM `*PREFIX*grauphel_notes`'
377             . ' WHERE `note_user` = ? AND `note_guid` = ?',
378             array($this->username, $guid)
379         );
380     }
381
382     /**
383      * Delete all notes from storage.
384      *
385      * @return void
386      */
387     public function deleteAll()
388     {
389         $this->db->executeQuery(
390             'DELETE FROM `*PREFIX*grauphel_notes`'
391             . ' WHERE `note_user` = ?',
392             array($this->username)
393         );
394     }
395
396     /**
397      * Load notes for the given user in short form.
398      * Optionally only those changed after $since revision
399      *
400      * @param integer $since       Revision number after which the notes changed
401      * @param string  $rawtag      Filter by tag. Special tags:
402      *                             - grauphel:special:all
403      *                             - grauphel:special:untagged
404      * @param boolean $includeDate Load the last modification date or not
405      *
406      * @return array Array of short note objects
407      */
408     public function loadNotesOverview(
409         $since = null, $rawtag = null, $includeDate = false
410     ) {
411         $sql = 'SELECT `note_guid`, `note_title`'
412             . ', `note_last_sync_revision`, `note_tags`'
413             . ', `note_last_change_date`'
414             . ' FROM `*PREFIX*grauphel_notes`'
415             . ' WHERE note_user = ?';
416         $sqlData = array($this->username);
417
418         if ($since !== null) {
419             $sqlData[] = $since;
420             $sql .= ' AND note_last_sync_revision > ?';
421         }
422
423         if ($rawtag == 'grauphel:special:all') {
424             $rawtag = null;
425         } else if ($rawtag == 'grauphel:special:untagged') {
426             $jsRawtag = json_encode(array());
427         } else {
428             $jsRawtag = json_encode($rawtag);
429         }
430         if ($rawtag !== null) {
431             $sqlData[] = '%' . $jsRawtag . '%';
432             $sql .= ' AND note_tags LIKE ?';
433         }
434
435         $result = $this->db->executeQuery($sql, $sqlData);
436         $notes = array();
437         while ($row = $result->fetch()) {
438             $note = array(
439                 'guid' => $row['note_guid'],
440                 'ref'  => array(
441                     'api-ref' => $this->urlGen->getAbsoluteURL(
442                         $this->urlGen->linkToRoute(
443                             'grauphel.api.note',
444                             array(
445                                 'username' => $this->username,
446                                 'guid' => $row['note_guid']
447                             )
448                         )
449                     ),
450                     'href' => $this->urlGen->getAbsoluteURL(
451                         $this->urlGen->linkToRoute(
452                             'grauphel.gui.note',
453                             array(
454                                 'guid' => $row['note_guid']
455                             )
456                         )
457                     ),
458                 ),
459                 'title' => $row['note_title'],
460             );
461             if ($includeDate) {
462                 $note['last-change-date'] = $row['note_last_change_date'];
463             }
464             $notes[] = $note;
465         }
466
467         return $notes;
468     }
469
470     /**
471      * Load notes for the given user in full form.
472      * Optionally only those changed after $since revision
473      *
474      * @param integer $since Revision number after which the notes changed
475      *
476      * @return array Array of full note objects
477      */
478     public function loadNotesFull($since = null)
479     {
480         $result = $this->db->executeQuery(
481             'SELECT * FROM `*PREFIX*grauphel_notes`'
482             . ' WHERE note_user = ?',
483             array($this->username)
484         );
485
486         $notes = array();
487         while ($row = $result->fetch()) {
488             if ($since !== null && $row['note_last_sync_revision'] <= $since) {
489                 continue;
490             }
491             $notes[] = $this->noteFromRow($row);
492         }
493
494         return $notes;
495     }
496
497     protected function fixDate($date)
498     {
499         if (strlen($date) == 32) {
500             //Bug in grauphel 0.1.1; date fields in DB had only 32 instead of 33
501             // characters. The last digit of the time zone was missing
502             $date .= '0';
503         }
504         return $date;
505     }
506
507     protected function noteFromRow($row)
508     {
509         return (object) array(
510             'guid'  => $row['note_guid'],
511
512             'create-date'               => $this->fixDate($row['note_create_date']),
513             'last-change-date'          => $this->fixDate($row['note_last_change_date']),
514             'last-metadata-change-date' => $this->fixDate($row['note_last_metadata_change_date']),
515
516             'title'                => $row['note_title'],
517             'note-content'         => $row['note_content'],
518             'note-content-version' => $row['note_content_version'],
519
520             'open-on-startup' => (bool) $row['note_open_on_startup'],
521             'pinned'          => (bool) $row['note_pinned'],
522             'tags'            => json_decode($row['note_tags']),
523
524             'last-sync-revision' => (int) $row['note_last_sync_revision'],
525         );
526     }
527
528     protected function rowFromNote($note)
529     {
530         return array(
531             'note_guid'  => $note->guid,
532             'note_title' => (string) $note->title,
533
534             'note_content'         => (string) $note->{'note-content'},
535             'note_content_version' => (string) $note->{'note-content-version'},
536
537             'note_create_date'               => $note->{'create-date'},
538             'note_last_change_date'          => $note->{'last-change-date'},
539             'note_last_metadata_change_date' => $note->{'last-metadata-change-date'},
540
541             'note_open_on_startup' => (int) $note->{'open-on-startup'},
542             'note_pinned'          => (int) $note->pinned,
543             'note_tags'            => json_encode($note->tags),
544
545             'note_last_sync_revision' => $note->{'last-sync-revision'},
546         );
547     }
548 }
549 ?>