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