1<?php
2/**
3 * DokuSIOC - SIOC plugin for DokuWiki
4 *
5 * DokuSIOC integrates the SIOC ontology within DokuWiki and provides an
6 * alternate RDF/XML views of the wiki documents.
7 *
8 * For DokuWiki we can't use the Triplify script because DokuWiki has not a RDBS
9 * backend. But the wiki API provides enough methods to get the data out, so
10 * DokuSIOC as a plugin uses the export hook to provide accessible data as
11 * RDF/XML, using the SIOC ontology as vocabulary.
12 * @copyright 2009 Michael Haschke
13 * @copyright 2020 mprins
14 * LICENCE
15 *
16 * This program is free software: you can redistribute it and/or modify it under
17 * the terms of the GNU General Public License as published by the Free Software
18 * Foundation, version 2 of the License.
19 *
20 * This program is distributed in the hope that it will be useful, but WITHOUT
21 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
22 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
23 *
24 * @link      http://www.gnu.org/licenses/old-licenses/gpl-2.0.html GNU General Public License 2.0 (GPLv2)
25 *
26 */
27
28class action_plugin_dokusioc extends DokuWiki_Action_Plugin
29{
30
31    private $agentlink = 'http://eye48.com/go/dokusioc?v=0.1.2';
32
33    /**
34     * Register it's handlers with the DokuWiki's event controller
35     */
36    public function register(Doku_Event_Handler $controller): void
37    {
38        //print_r(headers_list()); die();
39        // test the requested action
40        $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'checkAction', $controller);
41    }
42
43    /* -- Event handlers ---------------------------------------------------- */
44
45    public function checkAction($action, $controller)
46    {
47        global $INFO;
48        //print_r($INFO); die();
49        //print_r(headers_list()); die();
50
51        if ($action->data === 'export_siocxml') {
52            // give back rdf
53            $this->exportSioc();
54        } elseif (($action->data === 'show' || $action->data === 'index') && $INFO['perm'] && !defined(
55            'DOKU_MEDIADETAIL'
56        ) && ($INFO['exists'] || getDwUserInfo($INFO['id'], $this)) && !isHiddenPage($INFO['id'])) {
57            if ($this->isRdfXmlRequest()) {
58                // forward to rdfxml document if requested
59                // print_r(headers_list()); die();
60                $location = $this->createRdfLink();
61                if (function_exists('header_remove')) {
62                    header_remove();
63                }
64                header('Location: ' . $location['href'], true, 303);
65                exit();
66            } else {
67                // add meta link to html head
68                $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'createRdfLink');
69            }
70        }
71        /*
72        else
73        {
74            print_r(array($action->data, $INFO['perm'], defined('DOKU_MEDIADETAIL'), $INFO['exists'],
75                    getDwUserInfo($INFO['id'],$this), isHiddenPage($INFO['id'])));
76            die();
77        }
78        */
79    }
80
81    public function exportSioc()
82    {
83        global $ID, $INFO;
84
85        if (isHiddenPage($ID)) {
86            $this->exit("HTTP/1.0 404 Not Found");
87        }
88
89        $sioc_type = $this->getContenttype();
90
91        // Test for valid types
92        if (!(($sioc_type == 'post' && $INFO['exists']) || $sioc_type == 'user' || $sioc_type == 'container')) {
93            $this->exit("HTTP/1.0 404 Not Found");
94        }
95
96        // Test for permission
97        if (!$INFO['perm']) {
98            // not enough rights to see the wiki page
99            $this->exit("HTTP/1.0 401 Unauthorized");
100        }
101
102        // Forward to URI with explicit type attribut
103        if (!isset($_GET['type'])) {
104            header('Location:' . $_SERVER['REQUEST_URI'] . '&type=' . $sioc_type, true, 302);
105        }
106
107        // Include SIOC libs
108        require_once(__DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'sioc_inc.php');
109        require_once(__DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'sioc_dokuwiki.php');
110
111        // Create exporter
112
113        $rdf              = new SIOCExporter();
114        $rdf->profile_url = normalizeUri(
115            getAbsUrl(
116                exportlink(
117                    $ID,
118                    'siocxml',
119                    array('type' => $sioc_type),
120                    false,
121                    '&'
122                )
123            )
124        );
125        $rdf->setURLParameters('type', 'id', 'page', false);
126
127        // Create SIOC-RDF content
128
129        switch ($sioc_type) {
130            case 'container':
131                $rdf = $this->exportContainercontent($rdf);
132                break;
133
134            case 'user':
135                $rdf = $this->exportUsercontent($rdf);
136                break;
137
138            case 'post':
139            default:
140                $rdf = $this->exportPostcontent($rdf);
141                break;
142        }
143
144        // export
145        if ($this->getConf('noindx')) {
146            header("X-Robots-Tag: noindex", true);
147        }
148        $rdf->export();
149
150        //print_r(headers_list()); die();
151        die();
152    }
153
154    private function exit($headermsg)
155    {
156        header($headermsg);
157        die();
158    }
159
160    /* -- public class methods ---------------------------------------------- */
161
162    private function getContenttype()
163    {
164        global $ID, $conf;
165
166        // check for type if unknown
167        if (!($_GET['type'] ?? "")) {
168            $userinfo = getDwUserInfo($ID, $this);
169
170            if ($userinfo) {
171                $type = 'user';
172            } elseif (isset($_GET['do']) && $_GET['do'] == 'index') {
173                $type = 'container';
174            } else {
175                $type = 'post';
176            }
177        } else {
178            $type = $_GET['type'];
179        }
180
181        return $type;
182    }
183
184    private function exportContainercontent($exporter)
185    {
186        global $ID, $INFO, $conf;
187
188        if ($ID == $conf['start']) {
189            $title = $conf['title'];
190        } elseif (isset($INFO['meta']['title'])) {
191            $title = $INFO['meta']['title'];
192        } else {
193            $title = $ID;
194        }
195
196        $exporter->setParameters(
197            'Container: ' . $title,
198            getAbsUrl(),
199            getAbsUrl() . 'doku.php?',
200            'utf-8',
201            $this->agentlink
202        );
203
204        // create container object
205        $wikicontainer = new SIOCDokuWikiContainer(
206            $ID,
207            normalizeUri($exporter->siocURL('container', $ID))
208        );
209
210        /* container is type=wiki */
211        if ($ID == $conf['start']) {
212            $wikicontainer->isWiki();
213        }
214        /* sioc:name              */
215        if ($INFO['exists']) {
216            $wikicontainer->addTitle($INFO['meta']['title']);
217        }
218        /* has_parent             */
219        if ($INFO['namespace']) {
220            $wikicontainer->addParent($INFO['namespace']);
221        }
222
223        // search next level entries (posts, sub containers) in container
224        require_once(DOKU_INC . 'inc/search.php');
225        $dir        = utf8_encodeFN(str_replace(':', '/', $ID));
226        $entries    = array();
227        $posts      = array();
228        $containers = array();
229        search($entries, $conf['datadir'], 'search_index', array('ns' => $ID), $dir);
230        foreach ($entries as $entry) {
231            if ($entry['type'] === 'f') {
232                // wikisite
233                $posts[] = $entry;
234            } elseif ($entry['type'] === 'd') {
235                // sub container
236                $containers[] = $entry;
237            }
238        }
239
240        // without sub content it can't be a container (so it does not exist as a container)
241        if (count($posts) + count($containers) == 0) {
242            $this->exit("HTTP/1.0 404 Not Found");
243        }
244
245        if (count($posts) > 0) {
246            $wikicontainer->addArticles($posts);
247        }
248        if (count($containers) > 0) {
249            $wikicontainer->addContainers($containers);
250        }
251
252        //print_r($containers);die();
253
254        // add container to exporter
255        $exporter->addObject($wikicontainer);
256
257        return $exporter;
258    }
259
260    /* -- private helpers --------------------------------------------------- */
261
262    private function exportUsercontent($exporter)
263    {
264        global $ID;
265
266        // get user info
267        $userinfo = getDwUserInfo($ID, $this);
268
269        // no userinfo means there is no user space or user does not exists
270        if ($userinfo === false) {
271            $this->exit("HTTP/1.0 404 Not Found");
272        }
273
274        $exporter->setParameters(
275            'Account: ' . $userinfo['name'],
276            getAbsUrl(),
277            getAbsUrl() . 'doku.php?',
278            'utf-8',
279            $this->agentlink
280        );
281        // create user object
282        //print_r($userinfo); die();
283        // $id, $url, $userid, $name, $email
284        $wikiuser = new SIOCDokuWikiUser(
285            $ID,
286            normalizeUri($exporter->siocURL('user', $ID)),
287            $userinfo['user'],
288            $userinfo['name'],
289            $userinfo['mail']
290        );
291        /* TODO: avatar (using Gravatar) */ /* TODO: creator_of */
292        // add user to exporter
293        $exporter->addObject($wikiuser);
294
295        //print_r(headers_list());die();
296        return $exporter;
297    }
298
299    private function exportPostcontent($exporter)
300    {
301        global $ID, $INFO, $REV, $conf;
302
303        $exporter->setParameters(
304            $INFO['meta']['title'] . ($REV ? ' (rev ' . $REV . ')' : ''),
305            $this->getDokuUrl(),
306            $this->getDokuUrl() . 'doku.php?',
307            'utf-8',
308            $this->agentlink
309        );
310
311        // create user object
312        $dwuserpage_id = cleanID($this->getConf('userns')) . ($conf['useslash'] ? '/' : ':') . $INFO['editor'];
313        // create wiki page object
314        $wikipage = new SIOCDokuWikiArticle(
315            $ID, // id
316            normalizeUri(
317                $exporter->siocURL(
318                    'post',
319                    $ID . ($REV ? $exporter->_urlseparator . 'rev' . $exporter->_urlequal . $REV : '')
320                )
321            ), // url
322            $INFO['meta']['title'] . ($REV ? ' (rev ' . $REV . ')' : ''), // subject
323            rawWiki($ID, $REV) // body (content)
324        );
325        /* encoded content   */
326        $wikipage->addContentEncoded(p_cached_output(wikiFN($ID, $REV), 'xhtml'));
327        /* created           */
328        if (isset($INFO['meta']['date']['created'])) {
329            $wikipage->addCreated(date('c', $INFO['meta']['date']['created']));
330        }
331        /* or modified       */
332        if (isset($INFO['meta']['date']['modified'])) {
333            $wikipage->addModified(date('c', $INFO['meta']['date']['modified']));
334        }
335        /* creator/modifier  */
336        if ($INFO['editor'] && $this->getConf('userns')) {
337            $wikipage->addCreator(array('foaf:maker' => '#' . $INFO['editor'], 'sioc:modifier' => $dwuserpage_id));
338        }
339        /* is creator        */
340        if (isset($INFO['meta']['date']['created'])) {
341            $wikipage->isCreator();
342        }
343        /* intern wiki links */
344        $wikipage->addLinks($INFO['meta']['relation']['references']);
345
346        // contributors - only for last revision b/c of wrong meta data for older revisions
347        if (!$REV && $this->getConf('userns') && isset($INFO['meta']['contributor'])) {
348            $cont_temp = array();
349            $cont_ns   = $this->getConf('userns') . ($conf['useslash'] ? '/' : ':');
350            foreach ($INFO['meta']['contributor'] as $cont_id => $cont_name) {
351                $cont_temp[$cont_ns . $cont_id] = $cont_name;
352            }
353            $wikipage->addContributors($cont_temp);
354        }
355
356        // backlinks - only for last revision
357        if (!$REV) {
358            require_once(DOKU_INC . 'inc/fulltext.php');
359            $backlinks = ft_backlinks($ID);
360            if (count($backlinks) > 0) {
361                $wikipage->addBacklinks($backlinks);
362            }
363        }
364
365        // TODO: addLinksExtern
366
367        /* previous and next revision */
368        $changelog = new PageChangeLog($ID);
369        $pagerevs  = $changelog->getRevisions(0, $conf['recent'] + 1);
370        $prevrev   = false;
371        $nextrev   = false;
372        if (!$REV) {
373            // latest revision, previous rev is on top in array
374            $prevrev = 0;
375        } else {
376            // other revision
377            $currentrev = array_search($REV, $pagerevs);
378            if ($currentrev !== false) {
379                $prevrev = $currentrev + 1;
380                $nextrev = $currentrev - 1;
381            }
382        }
383        if ($prevrev !== false && $prevrev > -1 && page_exists($ID, $pagerevs[$prevrev])) {
384            /* previous revision*/
385            $wikipage->addVersionPrevious($pagerevs[$prevrev]);
386        }
387        if ($nextrev !== false && $nextrev > -1 && page_exists($ID, $pagerevs[$nextrev])) {
388            /* next revision*/
389            $wikipage->addVersionNext($pagerevs[$nextrev]);
390        }
391
392        /* latest revision   */
393        if ($REV) {
394            $wikipage->addVersionLatest();
395        }
396        // TODO: topics
397        /* has_container     */
398        if ($INFO['namespace']) {
399            $wikipage->addContainer($INFO['namespace']);
400        }
401        /* has_space         */
402        if ($this->getConf('owners')) {
403            $wikipage->addSite($this->getConf('owners'));
404        }
405        // TODO: dc:contributor / has_modifier
406        // TODO: attachment (e.g. pictures in that dwns)
407
408        // add wiki page to exporter
409        $exporter->addObject($wikipage);
410        //if ($INFO['editor'] && $this->getConf('userns')) $exporter->addObject($pageuser);
411
412        return $exporter;
413    }
414
415    private function getDokuUrl($url = null)
416    {
417        return getAbsUrl($url);
418    }
419
420    public function isRdfXmlRequest(): bool
421    {
422        if (!isset($_SERVER['HTTP_ACCEPT'])) {
423            return false;
424        }
425
426        // get accepted types
427        $http_accept = trim($_SERVER['HTTP_ACCEPT']);
428
429        // save accepted types in array
430        $accepted = explode(',', $http_accept);
431
432        if ($this->getConf('softck') && strpos($_SERVER['HTTP_ACCEPT'], 'application/rdf+xml') !== false) {
433            return true;
434        }
435
436        if (count($accepted) > 0) {
437            // hard check, only serve RDF if it is requested first or equal to first type
438
439            // extract accepting ratio
440            $test_accept = array();
441            foreach ($accepted as $format) {
442                $formatspec = explode(';', $format);
443                $k          = trim($formatspec[0]);
444                if (count($formatspec) === 2) {
445                    $test_accept[$k] = trim($formatspec[1]);
446                } else {
447                    $test_accept[$k] = 'q=1.0';
448                }
449            }
450
451            // sort by ratio
452            arsort($test_accept);
453            $accepted_order = array_keys($test_accept);
454
455            if ($accepted_order[0] === 'application/rdf+xml' || (array_key_exists(
456                'application/rdf+xml',
457                $test_accept
458            ) && $test_accept['application/rdf+xml'] === 'q=1.0')) {
459                return true;
460            }
461        }
462
463        // print_r($accepted_order);print_r($test_accept);die();
464
465        return false;
466    }
467
468    /**
469     */
470    public function createRdfLink($event = null, $param = null)
471    {
472        global $ID, $INFO, $conf;
473
474        // Test for hidden pages
475
476        if (isHiddenPage($ID)) {
477            return false;
478        }
479
480        // Get type of SIOC content
481
482        $sioc_type = $this->getContenttype();
483
484        // Test for valid types
485
486        if (!(($sioc_type === 'post' && $INFO['exists']) || $sioc_type === 'user' || $sioc_type === 'container')) {
487            return false;
488        }
489
490        // Test for permission
491
492        if (!$INFO['perm']) {
493            // not enough rights to see the wiki page
494            return false;
495        }
496
497        $userinfo = getDwUserInfo($ID, $this);
498
499        // Create attributes for meta link
500
501        $metalink['type'] = 'application/rdf+xml';
502        $metalink['rel']  = 'meta';
503
504        switch ($sioc_type) {
505            case 'container':
506                $title     = htmlentities(
507                    "Container '" . ($INFO['meta']['title'] ?? $ID) . "' (SIOC document as RDF/XML)"
508                );
509                $queryAttr = array('type' => 'container');
510                break;
511
512            case 'user':
513                $title     = htmlentities($userinfo['name']);
514                $queryAttr = array('type' => 'user');
515                break;
516
517            case 'post':
518            default:
519                $title     = htmlentities($INFO['meta']['title'] ?? $ID);
520                $queryAttr = array('type' => 'post');
521                if (isset($_GET['rev']) && $_GET['rev'] === (int)$_GET['rev']) {
522                    $queryAttr['rev'] = $_GET['rev'];
523                }
524                break;
525        }
526
527        $metalink['title'] = $title;
528        $metalink['href']  = normalizeUri(getAbsUrl(exportlink($ID, 'siocxml', $queryAttr, false, '&')));
529
530        if ($event !== null) {
531            $event->data['link'][] = $metalink;
532
533            // set canocial link for type URIs to prevent indexing double content
534            if ($_GET['type'] ?? "") {
535                $event->data['link'][] = array('rel' => 'canonical', 'href' => getAbsUrl(wl($ID)));
536            }
537        }
538
539        return $metalink;
540    }
541}
542
543// TODO cleanup and just have this unconditionally
544if (!function_exists('getAbsUrl')) {
545    /**
546     * @param $url
547     * @return string
548     * @deprecated cleanup, use build-in function
549     */
550    function getAbsUrl($url = null): string
551    {
552        if ($url === null) {
553            $url = DOKU_BASE;
554        }
555        return str_replace(DOKU_BASE, DOKU_URL, $url);
556    }
557}
558
559if (!function_exists('getDwUserEmail')) {
560    /**
561     * @param $user
562     * @return string
563     * @deprecated not used, will be removed
564     */
565    function getDwUserEmail($user): string
566    {
567        global $auth;
568        if ($info = $auth->getUserData($user)) {
569            return $info['mail'];
570        } else {
571            return false;
572        }
573    }
574}
575
576if (!function_exists('getDwUserInfo')) {
577    /**
578     * @param $id
579     * @param $pobj
580     * @param $key
581     * @return array|false
582     * @deprecated cleanup, use build-in function
583     */
584    function getDwUserInfo($id, $pobj, $key = null)
585    {
586        global $auth, $conf;
587
588        if (!$pobj->getConf('userns')) {
589            return false;
590        }
591
592        // get user id
593        $userid = str_replace(cleanID($pobj->getConf('userns')) . ($conf['useslash'] ? '/' : ':'), '', $id);
594
595        if ($info = $auth->getUserData($userid)) {
596            if ($key) {
597                return $info['key'];
598            } else {
599                return $info;
600            }
601        } else {
602            return false;
603        }
604    }
605}
606
607// sort query attributes by name
608if (!function_exists('normalizeUri')) {
609    /**
610     * @param $uri
611     * @return string
612     * @deprecated cleanup, use build-in function
613     */
614    function normalizeUri($uri): string
615    {
616        // part URI
617        $parts = explode('?', $uri);
618
619        // part query
620        if (isset($parts[1])) {
621            $query = $parts[1];
622
623            // test separator
624            $sep = '&';
625            if (strpos($query, '&amp;') !== false) {
626                $sep = '&amp;';
627            }
628            $attr = explode($sep, $query);
629
630            sort($attr);
631
632            $parts[1] = implode($sep, $attr);
633        }
634
635        return implode('?', $parts);
636    }
637}
638