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