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, normalizeUri($exporter->siocURL('container', $ID))
207        );
208
209        /* container is type=wiki */
210        if ($ID == $conf['start']) {
211            $wikicontainer->isWiki();
212        }
213        /* sioc:name              */
214        if ($INFO['exists']) {
215            $wikicontainer->addTitle($INFO['meta']['title']);
216        }
217        /* has_parent             */
218        if ($INFO['namespace']) {
219            $wikicontainer->addParent($INFO['namespace']);
220        }
221
222        // search next level entries (posts, sub containers) in container
223        require_once(DOKU_INC . 'inc/search.php');
224        $dir        = utf8_encodeFN(str_replace(':', '/', $ID));
225        $entries    = array();
226        $posts      = array();
227        $containers = array();
228        search($entries, $conf['datadir'], 'search_index', array('ns' => $ID), $dir);
229        foreach ($entries as $entry) {
230            if ($entry['type'] === 'f') {
231                // wikisite
232                $posts[] = $entry;
233            } elseif ($entry['type'] === 'd') {
234                // sub container
235                $containers[] = $entry;
236            }
237        }
238
239        // without sub content it can't be a container (so it does not exist as a container)
240        if (count($posts) + count($containers) == 0) {
241            $this->exit("HTTP/1.0 404 Not Found");
242        }
243
244        if (count($posts) > 0) {
245            $wikicontainer->addArticles($posts);
246        }
247        if (count($containers) > 0) {
248            $wikicontainer->addContainers($containers);
249        }
250
251        //print_r($containers);die();
252
253        // add container to exporter
254        $exporter->addObject($wikicontainer);
255
256        return $exporter;
257    }
258
259    /* -- private helpers --------------------------------------------------- */
260
261    private function exportUsercontent($exporter)
262    {
263        global $ID;
264
265        // get user info
266        $userinfo = getDwUserInfo($ID, $this);
267
268        // no userinfo means there is no user space or user does not exists
269        if ($userinfo === false) {
270            $this->exit("HTTP/1.0 404 Not Found");
271        }
272
273        $exporter->setParameters(
274            'Account: ' . $userinfo['name'],
275            getAbsUrl(),
276            getAbsUrl() . 'doku.php?',
277            'utf-8',
278            $this->agentlink
279        );
280        // create user object
281        //print_r($userinfo); die();
282        // $id, $url, $userid, $name, $email
283        $wikiuser = new SIOCDokuWikiUser(
284            $ID, normalizeUri($exporter->siocURL('user', $ID)), $userinfo['user'], $userinfo['name'], $userinfo['mail']
285        );
286        /* TODO: avatar (using Gravatar) */ /* TODO: creator_of */
287        // add user to exporter
288        $exporter->addObject($wikiuser);
289
290        //print_r(headers_list());die();
291        return $exporter;
292    }
293
294    private function exportPostcontent($exporter)
295    {
296        global $ID, $INFO, $REV, $conf;
297
298        $exporter->setParameters(
299            $INFO['meta']['title'] . ($REV ? ' (rev ' . $REV . ')' : ''),
300            $this->getDokuUrl(),
301            $this->getDokuUrl() . 'doku.php?',
302            'utf-8',
303            $this->agentlink
304        );
305
306        // create user object
307        $dwuserpage_id = cleanID($this->getConf('userns')) . ($conf['useslash'] ? '/' : ':') . $INFO['editor'];
308        // create wiki page object
309        $wikipage = new SIOCDokuWikiArticle(
310            $ID, // id
311            normalizeUri(
312                $exporter->siocURL(
313                    'post',
314                    $ID . ($REV ? $exporter->_urlseparator . 'rev' . $exporter->_urlequal . $REV : '')
315                )
316            ), // url
317            $INFO['meta']['title'] . ($REV ? ' (rev ' . $REV . ')' : ''), // subject
318            rawWiki($ID, $REV) // body (content)
319        );
320        /* encoded content   */
321        $wikipage->addContentEncoded(p_cached_output(wikiFN($ID, $REV), 'xhtml'));
322        /* created           */
323        if (isset($INFO['meta']['date']['created'])) {
324            $wikipage->addCreated(date('c', $INFO['meta']['date']['created']));
325        }
326        /* or modified       */
327        if (isset($INFO['meta']['date']['modified'])) {
328            $wikipage->addModified(date('c', $INFO['meta']['date']['modified']));
329        }
330        /* creator/modifier  */
331        if ($INFO['editor'] && $this->getConf('userns')) {
332            $wikipage->addCreator(array('foaf:maker' => '#' . $INFO['editor'], 'sioc:modifier' => $dwuserpage_id));
333        }
334        /* is creator        */
335        if (isset($INFO['meta']['date']['created'])) {
336            $wikipage->isCreator();
337        }
338        /* intern wiki links */
339        $wikipage->addLinks($INFO['meta']['relation']['references']);
340
341        // contributors - only for last revision b/c of wrong meta data for older revisions
342        if (!$REV && $this->getConf('userns') && isset($INFO['meta']['contributor'])) {
343            $cont_temp = array();
344            $cont_ns   = $this->getConf('userns') . ($conf['useslash'] ? '/' : ':');
345            foreach ($INFO['meta']['contributor'] as $cont_id => $cont_name) {
346                $cont_temp[$cont_ns . $cont_id] = $cont_name;
347            }
348            $wikipage->addContributors($cont_temp);
349        }
350
351        // backlinks - only for last revision
352        if (!$REV) {
353            require_once(DOKU_INC . 'inc/fulltext.php');
354            $backlinks = ft_backlinks($ID);
355            if (count($backlinks) > 0) {
356                $wikipage->addBacklinks($backlinks);
357            }
358        }
359
360        // TODO: addLinksExtern
361
362        /* previous and next revision */
363        $changelog = new PageChangeLog($ID);
364        $pagerevs  = $changelog->getRevisions(0, $conf['recent'] + 1);
365        $prevrev   = false;
366        $nextrev   = false;
367        if (!$REV) {
368            // latest revision, previous rev is on top in array
369            $prevrev = 0;
370        } else {
371            // other revision
372            $currentrev = array_search($REV, $pagerevs);
373            if ($currentrev !== false) {
374                $prevrev = $currentrev + 1;
375                $nextrev = $currentrev - 1;
376            }
377        }
378        if ($prevrev !== false && $prevrev > -1 && page_exists($ID, $pagerevs[$prevrev])) {
379            /* previous revision*/
380            $wikipage->addVersionPrevious($pagerevs[$prevrev]);
381        }
382        if ($nextrev !== false && $nextrev > -1 && page_exists($ID, $pagerevs[$nextrev])) {
383            /* next revision*/
384            $wikipage->addVersionNext($pagerevs[$nextrev]);
385        }
386
387        /* latest revision   */
388        if ($REV) {
389            $wikipage->addVersionLatest();
390        }
391        // TODO: topics
392        /* has_container     */
393        if ($INFO['namespace']) {
394            $wikipage->addContainer($INFO['namespace']);
395        }
396        /* has_space         */
397        if ($this->getConf('owners')) {
398            $wikipage->addSite($this->getConf('owners'));
399        }
400        // TODO: dc:contributor / has_modifier
401        // TODO: attachment (e.g. pictures in that dwns)
402
403        // add wiki page to exporter
404        $exporter->addObject($wikipage);
405        //if ($INFO['editor'] && $this->getConf('userns')) $exporter->addObject($pageuser);
406
407        return $exporter;
408    }
409
410    private function getDokuUrl($url = null)
411    {
412        return getAbsUrl($url);
413    }
414
415    public function isRdfXmlRequest(): bool
416    {
417        // get accepted types
418        $http_accept = trim($_SERVER['HTTP_ACCEPT']);
419
420        // save accepted types in array
421        $accepted = explode(',', $http_accept);
422
423        if ($this->getConf('softck') && strpos($_SERVER['HTTP_ACCEPT'], 'application/rdf+xml') !== false) {
424            return true;
425        }
426
427        if (count($accepted) > 0) {
428            // hard check, only serve RDF if it is requested first or equal to first type
429
430            // extract accepting ratio
431            $test_accept = array();
432            foreach ($accepted as $format) {
433                $formatspec = explode(';', $format);
434                $k          = trim($formatspec[0]);
435                if (count($formatspec) === 2) {
436                    $test_accept[$k] = trim($formatspec[1]);
437                } else {
438                    $test_accept[$k] = 'q=1.0';
439                }
440            }
441
442            // sort by ratio
443            arsort($test_accept);
444            $accepted_order = array_keys($test_accept);
445
446            if ($accepted_order[0] === 'application/rdf+xml' || (array_key_exists(
447                        'application/rdf+xml',
448                        $test_accept
449                    ) && $test_accept['application/rdf+xml'] === 'q=1.0')) {
450                return true;
451            }
452        }
453
454        // print_r($accepted_order);print_r($test_accept);die();
455
456        return false;
457    }
458
459    /**
460     */
461    public function createRdfLink($event = null, $param = null)
462    {
463        global $ID, $INFO, $conf;
464
465        // Test for hidden pages
466
467        if (isHiddenPage($ID)) {
468            return false;
469        }
470
471        // Get type of SIOC content
472
473        $sioc_type = $this->getContenttype();
474
475        // Test for valid types
476
477        if (!(($sioc_type === 'post' && $INFO['exists']) || $sioc_type === 'user' || $sioc_type === 'container')) {
478            return false;
479        }
480
481        // Test for permission
482
483        if (!$INFO['perm']) {
484            // not enough rights to see the wiki page
485            return false;
486        }
487
488        $userinfo = getDwUserInfo($ID, $this);
489
490        // Create attributes for meta link
491
492        $metalink['type'] = 'application/rdf+xml';
493        $metalink['rel']  = 'meta';
494
495        switch ($sioc_type) {
496            case 'container':
497                $title     = htmlentities(
498                    "Container '" . ($INFO['meta']['title'] ?? $ID) . "' (SIOC document as RDF/XML)"
499                );
500                $queryAttr = array('type' => 'container');
501                break;
502
503            case 'user':
504                $title     = htmlentities($userinfo['name']);
505                $queryAttr = array('type' => 'user');
506                break;
507
508            case 'post':
509            default:
510                $title     = htmlentities($INFO['meta']['title'] ?? $ID);
511                $queryAttr = array('type' => 'post');
512                if (isset($_GET['rev']) && $_GET['rev'] === (int)$_GET['rev']) {
513                    $queryAttr['rev'] = $_GET['rev'];
514                }
515                break;
516        }
517
518        $metalink['title'] = $title;
519        $metalink['href']  = normalizeUri(getAbsUrl(exportlink($ID, 'siocxml', $queryAttr, false, '&')));
520
521        if ($event !== null) {
522            $event->data['link'][] = $metalink;
523
524            // set canocial link for type URIs to prevent indexing double content
525            if ($_GET['type'] ?? "") {
526                $event->data['link'][] = array('rel' => 'canonical', 'href' => getAbsUrl(wl($ID)));
527            }
528        }
529
530        return $metalink;
531    }
532}
533
534// TODO cleanup and just have this unconditionally
535if (!function_exists('getAbsUrl')) {
536    /**
537     * @param $url
538     * @return string
539     * @deprecated cleanup, use build-in function
540     */
541    function getAbsUrl($url = null): string
542    {
543        if ($url === null) {
544            $url = DOKU_BASE;
545        }
546        return str_replace(DOKU_BASE, DOKU_URL, $url);
547    }
548}
549
550if (!function_exists('getDwUserEmail')) {
551    /**
552     * @param $user
553     * @return string
554     * @deprecated not used, will be removed
555     */
556    function getDwUserEmail($user): string
557    {
558        global $auth;
559        if ($info = $auth->getUserData($user)) {
560            return $info['mail'];
561        } else {
562            return false;
563        }
564    }
565}
566
567if (!function_exists('getDwUserInfo')) {
568    /**
569     * @param $id
570     * @param $pobj
571     * @param $key
572     * @return array|false
573     * @deprecated cleanup, use build-in function
574     */
575    function getDwUserInfo($id, $pobj, $key = null)
576    {
577        global $auth, $conf;
578
579        if (!$pobj->getConf('userns')) {
580            return false;
581        }
582
583        // get user id
584        $userid = str_replace(cleanID($pobj->getConf('userns')) . ($conf['useslash'] ? '/' : ':'), '', $id);
585
586        if ($info = $auth->getUserData($userid)) {
587            if ($key) {
588                return $info['key'];
589            } else {
590                return $info;
591            }
592        } else {
593            return false;
594        }
595    }
596}
597
598// sort query attributes by name
599if (!function_exists('normalizeUri')) {
600    /**
601     * @param $uri
602     * @return string
603     * @deprecated cleanup, use build-in function
604     */
605    function normalizeUri($uri): string
606    {
607        // part URI
608        $parts = explode('?', $uri);
609
610        // part query
611        if (isset($parts[1])) {
612            $query = $parts[1];
613
614            // test separator
615            $sep = '&';
616            if (strpos($query, '&amp;') !== false) {
617                $sep = '&amp;';
618            }
619            $attr = explode($sep, $query);
620
621            sort($attr);
622
623            $parts[1] = implode($sep, $attr);
624        }
625
626        return implode('?', $parts);
627    }
628}
629
630