1<?php
2
3/**
4 * Plugin RefNotes: Reference database
5 *
6 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 * @author     Mykola Ostrovskyy <dwpforge@gmail.com>
8 */
9
10////////////////////////////////////////////////////////////////////////////////////////////////////
11class refnotes_reference_database {
12
13    private static $instance = NULL;
14
15    private $note;
16    private $key;
17    private $page;
18    private $namespace;
19    private $enabled;
20
21    /**
22     *
23     */
24    public static function getInstance() {
25        if (self::$instance == NULL) {
26            self::$instance = new refnotes_reference_database();
27
28            /* Loading has to be separated from construction to prevent infinite recursion */
29            self::$instance->load();
30        }
31
32        return self::$instance;
33    }
34
35    /**
36     * Constructor
37     */
38    public function __construct() {
39        $this->page = array();
40        $this->namespace = array();
41        $this->enabled = true;
42    }
43
44    /**
45     *
46     */
47    private function load() {
48        $this->loadNotesFromConfiguration();
49
50        if (refnotes_configuration::getSetting('reference-db-enable')) {
51            $this->loadKeys();
52            $this->loadPages();
53            $this->loadNamespaces();
54        }
55    }
56
57    /**
58     *
59     */
60    private function loadNotesFromConfiguration() {
61        $note = refnotes_configuration::load('notes');
62
63        foreach ($note as $name => $data) {
64            $this->note[$name] = new refnotes_reference_database_note('{configuration}', $data);
65        }
66    }
67
68    /**
69     *
70     */
71    private function loadKeys() {
72        $locale = refnotes_localization::getInstance();
73        foreach ($locale->getByPrefix('dbk') as $key => $text) {
74            $this->key[$this->normalizeKeyText($text)] = $key;
75        }
76    }
77
78    /**
79     *
80     */
81    public function getKey($text) {
82        $result = '';
83        $text = $this->normalizeKeyText($text);
84
85        if (in_array($text, $this->key)) {
86            $result = $text;
87        }
88        elseif (array_key_exists($text, $this->key)) {
89            $result = $this->key[$text];
90        }
91
92        return $result;
93    }
94
95    /**
96     *
97     */
98    private function normalizeKeyText($text) {
99        return preg_replace('/\s+/', ' ', utf8_strtolower(trim($text)));
100    }
101
102    /**
103     *
104     */
105    private function loadPages() {
106        global $conf;
107
108        if (file_exists($conf['indexdir'] . '/page.idx')) {
109            require_once(DOKU_INC . 'inc/indexer.php');
110
111            $pageIndex = idx_getIndex('page', '');
112            $namespace = refnotes_configuration::getSetting('reference-db-namespace');
113            $namespacePattern = '/^' . trim($namespace, ':') . ':/';
114            $cache = new refnotes_reference_database_cache();
115
116            foreach ($pageIndex as $pageId) {
117                $pageId = trim($pageId);
118
119                if ((preg_match($namespacePattern, $pageId) == 1) && file_exists(wikiFN($pageId))) {
120                    $this->enabled = false;
121                    $this->page[$pageId] = new refnotes_reference_database_page($this, $cache, $pageId);
122                    $this->enabled = true;
123                }
124            }
125
126            $cache->save();
127        }
128    }
129
130    /**
131     *
132     */
133    private function loadNamespaces() {
134        foreach ($this->page as $pageId => $page) {
135            foreach ($page->getNamespaces() as $ns) {
136                $this->namespace[$ns][] = $pageId;
137            }
138        }
139    }
140
141    /**
142     *
143     */
144    public function findNote($name) {
145        if (!$this->enabled) {
146            return NULL;
147        }
148
149        $found = array_key_exists($name, $this->note);
150
151        if (!$found) {
152            list($namespace, $temp) = refnotes_namespace::parseName($name);
153
154            if (array_key_exists($namespace, $this->namespace)) {
155                $this->loadNamespaceNotes($namespace);
156
157                $found = array_key_exists($name, $this->note);
158            }
159        }
160
161        return $found ? $this->note[$name] : NULL;
162    }
163
164    /**
165     *
166     */
167    private function loadNamespaceNotes($namespace) {
168        foreach ($this->namespace[$namespace] as $pageId) {
169            if (array_key_exists($pageId, $this->page)) {
170                $this->enabled = false;
171                $this->note = array_merge($this->note, $this->page[$pageId]->getNotes());
172                $this->enabled = true;
173
174                unset($this->page[$pageId]);
175            }
176        }
177
178        unset($this->namespace[$namespace]);
179    }
180}
181
182////////////////////////////////////////////////////////////////////////////////////////////////////
183class refnotes_reference_database_page {
184
185    private $database;
186    private $id;
187    private $fileName;
188    private $namespace;
189    private $note;
190
191    /**
192     * Constructor
193     */
194    public function __construct($database, $cache, $id) {
195        $this->database = $database;
196        $this->id = $id;
197        $this->fileName = wikiFN($id);
198        $this->namespace = array();
199        $this->note = array();
200
201        if ($cache->isCached($this->fileName)) {
202            $this->namespace = $cache->getNamespaces($this->fileName);
203        }
204        else {
205            $this->parse();
206
207            $cache->update($this->fileName, $this->namespace);
208        }
209    }
210
211    /**
212     *
213     */
214    private function parse() {
215        $text = io_readWikiPage($this->fileName, $this->id);
216        $call = p_cached_instructions($this->fileName);
217        $calls = count($call);
218
219        for ($c = 0; $c < $calls; $c++) {
220            if ($call[$c][0] == 'table_open') {
221                $c = $this->parseTable($call, $calls, $c, $text);
222            }
223            elseif ($call[$c][0] == 'code') {
224                $this->parseCode($call[$c]);
225            }
226            elseif (($call[$c][0] == 'plugin') && ($call[$c][1][0] == 'data_entry')) {
227                $this->parseDataEntry($call[$c][1][1]);
228            }
229        }
230    }
231
232    /**
233     *
234     */
235    private function parseTable($call, $calls, $c, $text) {
236        $row = 0;
237        $column = 0;
238        $columns = 0;
239        $valid = true;
240
241        for ( ; $c < $calls; $c++) {
242            switch ($call[$c][0]) {
243                case 'tablerow_open':
244                    $column = 0;
245                    break;
246
247                case 'tablerow_close':
248                    if ($row == 0) {
249                        $columns = $column;
250                    }
251                    else {
252                        if ($column != $columns) {
253                            $valid = false;
254                            break 2;
255                        }
256                    }
257                    $row++;
258                    break;
259
260                case 'tablecell_open':
261                case 'tableheader_open':
262                    $cellOpen = $call[$c][2];
263                    break;
264
265                case 'tablecell_close':
266                case 'tableheader_close':
267                    $table[$row][$column] = trim(substr($text, $cellOpen, $call[$c][2] - $cellOpen), "^| ");
268                    $column++;
269                    break;
270
271                case 'table_close':
272                    break 2;
273            }
274        }
275
276        if ($valid && ($row > 1) && ($columns > 1)) {
277            $this->handleTable($table, $columns, $row);
278        }
279
280        return $c;
281    }
282
283    /**
284     *
285     */
286    private function handleTable($table, $columns, $rows) {
287        $key = array();
288        for ($c = 0; $c < $columns; $c++) {
289            $key[$c] = $this->database->getKey($table[0][$c]);
290        }
291
292        if (!in_array('', $key)) {
293            $this->handleDataSheet($table, $columns, $rows, $key);
294        }
295        else {
296            if ($columns == 2) {
297                $key = array();
298                for ($r = 0; $r < $rows; $r++) {
299                    $key[$r] = $this->database->getKey($table[$r][0]);
300                }
301
302                if (!in_array('', $key)) {
303                    $this->handleDataCard($table, $rows, $key);
304                }
305            }
306        }
307    }
308
309    /**
310     * The data is organized in rows, one note per row. The first row contains the caption.
311     */
312    private function handleDataSheet($table, $columns, $rows, $key) {
313        for ($r = 1; $r < $rows; $r++) {
314            $data = array();
315
316            for ($c = 0; $c < $columns; $c++) {
317                $data[$key[$c]] = $table[$r][$c];
318            }
319
320            $this->handleNote($data);
321        }
322    }
323
324    /**
325     * Every note is stored in a separate table. The first column of the table contains
326     * the caption, the second one contains the data.
327     */
328    private function handleDataCard($table, $rows, $key) {
329        $data = array();
330
331        for ($r = 0; $r < $rows; $r++) {
332            $data[$key[$r]] = $table[$r][1];
333        }
334
335        $this->handleNote($data);
336    }
337
338    /**
339     *
340     */
341    private function parseCode($call) {
342        switch ($call[1][1]) {
343            case 'bibtex':
344                $this->parseBibtex($call[1][0]);
345                break;
346        }
347    }
348
349    /**
350     *
351     */
352    private function parseBibtex($text) {
353        foreach (refnotes_bibtex_parser::getInstance()->parse($text) as $data) {
354            $this->handleNote($data);
355        }
356    }
357
358    /**
359     *
360     */
361    private function parseDataEntry($pluginData) {
362        if (preg_match('/\brefnotes\b/', $pluginData['classes'])) {
363            $data = array();
364
365            foreach ($pluginData['data'] as $key => $value) {
366                if (is_array($value)) {
367                    $data[$key . 's'] = implode(', ', $value);
368                }
369                else {
370                    $data[$key] = $value;
371                }
372            }
373
374            $this->handleNote($data);
375        }
376    }
377
378    /**
379     *
380     */
381    private function handleNote($data) {
382        $note = new refnotes_reference_database_note($this->id, $data);
383
384        list($namespace, $name) = $note->getNameParts();
385
386        if ($name != '') {
387            if (!in_array($namespace, $this->namespace)) {
388                $this->namespace[] = $namespace;
389            }
390
391            $this->note[$namespace . $name] = $note;
392        }
393    }
394
395    /**
396     *
397     */
398    public function getNamespaces() {
399        return $this->namespace;
400    }
401
402    /**
403     *
404     */
405    public function getNotes() {
406        if (empty($this->note)) {
407            $this->parse();
408        }
409
410        return $this->note;
411    }
412}
413
414////////////////////////////////////////////////////////////////////////////////////////////////////
415class refnotes_reference_database_note extends refnotes_refnote {
416
417    private $nameParts;
418
419    /**
420     * Constructor
421     */
422    public function __construct($source, $data) {
423        parent::__construct();
424
425        $this->nameParts = array('', '');
426
427        if ($source == '{configuration}') {
428            $this->initializeConfigNote($data);
429        }
430        else {
431            $this->initializePageNote($data);
432        }
433
434        $this->attributes['source'] = $source;
435    }
436
437    /**
438     *
439     */
440    public function initializeConfigNote($data) {
441        $this->data['note-text'] = $data['text'];
442
443        unset($data['text']);
444
445        $this->attributes = $data;
446    }
447
448    /**
449     *
450     */
451    public function initializePageNote($data) {
452        if (isset($data['note-name'])) {
453            if (preg_match('/^' . refnotes_note::getNamePattern('full-extended') . '$/', $data['note-name']) == 1) {
454                $this->nameParts = refnotes_namespace::parseName($data['note-name']);
455            }
456
457            unset($data['note-name']);
458        }
459
460        $this->data = $data;
461    }
462
463    /**
464     *
465     */
466    public function getNameParts() {
467        return $this->nameParts;
468    }
469}
470
471////////////////////////////////////////////////////////////////////////////////////////////////////
472class refnotes_reference_database_cache {
473
474    private $fileName;
475    private $cache;
476    private $requested;
477    private $updated;
478
479    /**
480     * Constructor
481     */
482    public function __construct() {
483        global $conf;
484
485        $this->fileName = $conf['cachedir'] . '/refnotes.database.dat';
486
487        $this->load();
488    }
489
490    /**
491     *
492     */
493    private function load() {
494        $this->cache = array();
495        $this->requested = array();
496
497        if (file_exists($this->fileName)) {
498            $this->cache = unserialize(io_readFile($this->fileName, false));
499        }
500
501        foreach (array_keys($this->cache) as $fileName) {
502            $this->requested[$fileName] = false;
503        }
504
505        $this->updated = false;
506    }
507
508    /**
509     *
510     */
511    public function isCached($fileName) {
512        $result = false;
513
514        if (array_key_exists($fileName, $this->cache)) {
515            if ($this->cache[$fileName]['time'] == @filemtime($fileName)) {
516                $result = true;
517            }
518        }
519
520        $this->requested[$fileName] = true;
521
522        return $result;
523    }
524
525    /**
526     *
527     */
528    public function getNamespaces($fileName) {
529        return $this->cache[$fileName]['ns'];
530    }
531
532    /**
533     *
534     */
535    public function update($fileName, $namespace) {
536        $this->cache[$fileName] = array('ns' => $namespace, 'time' => @filemtime($fileName));
537        $this->updated = true;
538    }
539
540    /**
541     *
542     */
543    public function save() {
544        $this->removeOldPages();
545
546        if ($this->updated) {
547            io_saveFile($this->fileName, serialize($this->cache));
548        }
549    }
550
551    /**
552     *
553     */
554    private function removeOldPages() {
555        foreach ($this->requested as $fileName => $requested) {
556            if (!$requested && array_key_exists($fileName, $this->cache)) {
557                unset($this->cache[$fileName]);
558
559                $this->updated = true;
560            }
561        }
562    }
563}
564