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