1<?php
2/**
3 * DokuWiki Plugin bibtex4dw (BibTeX Renderer Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Till Biskup <till@till-biskup.de>
7 * @version 0.4.0
8 * @date    2023-05-28
9 */
10
11require_once(DOKU_PLUGIN.'bibtex4dw/lib/bibtexparser.php');
12
13class bibtexrender_plugin_bibtex4dw {
14
15    /**
16     * Array containing all references to objects of this class that already exist.
17     *
18     * Necessary to synchronize calls from the two different syntax plugins for the same page
19     */
20    public static $resources = array();
21
22    /**
23     * Handle to SQLite db
24     */
25    public static $sqlite = array();
26
27    /**
28     * Array containing local configuration of the object
29     *
30     * Options can be set by calling setOptions($options = array())
31     */
32    private $_conf = array(
33        'sqlite' => false,
34        'file' => array(),
35        'citetype' => '',
36        'sort' => false,
37        'formatstring' => array(),
38        );
39
40    private $_langStrings = array(
41        'pageabbrev',
42        'pagesabbrev',
43        'chapterabbrev',
44        'editorabbrev',
45        'mastersthesis',
46        'phdthesis',
47        'techreport',
48        'unpublished'
49        );
50
51    /**
52     * Array containing all references from the BibTeX files loaded
53     */
54    private $_bibtex_references = array();
55
56    /**
57     * Array containing all keys from the BibTeX records in $_bibtex_references
58     */
59    private $_bibtex_keys = array();
60
61    /**
62     * Array containing all keys already cited together with their number
63     */
64    private $_bibtex_keysCited = array();
65
66    /**
67     * Array containing all keys given with "nocite" together with their number
68     */
69    private $_bibtex_keysNotCited = array();
70
71    /**
72     * Number of the currently highest cite key if style "numeric" is used
73     */
74    private $_currentKeyNumber = 0;
75
76    /**
77     * Initializes the class
78     *
79     * As it loads the configuration options set e.g. via the config manager via the admin
80     * interface of dokuwiki, the plugin can be configured such that only the final
81     * <bibtex bibliography></bibtex> pattern is necessary to print the actual bibliography.
82     */
83    function __construct() {
84        // Use trick to access configuration and language stuff from the actual bibtex plugin
85        // with the builtin methods of DW
86        require_once(DOKU_PLUGIN.'bibtex4dw/syntax/bibtex.php');
87        $this->plugin = new syntax_plugin_bibtex4dw_bibtex();
88
89        // Transfer config settings from plugin config (via config manager) to local config
90        // Note: Only those settings that can be changed by this class need to be transferred.
91        // Therefore it is not necessary to transfer the format strings for the entries.
92        $this->_conf['sqlite'] = $this->plugin->getConf('sqlite');
93        $this->_conf['file'] = explode(';',$this->plugin->getConf('file'));
94        $this->_conf['pdfdir'] = explode(';',$this->plugin->getConf('pdfdir'));
95        $this->_conf['citetype'] = $this->plugin->getConf('citetype');
96
97        // In case we shall use SQLite
98        if ($this->_conf['sqlite']) {
99            $this->sqlite = plugin_load('helper', 'sqlite');
100            if(!$this->sqlite){
101                msg('You asked for using the sqlite plugin but it is not installed. Please install it', -1);
102                return;
103            }
104            // initialize the database connection
105            if(!$this->sqlite->init('bibtex4dw', DOKU_PLUGIN.'bibtex4dw/db/')){
106                return;
107            }
108        } else {
109            // If there are files to load, load and parse them
110            if (array_key_exists('file', $this->_conf)) {
111                $this->_parseBibtexFile();
112            }
113        }
114    }
115
116    /**
117     * Gets instance of the class by id
118     *
119     * @param pageid
120     */
121    public static function getResource($id = NULL) {
122        // exit if given parameters not sufficient.
123        if (is_null($id)) {
124            return null;
125        }
126        if (!array_key_exists($id, self::$resources)) {
127            $x = new bibtexrender_plugin_bibtex4dw($id);
128            self::$resources[$id] = $x;
129        }
130        // return the desired object or null in case of error
131        return self::$resources[$id];
132    }
133
134    /**
135     * Set options of object
136     *
137     * @param options
138     */
139    public function setOptions($options) {
140        // Clear nocite array if it already exists and is not empty.
141        // This is necessary for the "furtherreading" bibliographies, so that you
142        // can have more than one of these bibliographies that are independent.
143        if (isset($this->_bibtex_keysNotCited) && (count($this->_bibtex_keysNotCited))) {
144            $this->_bibtex_keysNotCited = array();
145        }
146        // Handle options, convert to $_conf if possible
147        foreach($options as $optkey => $optval) {
148            if ('nocite' == $optkey) {
149                // if $optval is an array (i.e. more than one "nocite" was given)
150                if (is_array($optval)) {
151                    $optval = implode(',',$optval);
152                }
153                $bibkeys = explode(',',$optval);
154                foreach ($bibkeys as $bibkey) {
155                    $this->_bibtex_keysNotCited[$bibkey] = 0;
156                }
157            }
158            if (array_key_exists($optkey,$this->_conf)) {
159                // For file, allow multiple entries
160                if ('file' == $optkey) {
161                    if (is_array($optval) && (count($optval) > 1)) {
162                        $this->_conf[$optkey] = $optval;
163                    } else {
164                        $this->_conf[$optkey][] = $optval[0];
165                    }
166                // In all other cases, the last entry of a type wins
167                } else {
168                    $this->_conf[$optkey] = $optval[count($optval)-1];
169                }
170            }
171        }
172
173        // Set sort option depending on citetype
174        if (!array_key_exists('sort',$options)) {
175            switch ($this->_conf['citetype']) {
176                case 'apa':
177                    $this->_conf['sort'] = 'true';
178                    break;
179                case 'alpha':
180                    $this->_conf['sort'] = 'true';
181                    break;
182                case 'authordate':
183                    $this->_conf['sort'] = 'true';
184                    break;
185                case 'numeric':
186                    $this->_conf['sort'] = 'false';
187                    break;
188                default:
189                    $this->_conf['sort'] = 'false';
190                    break;
191            }
192        }
193
194    }
195
196    /**
197     * Internal function parsing the contents of the BibTeX file
198     *
199     * The result will be stored in $this->_bibtex_references
200     */
201    private function _parseBibtexFile() {
202        $bibtex = '';
203        // Load all files and concatenate their contents
204        foreach($this->_conf['file'] as $file) {
205            $bibtex .= $this->_loadBibtexFile($file, 'page');
206        }
207        $this->_parser = new bibtexparser_plugin_bibtex4dw();
208        $this->_parser->loadString($bibtex);
209        $stat = $this->_parser->parseBibliography();
210        if ( !$stat ) {
211            return $stat;
212        }
213        //$this->_bibtex_references = $this->_parser->data;
214        $this->_bibtex_references = $this->_parser->entries;
215
216        foreach($this->_bibtex_references as $refno => $ref) {
217            if (is_array($ref) && array_key_exists('cite', $ref)) {
218                $this->_bibtex_keys[$ref['cite']] = $refno;
219            }
220        }
221    }
222
223    /**
224     * Internal function adding the content to the SQLite database
225     */
226    public function addBibtexToSQLite($bibtex,$ID) {
227        if (!$this->_conf['sqlite']) {
228            return;
229        }
230        if (!in_array(':'.$ID, $this->_conf['file'])) {
231            msg("Current page (:$ID) not configured to be a BibTeX DB, hence ignoring.
232                Change in config if this is not intended.");
233            return;
234        }
235        $this->_parser = new bibtexparser_plugin_bibtex4dw();
236        $this->_parser->loadString($bibtex);
237        $this->_parser->sqlite = $this->sqlite;
238        $stat = $this->_parser->parseBibliography($sqlite=true);
239
240        if ( !$stat ) {
241            msg('Some problems with parsing BIBTeX code',-1);
242        }
243
244        if ( ($this->_parser->warnings['warning']) && (count($this->_parser->warnings['warning']))) {
245            foreach($this->_parser->warnings as $parserWarning) {
246                msg($this->_parser->warnings[$parserWarning]['warning'],'2');
247            }
248        }
249    }
250
251    /**
252     * Prints the reference corresponding to the given bibtex key
253     *
254     * The format of the reference can be freely configured using the
255     * dokuwiki configuration interface (see files in conf dir)
256     *
257     * @param  string BibTeX key of the reference
258     * @return string (HTML) formatted BibTeX reference
259     */
260    public function printReference($bibtex_key) {
261        global $INFO;
262
263        if ($this->_conf['sqlite']) {
264            $this->_parser = new bibtexparser_plugin_bibtex4dw();
265            $this->_parser->sqlite = $this->sqlite;
266            $rawBibtexEntry = $this->sqlite->res2arr($this->sqlite->query("SELECT entry FROM bibtex WHERE key=?",$bibtex_key));
267            $this->_parser->loadString($rawBibtexEntry[0]['entry']);
268            $stat = $this->_parser->parse();
269            if ( !$stat ) {
270                return $stat;
271            }
272            $ref = $this->_parser->data[0];
273        } else {
274            //$ref = $this->_bibtex_references[$this->_bibtex_keys[$bibtex_key]];
275            $rawBibtexEntry = $this->_bibtex_references[$bibtex_key];
276            $this->_parser->loadString($rawBibtexEntry);
277            $stat = $this->_parser->parse();
278            if ( !$stat ) {
279                return $stat;
280            }
281            $ref = $this->_parser->data[0];
282        }
283        if (empty($ref)) {
284            return;
285        }
286        // Variant of $ref with normalized (i.e., all uppercase) field names
287        $normalizedRef = [];
288        foreach ($ref as $key => $value) {
289            $normalizedRef[strtoupper($key)] = $value;
290        }
291        // Get format string from plugin config
292        $formatstring = $this->plugin->getConf('fmtstr_'.$normalizedRef['ENTRYTYPE']);
293        // Replace each language string ($this->_langStrings) pattern '@placeholder@' with respective value
294        foreach ($this->_langStrings as $lang) {
295            $formatstring = str_replace('@'.strtolower($lang).'@', $this->plugin->getLang($lang), $formatstring);
296        }
297        // Replace each field pattern '{...@FIELDNAME@...}' with respective value from bib data
298        preg_match_all("#\{([^@]*)@([A-Z]+)@([^@]*)\}#U", $formatstring, $fieldsToBeReplaced, PREG_SET_ORDER);
299        foreach ($fieldsToBeReplaced as $matchPair) {
300            $partOfFormatstring = $matchPair[0];
301            $priorToName = $matchPair[1];
302            $fieldName = $matchPair[2];
303            $afterName = $matchPair[3];
304            if (empty($normalizedRef[$fieldName])) {
305                $formatstring = str_replace($partOfFormatstring, "", $formatstring);
306                continue;
307            }
308            $formattedPart = $priorToName;
309            $formattedPart .= $normalizedRef[$fieldName];
310            $formattedPart .= $afterName;
311            $formatstring = str_replace($partOfFormatstring, $formattedPart, $formatstring);
312        }
313        // Handle PDF files
314        // Check whether we have a directory for PDF files
315        if (array_key_exists('pdfdir',$this->_conf)) {
316            // Check whether we are logged in and have permissions to access the PDFs
317            if ((auth_quickaclcheck($this->_conf['pdfdir'][0]) >= AUTH_READ) &&
318                array_key_exists('name',$INFO['userinfo'])) {
319                // do sth.
320                $pdffilename = mediaFN($this->_conf['pdfdir'][0]) . "/" . $bibtex_key . ".pdf";
321                if (file_exists($pdffilename)) {
322                    resolve_mediaid($this->_conf['pdfdir'][0], $pdflinkname, $exists);
323                    $formatstring .= '&nbsp;<a href="' .
324                    ml($pdflinkname) . "/" . $bibtex_key . ".pdf" . '">PDF</a>';
325                }
326            }
327        }
328
329        return $formatstring;
330    }
331
332    /**
333     * Prints the whole bibliography
334     *
335     * @param  string substate (bibliography, furtherreading)
336     * @return string (HTML) formatted bibliography
337     */
338    public function printBibliography($substate) {
339        switch ($substate) {
340            case 'bibliography':
341                if (!isset($this->_bibtex_keysCited) || empty($this->_bibtex_keysCited)) {
342                    return;
343                }
344                // If there are nocite entries
345                if (isset($this->_bibtex_keysNotCited) && !empty($this->_bibtex_keysNotCited)) {
346                    foreach ($this->_bibtex_keysNotCited as $key => $no) {
347                        if (!array_key_exists($key,$this->_bibtex_keysCited)) {
348                            $this->_bibtex_keysCited[$key] = ++$this->_currentKeyNumber;
349                        }
350                    }
351                }
352                if ('true' == $this->_conf['sort'] && 'numeric' != $this->_conf['citetype']) {
353                    $citedKeys = array();
354                    foreach ($this->_bibtex_keysCited as $key => $no) {
355                        if ($this->_conf['sqlite']) {
356                            $this->_parser = new bibtexparser_plugin_bibtex4dw();
357                            $this->_parser->sqlite = $this->sqlite;
358                            $rawBibtexEntry = $this->sqlite->res2arr($this->sqlite->query("SELECT entry FROM bibtex WHERE key=?",$key));
359                            $this->_parser->loadString($rawBibtexEntry[0]['entry']);
360                            $stat = $this->_parser->parse();
361                            if ( !$stat ) {
362                                return $stat;
363                            }
364                            $citedKeys[$key] = $this->_parser->data[0]['authoryear'];
365                        } else {
366                            $citedKeys[$key] = $this->_bibtex_references[$this->_bibtex_keys[$key]]['authoryear'];
367                        }
368                    }
369                    asort($citedKeys);
370                } else {
371                    $citedKeys = $this->_bibtex_keysCited;
372                }
373                if ('authordate' == $this->_conf['citetype']) {
374                    $html = $this->_printReferencesAsUnorderedList($citedKeys);
375                } else {
376                    $html = $this->_printReferencesAsDefinitionlist($citedKeys);
377                }
378                return $html;
379            case 'furtherreading':
380                if (!isset($this->_bibtex_keysNotCited) || empty($this->_bibtex_keysNotCited)) {
381                    return;
382                }
383                $this->_currentKeyNumber = 0;
384                if ('true' == $this->_conf['sort']) {
385                    $notcitedKeys = array();
386                    foreach ($this->_bibtex_keysNotCited as $key => $no) {
387                        if ($this->_conf['sqlite']) {
388                            $this->_parser = new bibtexparser_plugin_bibtex4dw();
389                            $this->_parser->sqlite = $this->sqlite;
390                            $rawBibtexEntry = $this->sqlite->res2arr($this->sqlite->query("SELECT entry FROM bibtex WHERE key=?",$key));
391                            $this->_parser->loadString($rawBibtexEntry[0]['entry']);
392                            $stat = $this->_parser->parse();
393                            if ( !$stat ) {
394                                return $stat;
395                            }
396                            $notcitedKeys[$key] = $this->_parser->data[0]['authoryear'];
397                        } else {
398                            $notcitedKeys[$key] = $this->_bibtex_references[$this->_bibtex_keys[$key]]['authoryear'];
399                        }
400                    }
401                    asort($notcitedKeys);
402                } else {
403                    $notcitedKeys = $this->_bibtex_keysNotCited;
404                }
405                if ('authordate' == $this->_conf['citetype']) {
406                    $html = $this->_printReferencesAsUnorderedList($notcitedKeys);
407                } else {
408                    $html = $this->_printReferencesAsDefinitionlist($notcitedKeys);
409                }
410                return $html;
411        }
412    }
413
414    /**
415     * Print references as unordered list
416     *
417     * Currently used only for "authordate" citation style
418     *
419     * @param array List of keys bibliography should be generated for
420     * @return string rendered HTML of bibliography
421     */
422    function _printReferencesAsUnorderedList($citedKeys) {
423        $html = '<ul class="bibtex_references">' . DOKU_LF;
424        foreach ($citedKeys as $key => $no) {
425            if ($this->keyExists($key)) {
426                $html .= '<li><div class="li" name="ref__' . $key . '" id="ref__'. $key . '">';
427                $html .= $this->printReference($key);
428                $html .= '</div></li>' . DOKU_LF;
429            } else {
430                msg("BibTeX key '$key' could not be found. Possible typo?");
431            }
432        }
433        $html .= '</ul>';
434        return $html;
435    }
436
437    /**
438     * Print references as definitionlist
439     *
440     * Currently used for all citation styles except "authordate"
441     *
442     * @param array List of keys bibliography should be generated for
443     * @return string rendered HTML of bibliography
444     */
445    function _printReferencesAsDefinitionlist($citedKeys) {
446        $html = '<dl class="bibtex_references">' . DOKU_LF;
447        foreach ($citedKeys as $key => $no) {
448            if ($this->keyExists($key)) {
449                $html .= '<dt>[';
450                $html .= $this->printCitekey($key);
451                $html .= ']</dt>' . DOKU_LF;
452                $html .= '<dd>';
453                $html .= $this->printReference($key);
454                $html .= '</dd>' . DOKU_LF;
455            } else {
456                msg("BibTeX key '$key' could not be found. Possible typo?");
457            }
458        }
459        $html .= '</dl>';
460        return $html;
461    }
462
463    /**
464     * Return the cite key of the given reference for using in the text
465     * The Output depends on the configuration via the variable
466     * $this->_conf['citetype']
467     * If the citetype is unknown, the bibtex key is returned
468     *
469     * @param string  bibtex key of the reference
470     * @return string cite key of the reference (according to the citetype set)
471     */
472    public function printCitekey($bibtex_key) {
473        if (!array_key_exists($bibtex_key,$this->_bibtex_keysCited)) {
474            $this->_currentKeyNumber++;
475            $this->_bibtex_keysCited[$bibtex_key] = $this->_currentKeyNumber;
476        }
477        if ($this->_conf['sqlite']) {
478            $this->_parser = new bibtexparser_plugin_bibtex4dw();
479            $rawBibtexEntry = $this->sqlite->res2arr($this->sqlite->query("SELECT entry FROM bibtex WHERE key=?",$bibtex_key));
480            if (empty($rawBibtexEntry)) {
481                return $bibtex_key;
482            }
483            $this->_parser->loadString($rawBibtexEntry[0]['entry']);
484            $stat = $this->_parser->parse();
485            if ( !$stat ) {
486                return $stat;
487            }
488            $ref = $this->_parser->data[0];
489        } else {
490            // Check whether key exists
491            if (empty($this->_bibtex_references[$bibtex_key])) {
492                return $bibtex_key;
493            }
494            $ref = $this->_bibtex_references[$this->_bibtex_keys[$bibtex_key]];
495        }
496        switch ($this->_conf['citetype']) {
497            case 'apa':
498                $bibtex_key = $ref['authors'][0]['last'];
499                if ($ref['authors'][0]['last'] == '') {
500                  $bibtex_key = $ref['editors'][0]['last'];
501                }
502                $bibtex_key .= $ref['year'];
503                break;
504            case 'alpha':
505                $bibtex_key = substr($ref['authors'][0]['last'],0,3);
506                if ($ref['authors'][0]['last'] == '') {
507                  $bibtex_key = substr($ref['editors'][0]['last'],0,3);
508                }
509                $bibtex_key .= substr($ref['year'],2,2);
510                break;
511            case 'authordate':
512                $bibtex_key = $ref['authors'][0]['last'] . ", ";
513                if ($ref['authors'][0]['last'] == '') {
514                  $bibtex_key = $ref['editors'][0]['last'] . ", ";
515                }
516                $bibtex_key .= $ref['year'];
517                break;
518            case 'numeric':
519                $bibtex_key = $this->_bibtex_keysCited[$bibtex_key];
520                break;
521            // If no known citation style is given - however, that should not happen
522            default:
523                $bibtex_key = $this->_bibtex_keysCited[$bibtex_key];
524                break;
525        }
526        return $bibtex_key;
527    }
528
529    /**
530     * Check if given key exists in currently used BibTeX database
531     *
532     * @param string  bibtex key of the reference
533     * @return Boolean value
534     */
535    function keyExists($bibkey) {
536        if ($this->_conf['sqlite']) {
537            $rawBibtexEntry = $this->sqlite->res2arr($this->sqlite->query("SELECT entry FROM bibtex WHERE key=?",$bibkey));
538            return (!empty($rawBibtexEntry));
539        } else {
540            return (!empty($this->_bibtex_references[$bibkey]));
541        }
542    }
543
544    /**
545     * Debug function to output the raw contents of the BibTeX file
546     *
547     * @return string raw BibTeX code
548     */
549    function rawOutput() {
550        $bibtex = '';
551        // Load all files and concatenate their contents
552        foreach($this->_conf['file'] as $file) {
553            $bibtex .= $this->_loadBibtexFile($file, 'page');
554        }
555        return $bibtex;
556    }
557
558    /**
559     * Loads BibTeX code and returns the raw BibTeX as string
560     *
561     * @param string uri  BibTeX file (path or dokuwiki page)
562     * @param string kind whether uri is a file or a dokuwiki page
563     * @return string raw BibTeX code
564     */
565    function _loadBibtexFile($uri, $kind) {
566        global $INFO;
567
568        if ( $kind == 'file' ) {
569            // FIXME: Adjust path - make it configurable
570            return file_get_contents(dirname(__FILE__).'/'.$kind.'/'.$uri);
571        } elseif ($kind == 'page') {
572            $exists = false;
573            resolve_pageid($INFO['namespace'], $uri, $exists);
574            if ( $exists ) {
575                return rawWiki($uri);
576            }
577        }
578
579        return null;
580    }
581
582}
583
584?>
585