<?php
/*
* @date: 2010-11-01
* @version: 0.9 (by hummel-riegel)
*
* See mediawiki.org/wiki/Extension:GraphViz for more information
*
* Extension to allow Graphviz to work inside MediaWiki.
* This Version is based on CoffMan's nice Graphviz Extension.
*
* Licence: http://www.gnu.org/copyleft/fdl.html
*
* Configuration
*
* These settings can be overwritten in LocalSettings.php.
* Configuration must be done AFTER including this extension using
* require("extensions/Graphviz.php");
*
* $wgGraphVizSettings->execPath
* Describes where your actual (dot) executable remains.
*
* Windows Default: C:/Programme/ATT/Graphviz/bin/
* Other Platform : /usr/local/bin/dot
*
* $wgGraphVizSettings->mscgenPath
* Describes where your actual mscgen-executable remains
*
* $wgGraphVizSettings->named
* Describes the way graph-files are named.
*
* named: name of your graph and its type determine its filename
* md5 : name of your graph is based on a md5 hash of its source.
* sha1 : name of your graph is based on a SHA1 hash of its source.
*
* Default : named
*
* $wgGraphVizSettings->install
* Gets you an errormessage if something fails, but maybe ruins your
* wiki's look. This message is in English, always.
*
* Default : false (not really implemented yet)
*
* Features
* - normally selects defaults for Windows or Unix-like automatically.
* - should run out of the box
* - Creates png (or maybe other image) + MAP File
* - additional storage modes (see discussion below)
* - Meaningful filename
* - Hash based filename
* - Configurable (name/md5/sha1)
*
* Storage Modes:
* MD5:
* + don't worry about graphnames
* + pretty fast hash
* - permanent cleanup necesary (manually or scripted)
* - md5 is buggy - possibility that 2 graphs have the same hash but
* are not the same
* SHA1:
* + don't worry about graphnames
* + no hash-bug as md5
* - permanent cleanup necessary (manually or scripted)
* - not so fast as md5
* Named:
* + Graphs have a name, now it's used
* + no permanent cleanup necessary.
* - Naming Conflicts
* a) if you have multiple graphs of the same name in the same
* article, you will only get 1 picture - independently if they're
* the same or not.
* b) possible naming conflicts in obscure cases (that should not happen)
* Read code for possibilities / exploits
*
*/
if (!defined('MEDIAWIKI')) die();
/*
* The GraphViz-Class with the essential settings
*/
class GraphVizSettings {
public $execPath, $mscgenPath, $named; // Execution Parameters
// public $uploadPath, $uploadDirectory; // where are the images/maps saved
public $outputType, $imageFormatting; // produced image
public $install, $info; // Information about Functionality
public $pruneEnabled, $pruneStrategy, $pruneValue, $pruneAmount; // Pruning Parameters
};
$wgGraphVizSettings = new GraphVizSettings(); // create new instance of GraphVizSettings
/*
* Configuration of the graphviz instance
* if parameters are overwritten in the LocalSettings.php nothing will be changed
*/
//Set execution path
if ( stristr(PHP_OS, 'WIN' ) && !stristr(PHP_OS, 'Darwin') ) {
$wgGraphVizSettings->execPath = 'C:/Program Files/Graphviz/bin/'; // '/' will be converted to '\\' later on, so feel free how to write your path C:/ or C:\\
} else {
$wgGraphVizSettings->execPath = '/usr/bin/'; // common: '/usr/bin/' '/usr/local/bin/' or (if set) '$DOT_PATH/'
}
//Set further default values for parameters
$wgGraphVizSettings->mscgenPath = ''; // installation path for mscgen-renderer
$wgGraphVizSettings->named = 'named'; // naming scheme for the maps/images
$wgGraphVizSettings->outputType = 'png'; //can be changed to gif, svg, ...
$wgGraphVizSettings->imageFormatting = 'false'; // Do we want to use border/position/... in comparison to the normal wiki images?
$wgGraphVizSettings->install = false; // Do not use on Linux ... it somewhere destroys the script // Do you want error messages displayed? This can ruin your layout
$wgGraphVizSettings->info = false; // Do you want additional info to your renders displayed?
$wgGraphVizSettings->pruneEnabled = false; // Do you want to prune?
$wgGraphVizSettings->pruneStrategy = 'filenumber'; //pruning strategy to use
// filesize - limit total size of files to amount of bytes
// filenumber - limit total number of files
$wgGraphVizSettings->pruneValue = '10000'; //value to apply to 'pruneStrategy'
// total file size (in bytes)
// total number of files allowed
$wgGraphVizSettings->pruneAmount = '0.5'; //amount by which we prune
//Removes this fraction of the oldest files come prune time
/*
* Media Wiki Plugin Stuff
*/
// Check if the Wiki supports the new extension syntax
if ( defined( 'MW_SUPPORTS_PARSERFIRSTCALLINIT' ) ) {
$wgHooks['ParserFirstCallInit'][] = 'wfGraphVizExtension';
} else { // Otherwise do things the old fashioned way
$wgExtensionFunctions[] = 'wfGraphVizExtension';
}
// Information about the people did this Parserhook
$wgExtensionCredits['parserhook'][] = array(
'name'=>'Graphviz',
'author'=>'CoffMan <http://wickle.com>, MasterOfDesaster <mailto://arno.venner@gmail.com>, Thomas Hummel <http://hummel-universe.net>',
'url'=> 'http://www.mediawiki.org/wiki/Extension:GraphViz',
'description'=>'Graphviz (http://www.graphviz.org) is a program/language that allows the creation of numerous types of graphs. This extension allows the embedding of graphviz markup in MediaWiki pages and generates inline images to display.'
);
/*
* Information about the hooks used
*/
function wfGraphVizExtension() {
global $wgParser;
$wgParser->setHook( "graphviz", "renderGraphviz" );
$wgParser->setHook( "mscgen", "renderMscGen" );
return true;
}
/**
* The rendering function for the mscgen-hook implementation
* @author Matthewpearson
*/
function renderMscGen( $timelinesrc, $args=null, $parser=null )
{
$args["renderer"] = "mscgen"; // set renderer to mscgen - not that nice, could be done better
return renderEngine($timelinesrc, $args, $parser); // go to the "normal" rendering stuff
}
/**
* The rendering function for the graphviz-implementation
* @author Thomas Hummel
*/
function renderGraphviz( $timelinesrc, $args=null, $parser=null ) // Raw Script data
{
if (isset($args["renderer"])) {
switch($args["renderer"]) {
case 'circo':
case 'dot':
case 'fdp':
case 'sfdp':
case 'neato':
case 'twopi':
break;
default:
$args["renderer"] = 'dot';
}
} else {
$args["renderer"] = "dot";
}
return renderEngine($timelinesrc, $args, $parser); // go to the "normal" rendering stuff
}
/*
* The actual rendering function for handling all the stuff
*/
function renderEngine( $timelinesrc, $args=null, $parser=null ) // Raw Script data
{
global
$wgUploadDirectory, // Storage of the final image (e.g. png) & map
$wgUploadPath, // HTML Reference
$wgGraphVizSettings, // Plugin Config
$info; // some html-output for retracing what we did
$info = "<h1>Graphviz Information</h1>";
$info .= "<h2>Called render</h2>";
/* Prepare Directories
*/
$dest = $wgUploadDirectory."/graphviz/";
if ( stristr(PHP_OS, 'WIN' ) && !stristr(PHP_OS, 'Darwin') ) {
$dest = str_replace("/", '\\', $dest); //switch the slashes for windows
$isWindows = true;
} else {
$isWindows = false;
}
if ( ! is_dir( $dest ) ) { mkdir( $dest, 0777 ); } //create directory if it isn't there
/* Start pruning (if enabled) - use directory generated before
* prune before creating a new image so that it won't be pruned right after creation
*/
if ($wgGraphVizSettings->pruneEnabled) {
prune($dest,$wgGraphVizSettings->pruneStrategy,$wgGraphVizSettings->pruneValue,$wgGraphVizSettings->pruneAmount); //prune the collection first
}
/* Check renderer - renderer should be set at least in renderMscgen or renderGraphviz but for security we check agaion and set some additional params
*/
if ( isset($args['renderer']) ) {
$renderer = $args['renderer'];
} else {
$renderer = 'dot';
}
switch($renderer) {
case 'circo':
case 'dot':
case 'fdp':
case 'sfdp':
case 'neato':
case 'twopi':
$mapDashTOption = ' -Tcmapx ';
$inputOption = '';
break;
case 'mscgen':
if ($wgGraphVizSettings->mscgenPath != null) { // check if path to mscgen is set - if not use agaion graphviz with dot
$inputOption = '-i ';
$mapDashTOption = ' -T ismap ';
$wgGraphVizSettings->execPath = $wgGraphVizSettings->mscgenPath; //not that nice but functional: overwrite execPath with the path to mscgen
} else {
$renderer = 'dot';
$mapDashTOption = ' -Tcmapx ';
$inputOption = '';
}
break;
default:
$renderer = 'dot';
$mapDashTOption = ' -Tcmapx ';
$inputOption = '';
}
/* create the command for graphviz or mscgen
*/
$cmd = $renderer;
$cmd = $wgGraphVizSettings->execPath . $cmd; // example: /user/bin/dot
if ( $isWindows ) {
$cmd = $cmd . '.exe'; // executables in windows
}
$info .= "<pre>Dir=$dest</pre>";
$info .= "<pre>execPath=".$wgGraphVizSettings->execPath ."</pre>";
$info .= "<pre>named=".$wgGraphVizSettings->named ."</pre>";
/* create actual storagename
*/
$wgGraphVizSettings->named = strtolower($wgGraphVizSettings->named); //avoid problems with upper/lowercase
$storagename = str_replace("%",'_perc_',urlencode($_GET['title'])).'---'; //clean up pagename (special chars etc)
if($wgGraphVizSettings->named == 'md5') {
$storagename .= md5($timelinesrc); // produce md5-hash out of the storagename !can be duplicate!
} else if ($wgGraphVizSettings->named == 'sha1') {
$storagename .= sha1($timelinesrc); // produce sha1-hash
} else { // named == 'named'
$storagename .= str_replace("%",'_perc_',
urlencode(
trim(
str_replace("\n",'',
str_replace("\\",'/',
substr($timelinesrc, 0, strpos($timelinesrc,'{')) // extract the name of the graph out of the graph
)
)
)
)
);
}
$info .= "<pre>storagename=".$storagename ."</pre>";
/* check if some outputtype is specified in the wikipage - else use the default value
*/
if ( isset($args['format']) ) {
$outputType = $args['format'];
} else {
$outputType = $wgGraphVizSettings->outputType;
}
/* inputcheck of supported image types
*/
if ($renderer != 'mscgen') {
//see supported types by graphviz itself (here only those that seem to be kind of useful) - http://www.graphviz.org/doc/info/output.html
switch($outputType) {
case 'bmp':
case 'gif':
case 'jpg':
case 'jpeg':
case 'png':
case 'svg': //for svg you need extra MediaWiki configuration
case 'svgz': //same as for svg
//case 'tif':
//case 'tiff':
break;
default:
$outputType = 'png';
}
} else {
//mscgen does only support png, svg and eps
switch($outputType) {
case 'png':
case 'svg':
break;
default:
$outputType = 'png';
}
}
$info .= "<pre>outputType=".$outputType ."</pre>";
/* prepare the actual files
*/
$src = $dest . $storagename; // the raw input code - needed for the renderers - e.g. /graphviz/imagename (will be deleted later on)
$imgn = $src . '.'. $outputType; // the whole image name - e.g. /graphviz/imagename.png
$mapn = $src . '.map'; // the whole map name - e.g. /graphviz/imagename.map
$info .= '<pre>Src='.$src.'</pre>';
$info .= '<pre>imgn='.$imgn.'</pre>';
$info .= '<pre>mapn='.$mapn.'</pre>';
/* The actual commands for the rendering
* check first if we have to overwrite the file (if we don't use hashes) or if it already exists
*/
if ( $wgGraphVizSettings->named == 'named' || !( file_exists( $imgn ) || file_exists($src.".err")))
{
$timelinesrc = rewriteWikiUrls( $timelinesrc); // if we use wiki-links we transform them to real urls
// write the given dot-commands into a textfile
$handle = fopen($src, "w");
if (! $handle) return 'Error writing graphviz file to disk.';
$ret2 = fwrite($handle, $timelinesrc);
$ret3 = fclose($handle);
$info.='<pre>Opened and closed $src, handle='.$handle.', timeelinesrc='.$timelinesrc.', ret2='.$ret2.', ret3='.$ret3.'</pre>';
// prepare the whole commands for image and map
$cmdline = wfEscapeShellArg($cmd).' -T '.$outputType.' -o '.wfEscapeShellArg($imgn).' '.$inputOption.wfEscapeShellArg($src);
$cmdlinemap = wfEscapeShellArg($cmd).$mapDashTOption.'-o '.wfEscapeShellArg($mapn).' '.$inputOption.wfEscapeShellArg($src);
// run the commands
if ( $isWindows ) {
$WshShell = new COM("WScript.Shell");
$ret = $WshShell->Exec($cmdline);
$retmap = $WshShell->Exec($cmdlinemap);
} else {
$ret = shell_exec($cmdline);
$retmap = shell_exec($cmdlinemap);
}
$info.='<pre>Ran cmd line (image). ret=$ret cmdline='.$cmdline.'</pre>';
$info.='<pre>Ran cmd line (map). ret=$ret cmdlinemap='.$cmdlinemap.'</pre>';
// Error messages for image-creation
if ($wgGraphVizSettings->install && $ret == "" ) {
echo '<div id="toc"><tt>Timeline error: Executable not found.'. "\n".'Command line was: '.$cmdline.'</tt></div>';
$info.= '<div id="toc"><tt>Timeline error: Executable not found.'. "\n".'Command line was: '.$cmdline.'</tt></div>';
exit;
}
// Error messages for map-creation
if ($wgGraphVizSettings->install && $retmap == "" ) {
echo '<div id="toc"><tt>Timeline error: Executable not found.'. "\n".'Command line was: '.$cmdlinemap.'</tt></div>';
$info.= '<div id="toc"><tt>Timeline error: Executable not found.'. "\n".'Command line was: '.$cmdlinemap.'</tt></div>';
exit;
}
// let some other programs do their stuff
if ( $isWindows ) {
while ( $ret->Status == 0 || $retmap->Status == 0 ) {
usleep(100);
}
}
unlink($src); // delete the src right away
}
/* put the produced into the website
*/
@$err=file_get_contents( $src.".err" );// not really used
if ( $err != "" ) {
$info .= '<div id="toc"><tt>'.$err.'</tt></div>'; // print error message
} else {
if (false == ($map = file_get_contents($mapn) )) {
if($wgGraphVizSettings->install){
echo '<div id="toc"><tt>File: '.$mapn.' is missing or empty.</tt></div>';
$info.= '<div id="toc"><tt>File: '.$mapn.' is missing or empty.</tt></div>';
}
}
// clean up map-name
$map = preg_replace('#<ma(.*)>#',' ',$map);
$map = str_replace('</map>','',$map);
if ( $renderer == 'mscgen' ) {
$mapbefore = $map;
$map = preg_replace('/(\w+)\s([_:%#/\w]+)\s(\d+,\d+)\s(\d+,\d+)/',
'<area shape="$1" href="$2" title="$2" alt="$2" coords="$3,$4" />',
$map);
}
/* Procduce html
*/
if ($wgGraphVizSettings->imageFormatting)
{
$txt = imageAtrributes($args, $storagename, $map, $outputType, $wgUploadPath); // if we want borders/position/...
} else {
$txt = '<map name="'.$storagename.'">'.$map.'</map>'.
'<img src="'.$wgUploadPath.'/graphviz/'.$storagename.'.'.$outputType.'"'.
' usemap="#'.$storagename.'" />';
}
}
/* give it back to your wiki
*/
if($wgGraphVizSettings->info){$txt .= $info;} // do we want the information snipptes?
return $txt;
}
function rewriteWikiUrls( &$source)
{
$line = preg_replace(
'|\[\[([^]]+)\]\]|e',
'Title::newFromText("$1")->getFullURL()',
$source
);
return $line;
}
/**
* Prunes the repository of generated files
* @author Gregory Szorc <gregory.szorc@gmail.com>
* @author Thomas Hummel (modified only)
*/
function prune($dest,$pruneStrategy,$pruneValue,$pruneAmount)
{
$pruneList = array(); //array of files that are prune candidates
$pruneListSize = 0; //size (in bytes) of prunable files
$directory = new DirectoryIterator($dest);
foreach ($directory as $file) {
//only look for actual files
if ($file->isFile()) {
//only mark files with defined prefix as prune candidates
$pruneList[$file->getPathname()] = $file->getMTime();
$pruneListSize += $file->getSize();
}
}
//so now we have our list of files
$doPrune = false;
//let's see if we actually need to prune
if ($pruneStrategy == 'filesize') {
if ($pruneListSize > $pruneValue) {
$doPrune = true;
}
} elseif ($pruneStrategy == 'filenumber') {
if (count($pruneList) > $pruneValue) {
$doPrune = true;
}
} else {
throw new MWException('Invalid prune strategy. Please read the instructions: ' . $pruneStrategy);
}
if ($doPrune) {
//sort the files in order of modification time
asort($pruneList, SORT_NUMERIC);
$filesToDelete = array_slice($pruneList, 0, (int)round(count($pruneList) * $pruneAmount));
foreach (array_keys($filesToDelete) as $file) {
unlink($file);
}
}
}
/*//every time we create a new file, we run the pruning algorithm
//we prune first so that in case settings aren't sane, we don't prune what we create
self::prune(); //prune the collection first
*/
/**
* Image Attributes (orientated on MediaWiki-Syntax like here: http://en.wikipedia.org/wiki/Wikipedia:Extended_image_syntax)
* syntax is <graphviz attribute='value'>
* @author Thomas Hummel
*/
function imageAtrributes($args=null, $storagename, $map, $outputType, $wgUploadPath) {
//Initialize Variables
$varnames = array("outerDivClass","middleDivClass","innerDivClass","imageClass");
$varnames[] = "imageStyle";
foreach ($varnames as $varname)
$$varname = '';
// Caption that is put below the image (can be overwritten by Type)
if ( isset($args['caption']) ) {$caption = $args['caption'];}
// Alt-Text for missing pictures, screenreaders, ... if not set use caption and at least default-String
if ( isset($args['alt']) ) {$alt = $args['alt'];
} elseif ( isset($args['caption'])) {$alt = $args['caption'];
} else {$alt = "This is a graph with borders and nodes. Maybe there is an Imagemap used so the nodes may be linking to some Pages.";
}
// For a border write <graphviz border='border'>
if ( isset($args['border']) ) {
switch($args['border']) {
case 'frame':
case 'border': $imageClass .= 'thumbborder'; break;
}
}
// Location defining horizontal alignment
if ( isset($args['location']) ) {
switch($args['location']) {
case 'left': $outerDivClass = 'floatleft'; break;
case 'middle':
case 'center': $outerDivClass = 'center'; $innerDivClass = 'floatnone'; break;
case 'right': $outerDivClass = 'floatright'; break;
case 'none': $outerDivClass = 'floatnone'; break;
}
}
// Alignment for the vertical alignment
if ( isset($args['alignment']) ) {
switch($args['alignment']) {
case 'baseline':
case 'middle':
case 'sub':
case 'super':
case 'text-top':
case 'text-bottom':
case 'top':
case 'bottom':
$imageStyle = 'vertical-align: '.$args['alignment']; break;
}
}
// Type:
if ( isset($args['type']) ) {
switch($args['type']) {
case 'frame':
case 'framed': // little bug (optical): if you center a framed Graph there will be a border over the whole width of the wiki-page
$middleDivClass = 'thumb';
if($outerDivClass != null) {$middleDivClass .= ' tnone';} else {$middleDivClass = 'tright';}
$innerDivClass = 'thumbinner';
$imageClass = 'thumbimage';
$captionDivClass = 'thumbcaption';
break;
case 'thumb':
case 'thumbnail':
// Differences to the MediaWiki-Behaviour: No extra Thumbs are generated - the browser has to resize the image itself!
// !!Please take into consideration that the mindmap will not fit to your smaller image!!
$middleDivClass = 'thumb';
if($outerDivClass != null) {$middleDivClass .= ' tnone';} else {$middleDivClass = 'tright';}
$innerDivClass = 'thumbinner" style="width:222px;';
$imageClass = 'thumbimage" width="220px';
$captionDivClass = 'thumbcaption';
$caption .= ' <a href="'.$wgUploadPath.'/graphviz/'.$storagename.'.'.$outputType.'">(+)</a>';
break;
case 'frameless':
$imageClass = '" width="220px';
default: // nothing specified
$caption = ''; // no caption as it is defined on the wiki page
}
} else {
$caption = ''; // no caption as it is defined on the wiki page
}
// Produce the basic html
$txt = '<map name="'.$storagename.'">'.$map.'</map>'.
'<img class="'.$imageClass.'" style="'.$imageStyle.'"'.
'alt="'.$alt.'" src="'.$wgUploadPath.'/graphviz/'.$storagename.'.'.$outputType.'"'.
' usemap="#'.$storagename.'" />';
// Add necessary containers
if($caption != null){
$txt .= '<div class="'.$captionDivClass.'">'.$caption.'</div>';
}
if($innerDivClass != null){
$txt = '<div class="'.$innerDivClass.'">'.$txt.'</div>';
}
if($middleDivClass != null){
$txt = '<div class="'.$middleDivClass.'">'.$txt.'</div>';
}
if($outerDivClass != null){
$txt = '<div class="'.$outerDivClass.'">'.$txt.'</div>';
}
return $txt;
}