1<?php
2
3if (!defined('DOKU_PLUGIN')) die('meh');
4
5use dokuwiki\File\PageResolver;
6
7class siteexport_toc
8{
9    private $emptyNSToc = true;
10    private $functions = null;
11    private $NS = null;
12    public $translation = null;
13
14    public function __construct($functions, $NS)
15    {
16        $this->doDebug = !empty($_REQUEST['tocDebug']);
17        $this->emptyNSToc = !empty($_REQUEST['emptyTocElem']);
18        $this->functions = $functions;
19        $this->NS = $NS;
20    }
21
22    private function isNotEmpty( $val ) {
23        return !empty($val);
24    }
25
26    private function shortenByTranslation(&$inputURL, $deepSearch = false)
27    {
28        // Mandatory: we allways want '/' insteadf of ':' here
29        $inputURL = str_replace(':', '/', $inputURL);
30
31        $checkArray = $this->translation ? $this->translation->translations : array(noNS($this->NS));
32
33        $url = explode('/', $inputURL);
34
35        $URLcount = count($url);
36        for ($i = 0; $i < $URLcount ; $i++)
37        {
38            if (in_array($url[$i], $checkArray))
39            {
40                // Rauswerfen und weg
41                $url[$i] = '';
42                break;
43            }
44
45            if (!$deepSearch)
46            {
47                break;
48            }
49
50            // Ok, remove anyway
51            $url[$i] = '';
52        }
53
54        $inputURL = implode('/', $url);
55        $inputURL = preg_replace("$\/+$", "/", $inputURL);
56
57        if (strlen($inputURL) > 0 && substr($inputURL, 0, 1) == '/')
58        {
59            $inputURL = substr($inputURL, 1);
60        }
61
62        return $inputURL;
63    }
64
65    /**
66     * Build the Java Documentation TOC XML
67     **/
68    public function __getJavaHelpTOCXML($DATA) {
69
70        if (count($DATA) == 0) {
71            return false;
72        }
73
74        $this->debug("#### STARTING ####");
75        $TOCXML = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<toc>";
76        $MAPXML = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<map version=\"1.0\">";
77
78        // Go through the pages
79        $CHECKDATA = array();
80        $nData = $DATA;
81        $DATA = array();
82        $check = array();
83        $startPageID = null;
84
85        foreach ( $nData as $elem )
86        {
87            // Check if available
88            $anchor = ( !empty($elem['anchor']) ? '#' . $elem['anchor'] : '' );
89            $elem['url'] = $this->functions->getSiteName($elem['id'], true); // Override - we need a clean name
90            $elem['mapURL'] = $elem['url'];
91            $this->shortenByTranslation($elem['url']);
92
93            // only add an url once
94            if ( in_array($elem['url'], $CHECKDATA) ) { continue; }
95
96            if ( !isset($elem['exists']) ) {
97                $elem['exists'] = page_exists( (new PageResolver( $elem['id'] ))->resolveId($elem['id']) );
98                $this->functions->debug->message("EXISTS previously not set.", $elem, 1);
99            }
100
101            // if not there, no map ids will be generated
102            $elem['mapID'] = intval($elem['exists']) == 1 ? $this->functions->getMapID($elem['id'], $elem['anchor'], $check) : array();
103            $elem['tags'] = explode(' ', p_get_metadata($elem['id'], 'context tags', true)); // thats from the tag plugin
104            $elem['tags'] = array_filter($elem['tags'], array($this, 'isNotEmpty'));
105            $elem['tags'] = array_map(array($this->functions, 'cleanId'), $elem['tags']);
106
107            if ( empty($elem['depth']) ) {
108                $elem['depth'] = count(explode('/', $elem['url']));
109            }
110            $CHECKDATA[] = $elem['url'];
111
112            if ( $startPageID == null )
113            {
114                $startPageID = $elem['mapID'][0];
115            }
116
117            // Only overwrite name when none was provided (e.g. link without title).
118            // When the toc link has an explicit title (e.g. [[.:configuration|configuration]]), keep it
119            // so the tocitem target stays lowercased and matches map.xml convention.
120            if ( empty( $elem['name'] ) ) {
121                $elem['name'] = $this->functions->getSiteTitle($elem['id']);
122
123                if ( is_array($elem['mapID']) && empty( $elem['mapID'] ) ) {
124                    array_push($elem['mapID'], noNs($elem['id']));
125                }
126
127                $this->debug("no name, get site title " . $elem['name']);
128                $this->debug($elem);
129            }
130
131            // Go on building mapXML
132            $this->shortenByTranslation($elem['mapURL'], true); // true to already remove all language stuff - false if not
133            foreach ( $elem['mapID'] as $VIEWID ) {
134                $MAPXML .= "\n\t<mapID target=\"" . $VIEWID . "\" url=\"" . $elem['mapURL'] . $anchor . "\"/>";
135            }
136
137            $elem['tocNS'] = getNS(cleanID($elem['url']));
138            $elem['tocNS'] = $this->shortenByTranslation($elem['tocNS'], true);
139            $elem['tocNS'] = strlen($elem['tocNS']) > 0 ? explode('/', $elem['tocNS']) : array();
140            $this->functions->debug->message("This will be the TOC elements data:", $elem, 1);
141
142            $this->__buildTOCTree($DATA, $elem['tocNS'], $elem);
143        }
144
145        $this->debug("#### Writing TOC Tree ####");
146        $TOCXML .= $this->__writeTOCTree($DATA) . "\n</toc>";
147        $this->debug("#### DONE: Writing TOC Tree ####");
148        $MAPXML .= "\n</map>";
149
150        $this->debug($DATA);
151        $this->debug($TOCXML);
152        $this->debug($MAPXML);
153
154        return array($TOCXML, $MAPXML, $startPageID);
155    }
156
157    /**
158     * Prepare the TOC Tree
159     **/
160    private function __buildTOCTree(&$DATA, $currentNSArray, $elemToAdd)
161    {
162        global $conf;
163
164        // Actual level
165        if (empty($currentNSArray)) {
166            $elemToAdd['isStartPage'] = noNS($elemToAdd['id']) == $conf['start'];
167            // $key = empty($elemToAdd['name']) || 1==1 ? noNS($elemToAdd['id']) : $elemToAdd['name'];
168            $key = noNS($elemToAdd['id']);
169            $DATA[$key] = $elemToAdd;
170            return;
171        }
172
173        $currentLevel = array_shift($currentNSArray);
174        $nextLevel = &$DATA[$currentLevel];
175        if (empty($nextLevel)) {
176            $nextLevel = array('pages' => array());
177        } else {
178            $nextLevel = &$DATA[$currentLevel]['pages'];
179        }
180
181        $this->__buildTOCTree($nextLevel, $currentNSArray, $elemToAdd);
182    }
183
184    /**
185     * Create a single TOC Item
186     **/
187    private function __TOCItem($item, $depth, $selfClosed = true)
188    {
189        $this->debug("creating toc item");
190        $this->debug($item);
191        $targetID = $item['mapID'][0] ?? '';
192        if (empty($targetID)) {
193            $targetID = $this->functions->cleanID($item['name']);
194            $this->debug("no map ID, using: " . $targetID);
195        }
196        return "\n" . str_repeat("\t", max($depth, 0)+1) . "<tocitem target=\"" . $targetID . "\"" . (intval($item['exists']) == 1 ? " text=\"" . $item['name'] . "\"" : "") . ( array_key_exists('tags', $item) && !empty($item['tags']) ? " tags=\"" . implode(' ', $item['tags']) . "\"": "")  . ($selfClosed ? '/' : '') . ">";
197    }
198
199    /**
200     * Create a single TOC Item
201     **/
202    private function __TOCItemClose($depth)
203    {
204        return "\n" . str_repeat("\t", max($depth, 0)+1) . "</tocitem>";
205    }
206
207    /**
208     * Write the whole TOC TREE
209     **/
210    private function __writeTOCTree($CURRENTNODE, $CURRENTNODENAME = null, $DEPTH = 0) {
211        global $conf;
212
213        $XML = '';
214        $didOpenItem = false;
215        if (!is_array($CURRENTNODE) || empty($CURRENTNODE))
216        {
217            // errr … no.
218            return $XML;
219        }
220
221        // This is an element!
222        if (!empty($CURRENTNODE['id']) && empty($CURRENTNODE['pages']))
223        {
224            // This has to be an item - only -!
225            return $this->__TOCItem($CURRENTNODE, $DEPTH);
226        }
227
228        // Look for start page
229        if (!empty($CURRENTNODE[$conf['start']]))
230        {
231            // YAY! StartPage found.
232            $didOpenItem = !(count(empty($CURRENTNODE['pages']) ? $CURRENTNODE : $CURRENTNODE['pages']) == 0);
233            $XML .= $this->__TOCItem($CURRENTNODE[$conf['start']], $DEPTH, !$didOpenItem);
234            unset($CURRENTNODE[$conf['start']]);
235        } else if (!empty($CURRENTNODE['element'])) {
236            $didOpenItem = !(count($CURRENTNODE['pages']) == 0);
237            $XML .= $this->__TOCItem($CURRENTNODE['element'], $DEPTH, !$didOpenItem);
238            unset($CURRENTNODE['element']);
239        } else if ($CURRENTNODENAME != null) {
240            // We have a parent node for what is comming … lets honor that
241            $didOpenItem = !(count($CURRENTNODE) == 0);
242            $XML .= $this->__TOCItem(array('name' => $CURRENTNODENAME), $DEPTH, !$didOpenItem);
243        } else {
244            // Woohoo … empty node? do not count up!
245            $DEPTH--;
246        }
247
248        $this->debug("-- This is the current node --");
249        $this->debug($CURRENTNODE);
250
251        // Circle through the entries
252        foreach (empty($CURRENTNODE['pages']) ? $CURRENTNODE : $CURRENTNODE['pages'] as $NODENAME => $ELEM)
253        {
254            // a node should have more than only one entry … otherwise we will not tell our name!
255            $XML .= $this->__writeTOCTree($ELEM, count($ELEM) >= 1 ? ( !empty($ELEM['name']) ? $ELEM['name'] : $NODENAME ) : null, $DEPTH+1);
256        }
257
258        // Close and return
259        return $XML . ($didOpenItem ? $this->__TOCItemClose($DEPTH) : '');
260    }
261
262    /**
263     * Build the Eclipse Documentation TOC XML
264     **/
265    public function __getTOCXML($DATA, $XML = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<?NLS TYPE=\"org.eclipse.help.toc\"?>\n") {
266
267        $pagesArray = array();
268
269        // Go through the pages
270        foreach ($DATA as $elem) {
271
272            $site = $elem['id'];
273            $elems = explode('/', $this->functions->getSiteName($site));
274
275            // Strip Site
276            array_pop($elems);
277
278            // build the topic Tree
279            $this->__buildTopicTree($pagesArray, $elems, $site);
280        }
281
282        $XML .= $this->__addXMLTopic($pagesArray, 'toc');
283
284        return $XML;
285
286    }
287
288    /**
289     * Load the topic Tree for the TOC - recursive
290     **/
291    private function __buildTopicTree(&$PAGES, $DATA, $SITE, $INSERTDATA = null) {
292
293        if (empty($DATA) || !is_array($DATA)) {
294
295            if ($INSERTDATA == null)
296            {
297                $INSERTDATA = $SITE;
298            }
299
300            // This is already a namespace
301            if (is_array($PAGES[noNS($SITE)])) {
302                // The root already exists!
303                if (!empty($PAGES[noNS($SITE)][noNS($SITE)])) {
304                    if (strstr($PAGES[noNS($SITE)][noNS($SITE)], $SITE)) {
305                        // The SITE is in the parent Namespace, and the current Namespace has an index with same name
306                        $PAGES['__' . noNS($SITE)] = $INSERTDATA;
307                    } else {
308                        $PAGES['__' . noNS($SITE)] = $PAGES[noNS($SITE)][noNS($SITE)];
309                        $PAGES[noNS($SITE)][noNS($SITE)] = $INSERTDATA;
310                    }
311                } else {
312                    $PAGES[noNS($SITE)][noNS($SITE)] = $INSERTDATA;
313                }
314            } else {
315                // just a Page
316                $PAGES[noNS($SITE)] = $INSERTDATA;
317            }
318            return;
319        }
320
321        $NS = array_shift($DATA);
322        if (!is_array($PAGES[$NS])) $PAGES[$NS] = empty($PAGES[$NS]) ? array() : array($PAGES[$NS]);
323        $this->__buildTopicTree($PAGES[$NS], $DATA, $SITE, $INSERTDATA);
324
325        return;
326    }
327
328    /**
329     * Build the Topic Tree for TOC.xml
330     **/
331    private function __addXMLTopic($DATA, $ITEM = 'topic', $LEVEL = 0, $NODENAME = '') {
332        global $conf;
333
334        $DEPTH = str_repeat("\t", $LEVEL);
335
336        if (!is_array($DATA)) {
337            return $DEPTH . '<' . $ITEM . ' label="' . $this->functions->getSiteTitle($DATA) . '" ' . ($ITEM != 'topic' ? 'topic' : 'href') . '="' . $this->functions->getSiteName($DATA) . "\" />\n";
338        }
339        // Is array from this point on
340        list($indexTitle, $indexFile) = $this->__getIndexItem($DATA, $NODENAME);
341
342        if (empty($indexTitle)) $indexTitle = $this->functions->getSiteTitle($conf['start']);
343        if (!empty($indexFile)) $indexFile = ($ITEM != 'topic' ? 'topic' : 'href') . "=\"$indexFile\"";
344
345        $isEmptyNode = count($DATA) == 1 && empty($indexFile);
346
347        if (!$isEmptyNode && ($this->emptyNSToc || count($DATA) > 0)) {
348            $XML = "$DEPTH<$ITEM label=\"$indexTitle\" $indexFile>";
349        } else {
350            $XML = "";
351        }
352
353        if (!$isEmptyNode && count($DATA) > 0) $XML .= "\n";
354
355        foreach ($DATA as $NODENAME => $NS) {
356            $XML .= $this->__addXMLTopic($NS, (!($this->emptyNSToc || count($DATA) > 1) && $ITEM != 'topic' ? $ITEM : 'topic'), $LEVEL+(!$isEmptyNode ? 1 : 0), $NODENAME);
357        }
358
359        if (!$isEmptyNode && count($DATA) > 0) $XML .= "$DEPTH";
360        if (!$isEmptyNode && ($this->emptyNSToc || count($DATA) > 0)) {
361            $XML .= "</$ITEM>\n";
362        }
363
364        return $XML;
365    }
366
367
368    /**
369     * Get the context XML
370     **/
371    public function __getContextXML($DATA) {
372
373        $XML = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<?NLS TYPE=\"org.eclipse.help.context\"?>\n<contexts>\n";
374
375        $check = array();
376        foreach ($DATA as $elem)
377        {
378            $ID = $elem['id'];
379            $meta = p_get_metadata($ID, 'context', true);
380            if (empty($meta['id'])) { continue; }
381
382            $TITLE = empty($meta['title']) ? $this->functions->getSiteTitle($ID) : $meta['title'];
383
384            // support more than one view IDs ... for more than one reference
385            $VIEWIDs = $this->functions->getMapID($elem['id'], $elem['anchor'], $check);
386
387            $DESCRIPTION = $this->functions->xmlEntities(p_get_metadata($ID, 'description abstract'));
388
389            // Build topic Links
390            $url = $this->functions->getSiteName($ID);
391            $this->shortenByTranslation($url);
392
393            $TOPICS = array($url => $TITLE . " (Details)");
394            $REFS = p_get_metadata($ID, 'relation references', true);
395            if (is_array($REFS))
396            foreach ($REFS as $REL => $EXISTS) {
397                if (!$EXISTS) { continue; }
398                $TOPICS[$this->functions->getSiteName($REL)] = $this->functions->getSiteTitle($REL);
399            }
400
401            // build XML - include multi view IDs
402            foreach ($VIEWIDs as $VIEWID) {
403                $XML .= "\t<context id=\"$VIEWID\" title=\"$TITLE\">\n";
404                $XML .= "\t\t<description>$DESCRIPTION</description>\n";
405
406                foreach ($TOPICS as $URL => $LABEL) {
407                    $XML .= "\t\t<topic label=\"$LABEL\" href=\"$URL\" />\n";
408                }
409
410                $XML .= "\t</context>\n";
411            }
412        }
413
414        $XML .= "</contexts>";
415        return $XML;
416
417    }
418
419    /**
420     * Determine if this is an index - and if so, find its Title
421     **/
422    private function __getIndexItem(&$DATA, $NODENAME = '') {
423        global $conf;
424
425        if (!is_array($DATA)) { return; }
426
427        $indexTitle = '';
428        $indexFile = '';
429        foreach ($DATA as $NODE => $indexSearch) {
430            // Skip next Namespaces
431            if (is_array($indexSearch)) { continue; }
432
433            // Skip if this is not a start
434            if ($NODE != $conf['start']) { continue; }
435
436            $indexTitle = $this->functions->getSiteTitle($indexSearch);
437            $indexFile = $indexSearch;
438            unset($DATA[$NODE]);
439            break;
440        }
441
442        if (empty($indexFile) && !empty($DATA[$NODENAME])) {
443            $indexTitle = $this->functions->getSiteTitle($DATA[$NODENAME]);
444            $indexFile = $DATA[$NODENAME];
445            unset($DATA[$NODENAME]);
446        }
447
448        return array($indexTitle, $this->functions->getSiteName($indexFile));
449    }
450
451    private $doDebug = false;
452    private static $didDebug = false;
453    public function debug($data, $final = false) {
454        if ( ! $this->doDebug ) { return; }
455
456        if ( !$this->didDebug ) {
457            print "<html><pre>";
458            $this->didDebug = true;
459        }
460
461        if ( is_array($data) ) {
462            print_r($data);
463        } else {
464            print str_replace("<", "&lt;", str_replace(">", "&gt;", $data));;
465        }
466
467        print "\n\n";
468
469        if ( $final ) {
470            print "</pre></html>";
471            exit;
472        }
473    }
474}
475