1<?php
2
3/**
4 * A Mindmap plugin using Graphviz.
5 *
6 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 * @author     Jannes Drost-Tenfelde <info@drost-tenfelde.de>
8 */
9if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
10if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
11require_once(DOKU_PLUGIN.'syntax.php');
12require_once(DOKU_INC.'inc/init.php');
13require_once(DOKU_INC.'inc/cliopts.php');
14
15/**
16 * Mindmap plugin.
17 *
18 */
19class syntax_plugin_mindmap extends DokuWiki_Syntax_Plugin {
20
21    /**
22     * Returns plugin information.
23     *
24     * @return array with plugin information.
25     */
26    function getInfo() {
27        return array(
28                'author' => 'Jannes Drost-Tenfelde',
29                'email'  => 'info@drost-tenfelde.de',
30                'date'   => '2011-10-11',
31                'name'   => 'mindmap',
32                'desc'   => 'This plugin allows you to make mindmaps of your website.',
33                'url'    => 'http://www.drost-tenfelde.de/?id=dokuwiki:plugins:mindmap',
34        );
35    }
36
37    /**
38	 * Returns the syntax type of the plugin.
39	 *
40	 * @return type.
41	 */
42	function getType(){
43		return 'substition';
44	}
45
46	/**
47	 * Returns the paragraph type.
48	 *
49	 * @return paragraph type.
50	 */
51	function getPType(){
52		return 'block';
53	}
54
55	/**
56	 * Where to sort in?
57	 */
58	function getSort(){
59		return 301;
60	}
61
62  /**
63	 * Connects the syntax pattern to the lexer.
64	 */
65	function connectTo($mode) {
66		$this->Lexer->addSpecialPattern('\{\{mindmap>[^}]*\}\}', $mode, 'plugin_mindmap');
67	}
68
69	/**
70	 * Handles the matched pattern.
71	 *
72	 */
73	function handle($match, $state, $pos, &$handler){
74		global $ID;
75
76		$info = $this->getInfo();
77
78        //strip markup from start and end
79		$match = substr($match, 10, -2);
80
81        // Assemble data
82		$data = array();
83
84        // Get namespaces and parameters
85        list($data['namespaces'], $parameters_string) = explode('#', $match);
86        $data['namespaces'] = str_replace('&', ',', $data['namespaces']);
87
88		parse_str($parameters_string, $params);
89
90		// Add the default values
91        $data['height'] = 0;
92        $data['width'] = 0;
93        $data['align'] = '';
94        $data['format'] = 'dot';
95        $data['depth'] = 3;
96        $data['include_media'] = 'none';
97        $data['use_cached_pages'] = 1;
98
99		// Get the parameters (key=value), seperated by &
100		$pairs = explode('&', $parameters_string);
101		// Turn the pairs into key=>value
102        foreach ($pairs as $pair) {
103			list($key, $value) = explode('=', $pair, 2);
104			$data[trim($key)] = trim($value);
105        }
106		// Turn all keys to lower case
107        $data = array_change_key_case($data, CASE_LOWER);
108		return $data;
109	}
110
111	/**
112	 * Renders the output.
113	 *
114	 */
115    function render($mode, &$renderer, $data) {
116        global $conf;
117		global $lang;
118
119		if ($mode == 'xhtml') {
120		    // Get the path
121		    $plugin_path = DOKU_BASE.'lib/plugins/mindmap/';
122            // Was a different location for the plugin set?
123            if ( $this->getConf('use_plugin_path') == 1) {
124                $conf_plugin_path = $this->getConf('plugin_path');
125
126                // Make sure the plugin path is set
127                if ( $conf_plugin_path != '' ) {
128                    // Make sure there is a trailing /
129                    if ( $conf_plugin_path[strlen($conf_plugin_path) - 1] != '/' ) {
130                        $conf_plugin_path .= '/';
131                    }
132                    $plugin_path = $conf_plugin_path;
133                }
134            }
135
136            if ( $data['format'] == 'gexf') {
137                // Add a link to the GEXF XML file
138                $xml = $plugin_path.'xml.php?'.buildURLparams($data);
139                $renderer->doc .= '<p><a href="'.$xml.'" target="_blank">'.$this->getLang('gexf_mindmap').'</a></p>';
140            }
141            else {
142                // Force dot format
143                $data['format'] = 'dot';
144
145                $img = $plugin_path.'img.php?'.buildURLparams($data);
146
147                if ( ($data['height'] != 0) || ($data['width'] != 0) ) {
148                    // Add a link
149                    $renderer->doc .= '<a href="'.$img.'" target="_blank" border="0">';
150                }
151
152                // Add the image
153                $renderer->doc .= '<img src="'.$img.'" class="media'.$data['align'].'" alt=""';
154                if($data['width'])  $renderer->doc .= ' width="'.$data['width'].'"';
155                if($data['height']) $renderer->doc .= ' height="'.$data['height'].'"';
156                if($data['align'] == 'right') $renderer->doc .= ' align="right"';
157                if($data['align'] == 'left')  $renderer->doc .= ' align="left"';
158                $renderer->doc .= '/>';
159
160                if ( ($data['height'] != 0) || ($data['width'] != 0) ) {
161                    // Close the link
162                    $renderer->doc .= '</a>';
163                }
164            }
165            return true;
166        }
167		return false;
168	}
169
170	/**
171	 * Wrapper which retruns the appropriate gathered data based on parameters.
172	 *
173	 * @param data plugin data
174	 * @return gathered pages and media.
175	 */
176	function get_gathered_data( $data ) {
177        // Use cached pages?
178        $use_cached_pages = true;
179        if ( $data['use_cached_pages'] == 0 )
180        {
181            // Safeguard that cache is used if no namespaces were given
182            if ( ($data['namespaces'] == '') || ($data['namespaces'] == ':') ) {
183                $use_cached_pages = true;
184            }
185            else {
186                $use_cached_pages = false;
187            }
188        }
189
190        // Use first page header?
191        $use_first_header = false;
192        if ( $data['use_first_header'] == 1 ) {
193            $use_first_header = true;
194        }
195
196        //Make a namespace array
197        $namespaces = explode(',', $data['namespaces']);
198
199        // Gather page/media data
200        $gathered_data = $this->gather_data(
201            $namespaces, $data['depth'],
202            $data['include_media'],
203            $use_cached_pages,
204            $use_first_header
205        );
206        return $gathered_data;
207    }
208
209    /**
210     * Returns the GEXF xml file.
211     *
212     * @param data parameters.
213     *
214     * @return XML.
215     */
216    function get_gexf_xml( $data ) {
217        global $conf;
218
219        $image = null;
220
221        $gathered_data = $this->get_gathered_data( $data );
222
223        $xml = $this->get_gexf( $gathered_data );
224
225        return $xml;
226    }
227
228    /**
229     * Returns the content of a graphviz image.
230     *
231     * @param data parameters.
232     *
233     * @return PNG image.
234     */
235    function get_graphviz_image( $data ) {
236        global $conf;
237
238        $image = null;
239
240        $gathered_data = $this->get_gathered_data( $data );
241        $dot_input = $this->get_dot( $gathered_data );
242
243        // See if a manual path was given for graphviz
244
245        if ( $this->getConf('graphviz_path') ) {
246            // Local build
247            $cmd  = $this->getConf('path');
248            $cmd .= ' -Tpng';
249            $cmd .= ' -K'.$data['layout'];
250            $cmd .= ' -o'.escapeshellarg($image); //output
251            $cmd .= ' '.escapeshellarg($dot_input); //input
252
253            exec($cmd, $image, $error);
254
255            if ($error != 0){
256                if($conf['debug']) {
257                    dbglog(join("\n",$image),'mindmap command failed: '.$cmd);
258                }
259                return false;
260            }
261        }
262        else {
263            // Remote via google chart tools
264            $http = new DokuHTTPClient();
265            $http->timeout=30;
266
267            $pass = array();
268            $pass['cht'] = 'gv:'.$data['format'];
269            $pass['chl'] = $dot_input;
270
271            $image = $http->post('http://chart.apis.google.com/chart',$pass,'&');
272            if(!$image) return false;
273        }
274        return $image;
275    }
276
277    /**
278     * Searches a namespace for media files and adds them to the media array.
279     *
280     * @param media pre-initialised media array.
281     * @param ns namespace in which to search for media files
282     * @param depth Depth of the search.
283     */
284    function get_media( &$media, $ns, $depth=0 )
285    {
286        global $conf;
287
288        $search_results = array();
289        // Search all media files within the namespace
290        search($search_results,
291            $conf['mediadir'],
292            'search_universal',
293            array (
294                'depth' => $depth,
295                'listfiles' => true,
296                'listdirs'  => false,
297                'pagesonly' => false,
298                'skipacl'   => true,
299                'keeptxt'   => true,
300                'meta'      => true,
301            ),
302            // Only search within the namespace
303            str_replace(':', '/', $ns)
304        );
305
306        // Loop through the results
307        while( $item = array_shift($search_results) ) {
308            // Make a new media[id]=>array(title,size,ns,time) for the item
309            $media[$item['id']] = array(
310                'title' => noNS($item['id']),
311                'size'  => $item['size'],
312                'ns'    => getNS($item['id']),
313                'time'  => $item['mtime'],
314            );
315        }
316    }
317
318    /**
319     * Adds all pages of a specific namespace to the pages array.
320     *
321     * @param pages pre-initialised pages array.
322     * @param ns Namespace in which to look for pages.
323     * @param depth Search depth.
324     * @param use_first_header (optional) Includes the first header as page title.
325     */
326    function get_pages( &$pages, $ns, $depth=0, $use_first_header=false )
327    {
328        global $conf;
329
330        // find pages
331        $search_results = array();
332        search($search_results,
333            $conf['datadir'],
334            'search_universal',
335            array(
336                'depth' => $depth,
337                'listfiles' => true,
338                'listdirs'  => false,
339                'pagesonly' => true,
340                'skipacl'   => true,
341                'firsthead' => true,
342                'meta'      => true,
343            ),
344            str_replace(':','/',$ns)
345        );
346
347        // Start page of the namespace
348        if ($ns && page_exists($ns)) {
349            // Add to the search results
350            $search_results[] = array(
351                'id'    => $ns,
352                'ns'    => getNS($ns),
353                'title' => p_get_first_heading($ns, false),
354                'size'  => filesize(wikiFN($ns)),
355                'mtime' => filemtime(wikiFN($ns)),
356                'perm'  => 16,
357                'type'  => 'f',
358                'level' => 0,
359                'open'  => 1,
360            );
361        }
362
363        // loop through the pages
364        while($item = array_shift($search_results)) {
365            // Check that the user is allowed to read the page
366            if ( (auth_quickaclcheck($item['id']) > AUTH_READ) ) {
367                    continue;
368            }
369            // Check that the user is allowed to read the page
370            if ( (auth_quickaclcheck($item['ns']) > AUTH_READ) ) {
371                    continue;
372            }
373
374            // Get the create time
375            $time = (int) p_get_metadata($item['id'], 'date created', false);
376            if(!$time) $time = $item['mtime'];
377
378            // Get specific language part
379            $lang = ($transplugin)?$transplugin->getLangPart($item['id']):'';
380            if($lang) {
381                $item['ns'] = preg_replace('/^'.$lang.'(:|$)/','',$item['ns']);
382            }
383
384            if ( $use_first_header ) {
385                $title = $item['title'];
386            }
387            else {
388                // Use the last part of the id for the name
389                $title = ucwords( substr(strrchr(strtr($item['id'],'_',' '), ':'), 1 ) );
390            }
391            // Add the page to the page list
392            $pages[$item['id']] = array (
393                'title' => $title,
394                'ns'    => $item['ns'],
395                'size'  => $item['size'],
396                'time'  => $time,
397                'links' => array(),
398                'media' => array(),
399                'lang'  => $lang
400            );
401        }
402    }
403
404    /**
405     * Gathers all page and media data for given namespaces.
406     *
407     * @namespaces array() of namespaces
408     * @depth Search depth
409     * @include_media Determines if media should be regarded, Values: 'ns','all','none'.
410     * @use_cached_pages Determines if only cached pages should be used. If this option is turned off, the operation will cache all non-cached pages within the namespace.
411     * @use_first_header Determines if the first header is used for title of the pages.
412     *
413     * @return array with pages and media: array('pages'=>pages, 'media'=>media).
414     */
415    function gather_data($namespaces, $depth=0, $include_media='none', $use_cached_pages=true, $use_first_header=false) {
416        global $conf;
417
418        $transplugin = plugin_load('helper','translation');
419
420        $pages = array();
421        $media = array();
422
423        // Loop through the namespaces
424        foreach ($namespaces as $ns) {
425            // Get the media of the namespace
426            if( $include_media == 'ns' ) {
427                $this->get_media( $media, $ns, $depth );
428            }
429            // Get the pages of the namespace
430            $this->get_pages( $pages, $ns, $depth, $use_first_header );
431        }
432
433        // Loop through the pages to get links and media
434
435        foreach($pages as $pid => $item){
436
437            // get instructions
438            $ins = p_cached_instructions(wikiFN($pid), $use_cached_pages, $pid);
439
440            // find links and media usage
441            foreach ($ins as $i) {
442                $mid = null;
443
444                // Internal link?
445                if ($i[0] == 'internallink') {
446                    $id     = $i[1][0];
447                    $exists = true;
448                    resolve_pageid($item['ns'],$id,$exists);
449                    list($id) = explode('#',$id,2);
450                    if($id == $pid) continue; // skip self references
451
452                    if($exists && isset($pages[$id])){
453                        $pages[$pid]['links'][] = $id;
454                    }
455                    if(is_array($i[1][1]) && $i[1][1]['type'] == 'internalmedia'){
456                        $mid = $i[1][1]['src']; // image link
457                    }else{
458                        continue; // we're done here
459                    }
460                }
461
462                if($i[0] == 'internalmedia') {
463                    $mid = $i[1][0];
464                }
465
466                if(is_null($mid)) continue;
467                if($include_media == 'none') continue; // no media wanted
468
469                $exists = true;
470                resolve_mediaid($item['ns'],$mid,$exists);
471                list($mid) = explode('#',$mid,2);
472                $mid = cleanID($mid);
473
474                if($exists){
475                    if($include_media == 'all'){
476                        if (!isset($media[$mid])) { //add node
477                            $media[$mid] = array(
478                                                'size'  => filesize(mediaFN($mid)),
479                                                'time'  => filemtime(mediaFN($mid)),
480                                                'ns'    => getNS($mid),
481                                                'title' => noNS($mid),
482                                           );
483                        }
484                        $pages[$pid]['media'][] = $mid;
485                    } elseif(isset($media[$mid])){
486                        $pages[$pid]['media'][] = $mid;
487                    }
488                }
489            }
490
491            // clean up duplicates
492            $pages[$pid]['links'] = array_unique($pages[$pid]['links']);
493            $pages[$pid]['media'] = array_unique($pages[$pid]['media']);
494        }
495
496        return array('pages'=>$pages, 'media'=>$media);
497    }
498
499    /**
500     * Create a Graphviz dot representation
501     */
502    function get_dot(&$data ) {
503        $pages =& $data['pages'];
504        $media =& $data['media'];
505
506        $output = "digraph G {\n";
507
508        // create all nodes first
509        foreach($pages as $id => $page) {
510            $output .= "    \"page-$id\" [shape=note, label=\"{$page['title']}\\n{$id}\", color=lightblue, fontname=Helvetica];\n";
511        }
512        foreach($media as $id => $item) {
513            $output .= "    \"media-$id\" [shape=box, label=\"$id\", color=sandybrown, fontname=Helvetica];\n";
514        }
515        // now create all the links
516        foreach($pages as $id => $page){
517            foreach($page['links'] as $link){
518                $output .= "    \"page-$id\" -> \"page-$link\" [color=navy];\n";
519            }
520            foreach($page['media'] as $link){
521                $output .= "    \"page-$id\" -> \"media-$link\" [color=firebrick];\n";
522            }
523        }
524        $output .= "}\n";
525
526        return $output;
527    }
528
529    /**
530     * Create a GEXF representation
531     */
532    function get_gexf(&$data){
533        $pages =& $data['pages'];
534        $media =& $data['media'];
535
536        $output = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
537        $output .= "<gexf xmlns=\"http://www.gexf.net/1.1draft\" version=\"1.1\"
538                       xmlns:viz=\"http://www.gexf.net/1.1draft/viz\">\n";
539        $output .= "    <meta lastmodifieddate=\"".date('Y-m-d H:i:s')."\">\n";
540        $output .= "        <creator>DokuWiki</creator>\n";
541        $output .= "    </meta>\n";
542        $output .= "    <graph mode=\"dynamic\" defaultedgetype=\"directed\">\n";
543
544        // define attributes
545        $output .= "        <attributes class=\"node\">\n";
546        $output .= "            <attribute id=\"title\" title=\"Title\" type=\"string\" />\n";
547        $output .= "            <attribute id=\"lang\" title=\"Language\" type=\"string\" />\n";
548        $output .= "            <attribute id=\"ns\" title=\"Namespace\" type=\"string\" />\n";
549        $output .= "            <attribute id=\"type\" title=\"Type\" type=\"liststring\">\n";
550        $output .= "                <default>page|media</default>\n";
551        $output .= "            </attribute>\n";
552        $output .= "            <attribute id=\"time\" title=\"Created\" type=\"long\" />\n";
553        $output .= "            <attribute id=\"size\" title=\"File Size\" type=\"long\" />\n";
554        $output .= "        </attributes>\n";
555
556        // create all nodes first
557        $output .= "        <nodes>\n";
558        foreach($pages as $id => $item){
559            $title = htmlspecialchars($item['title']);
560            $lang  = htmlspecialchars($item['lang']);
561            $output .= "            <node id=\"page-$id\" label=\"$id\" start=\"{$item['time']}\">\n";
562            $output .= "               <attvalues>\n";
563            $output .= "                   <attvalue for=\"type\" value=\"page\" />\n";
564            $output .= "                   <attvalue for=\"title\" value=\"$title\" />\n";
565            $output .= "                   <attvalue for=\"lang\" value=\"$lang\" />\n";
566            $output .= "                   <attvalue for=\"ns\" value=\"{$item['ns']}\" />\n";
567            $output .= "                   <attvalue for=\"time\" value=\"{$item['time']}\" />\n";
568            $output .= "                   <attvalue for=\"size\" value=\"{$item['size']}\" />\n";
569            $output .= "               </attvalues>\n";
570            $output .= "               <viz:shape value=\"square\" />\n";
571            $output .= "               <viz:color r=\"173\" g=\"216\" b=\"230\" />\n";
572            $output .= "            </node>\n";
573        }
574        foreach($media as $id => $item){
575            $title = htmlspecialchars($item['title']);
576            $lang  = htmlspecialchars($item['lang']);
577            $output .= "            <node id=\"media-$id\" label=\"$id\" start=\"{$item['time']}\">\n";
578            $output .= "               <attvalues>\n";
579            $output .= "                   <attvalue for=\"type\" value=\"media\" />\n";
580            $output .= "                   <attvalue for=\"title\" value=\"$title\" />\n";
581            $output .= "                   <attvalue for=\"lang\" value=\"$lang\" />\n";
582            $output .= "                   <attvalue for=\"ns\" value=\"{$item['ns']}\" />\n";
583            $output .= "                   <attvalue for=\"time\" value=\"{$item['time']}\" />\n";
584            $output .= "                   <attvalue for=\"size\" value=\"{$item['size']}\" />\n";
585            $output .= "               </attvalues>\n";
586            $output .= "               <viz:shape value=\"disc\" />\n";
587            $output .= "               <viz:color r=\"244\" g=\"164\" b=\"96\" />\n";
588            $output .= "            </node>\n";
589        }
590        $output .= "        </nodes>\n";
591
592        // now create all the edges
593        $output .= "        <edges>\n";
594        $cnt = 0;
595        foreach($pages as $id => $page){
596            foreach($page['links'] as $link){
597                $cnt++;
598                $output .= "            <edge id=\"$cnt\" source=\"page-$id\" target=\"page-$link\" />\n";
599            }
600            foreach($page['media'] as $link){
601                $cnt++;
602                $output .= "            <edge id=\"$cnt\" source=\"page-$id\" target=\"media-$link\" />\n";
603            }
604        }
605        $output .= "        </edges>\n";
606
607        $output .= "    </graph>\n";
608        $output .= "</gexf>\n";
609        return $output;
610    }
611}
612
613
614