Extension:Graphical Category Browser

Why is this extension necessary?
Showing the interconnection of categories as a graph enhances navigatibility of the site.


 * The Graph data structure extension creates XML which might be used for offline creation of a graph of all categories and creates a graphs on the fly of direct neighbours on category pages using Graphviz. It relies on the graphviz (Graphviz (IndyGreg) extension (see also Graphviz Extension on CaseWiki).

Unfortunately the Graphviz extension does not support MS Windows. Furthermore it does not provide cache control to ensure that HTML s and PNG images stay in synch.

What this extension does

 * A special page "Graphical Categories Browser" is added.
 * A hook is supplied to the category pages to add a graph on top.

Images are file cached. Cache control to match html and image output is supplied.

Installation
require_once("$IP/extensions/xyCategoryBrowser.php");
 * Install Graphviz - Graph Visualization Software.
 * Copy script below to extensions.
 * Correct $xyDotPath and $xyCategoriesCache to Your needs.
 * Add the following line to LocalSettings.php

The Script
 serveFile) die; header("HTTP/1.1 404 Not Found"); die("404 Not Found "); }

//install special page $wgExtensionFunctions[] = 'xyfCategoryBrowserSetup'; $wgExtensionCredits['specialpage'][] = array(       'name' => 'Xypron Category Browser',        'author' =>'xypron',        'description' => 'Graphviz graphs for categories',        'url' => 'http://www.xypron.de');

// Setup Special Page function xyfCategoryBrowserSetup { global $IP, $wgMessageCache, $wgHooks;

$wgHooks['CategoryPageView'][] = 'xyCategoryGraphHook'; require_once($IP . '/includes/SpecialPage.php'); SpecialPage::addPage(new SpecialPage('Xygraphicalcategorybrowser', '', true, 'xyfGraphicalCategoryBrowser', false));

$wgMessageCache->addMessages( array( 'xygraphicalcategorybrowser' => 'Graphical Category Browser', 'xyrenderedwith'   => 'rendered with')    ); $wgMessageCache->addMessages( array( 'xygraphicalcategorybrowser' => 'Graphische Kategorie-&'.'Uuml;bersicht', 'xyrenderedwith'   => 'gezeichnet mit'),    'de'); }

function xyfGraphicalCategoryBrowser { $cap = new xyCategoriesPage; $cap->doQuery; $cap->doDot('xycategorybrowser'); $cap->showImg('xycategorybrowser'); }

class xyCategoriesPage { var $debug = false; var $dot = "";

function doQuery($title = null) { global $wgOut;

error_reporting(0);

$redirections= Array; $nodes=Array; $this->dot = "digraph a {\nsize=\"8,20\";\nrankdir=LR;\nnode [height=0 style=\"filled\", shape=\"box\", font=\"Helvetica-Bold\", fontsize=\"10\", color=\"#00000\"];\n"; $dbr =& wfGetDB( DB_SLAVE ); $sql= $this->getSQLCategories($title); $res = $dbr->query( $sql ); # Only read at most $num rows, because $res may contain the whole 1000 for ( $i = 0; $obj = $dbr->fetchObject($res); $i++ ) { $l_title = Title::makeTitle(NS_CATEGORY, $obj->cat);

$color = "#CCFFCC"; if($obj->redirect==1) $color = "#FFCCCC"; if($obj->virtual == 1) $color = "#FFFFCC"; $nodes[$obj->cat] = array(       'color' => $color,        'url'   => $l_title->getFullURL,        'peri'  => 1        );

if ($title && $obj->cat == $title->getDBkey) { $nodes[$obj->cat]['peri']=2; }     if ($obj->redirect) { $article = new article($l_title); if ($article) { $text = $article->getContent; $rt = Title::newFromRedirect($text); if ($rt) { if (NS_CATEGORY == $rt->getNamespace) { $redirections[$l_title->getDBkey] = $rt->getDBkey; if (!$nodes[$rt->getDBkey]){ $nodes[$rt->getDBkey] = array(                 'color' => "#CCFFCC",                  'url'   => $rt->getFullURL,                  'peri'  => 1                  ); }             }            }          }        }      }    $sql= $this->getSQLCategoryLinks($title); $res = $dbr->query( $sql ); for ( $i = 0; $obj = $dbr->fetchObject( $res ); $i++ ) { $cat_from = Title::makeName(NS_CATEGORY, $obj->cat_from); $cat_to  = Title::makeName(NS_CATEGORY, $obj->cat_to); if (@!$nodes[$obj->cat_to]){ $rt = Title::makeTitle(NS_CATEGORY, $obj->cat_to); $nodes[$rt->getDBkey] = array(         'color' => "#FF0000",          'url'   => $rt->getFullURL,          'peri'  => ($title && $rt->getDBkey == $title->getDBkey)? 2 : 1          ); }     if (!$redirections[$obj->cat_from] &&  $redirections[$obj->cat_from] != $obj->cat_to) { $this->dot .= "\"$obj->cat_to\" -> \"$obj->cat_from\" [dir=back];\n"; }     }    foreach( $redirections as $cat_from => $cat_to) { $this->dot .= "\"$cat_to\" -> \"$cat_from\" [color=\"#FF0000\", dir=back];\n"; }

foreach( $nodes as $l_DbKey=>$properties ) { $l_title = Title::makeTitle(NS_CATEGORY, $l_DbKey); $this->dot .= "\"$l_DbKey\" [URL=\"{$properties['url']}\", peripheries={$properties['peri']}, fillcolor=\"{$properties['color']}\"];\n"; }   $this->dot .= "}\n"; if ($this->debug) $wgOut->addWikiText("<"."pre>$this->dot<"."pre>"); }

/** * Save dot file and generate png and map file * @param title to generate md5 for filename */ function cacheAge( $title ) { $md5 = md5($title); $docRoot = $this->cachePath; $fileMap = "$docRoot$md5.map"; if (!file_exists($fileMap)) return false; return time - filemtime($fileMap); } /** * Save dot file and generate png and map file * @param title to generate md5 for filename */ function doDot( $title ) { global $wgOut; global $xyDotPath;

$md5 = md5($title); $docRoot = $this->cachePath; $fileDot = "$docRoot$md5.dot"; $fileMap = "$docRoot$md5.map"; $filePng = "$docRoot$md5.png";

$this->file_put_contents($fileDot, $this->dot); if ($xyDotPath) { if ($this->debug) $wgOut->addWikiText("$xyDotPath -Tpng -o$filePng <$fileDot"); $result = shell_exec("$xyDotPath -Tpng -o$filePng <$fileDot"); if ($this->debug) $wgOut->addWikiText("$xyDotPath -Tcmap -o$fileMap <$fileDot"); $map = shell_exec("$xyDotPath -Tcmap -o$fileMap <$fileDot"); }   }

/** * serveFile * * This function is used to deliver the PNG file to the client. * Client side cache behaviour is controlled here. * This is necessary to get a match between the html and the image. */ function serveFile { global $xyCategoriesMaxAge; // Get filename from GET parameter if(isset($_GET['png'])) { $filename = @$_GET['png']; }  else { return false; }  // Check filename is valid if (preg_match('/\\W/',$filename))return false; $docRoot = $this->cachePath; $file = "$docRoot$filename.png"; // Check file exists if (!file_exists($file)) return false; // Get filetime $time = @filemtime($file); // Get filesize $size = @filesize($file); $etag = md5("$time|$size"); // Get "Last-Modified" if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { $oldtime=strtotime(current(explode(';',$_SERVER['HTTP_IF_MODIFIED_SINCE']))); }  // Get "ETag" if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) { $oldetag=explode(',',$_SERVER['HTTP_IF_NONE_MATCH']); }  // If either is unchanged the file is not modified. if ( (isset($oldtime) && $oldtime == $time ) ||     (isset($oldetag) && $oldetag == $etag ) ) { header('HTTP/1.1 304 Not Modified'); header('Date: '.gmdate('D, d M Y H:i:s').' GMT'); header('Server: PHP'); header("ETag: $etag"); return true; }  // Send headers header('HTTP/1.1 200 OK'); header('Date: '.gmdate('D, d M Y H:i:s').' GMT'); header('Server: PHP'); header('Last-Modified: '.gmdate('D, d M Y H:i:s',$time).' GMT'); header('Expires: '.gmdate('D, d M Y H:i:s',$time+$xyCategoriesMaxAge).' GMT'); // Supply the filename that is proposed when saving the file to disk header("Content-Disposition: inline; filename=cat.png"); header("ETag: $etag"); header("Accept-Ranges: bytes"); header("Content-Length: ".(string)(filesize($file))); header("Connection: close\n"); header("Content-Type: image/png"); // Send file $h = fopen($file, 'rb'); fpassthru($h); fclose($h); return true; }

/** * Output the image to the OutputPage object. * @param title to generate md5 for filename */ function showImg( $title ) { global $wgOut; global $wgUploadPath, $wgScriptPath;

$docRoot = $this->cachePath; $md5 = md5($title); $fileMap = "$docRoot$md5.map"; // Get name of this script this will keep it working after renaming $path_parts = pathinfo(__FILE__); $script = $path_parts['basename'];

if (file_exists($fileMap)) { $map = $this->file_get_contents($fileMap); if ($this->debug) $wgOut->addWikiText("<"."pre>$map<"."/pre>");

$URLpng = "$wgScriptPath/extensions/$script?png=$md5"; $wgOut->addHTML("$map"); $wgOut->addWikiText(       wfMsg('xyrenderedwith')." Graphviz - Graph Visualization Software".        ', '.date("Y-m-d H:i:s.", filemtime($fileMap))."\n\n"); return true; }   else { return false; }   }

/** * @return directory for graphviz files */ function cachePath { global $xyCategoriesCache; $path = pathinfo(__FILE__); $path = $path['dirname'].$xyCategoriesCache; if (substr( php_uname, 0, 7 ) == "Windows") $path = preg_replace('/\\//', '\\',$path); if (!is_dir($path)) { mkdir($path, 0775); }   return $path; } function getSQLCategories( $title = null ) { global $wgOut;

$NScat = NS_CATEGORY; $dbr =& wfGetDB(DB_SLAVE); $categorylinks = $dbr->tableName('categorylinks'); $page         = $dbr->tableName('page'); $sql = "SELECT\n". "   page_title AS cat,\n". "   page_is_redirect AS redirect,\n". "   0 AS virtual\n". " FROM $page\n". " WHERE\n". "   page_namespace={$NScat}\n". "UNION\n". "SELECT\n". "   cl_to as cat,\n". "   0 AS redirect,\n". "   1 AS virtual\n". " FROM $categorylinks\n". " LEFT JOIN $page\n". " ON page_title=cl_to\n". " WHERE\n". "   page_id IS NULL"; if ($this->debug) $wgOut->addWikiText("<"."pre>$sql<"."/pre>"); return $sql; }

function getSQLCategoryLinks( $title = null ) { global $wgOut;

$NScat = NS_CATEGORY; $dbr =& wfGetDB(DB_SLAVE); $categorylinks = $dbr->tableName('categorylinks'); $page         = $dbr->tableName('page'); $sql = "SELECT\n". "   page_title AS cat_from, \n". "   cl_to as cat_to,\n". "   page_is_redirect AS redirect\n". " FROM $page\n". " INNER JOIN $categorylinks\n". " ON page_id=cl_from\n". " WHERE\n". "   page_namespace={$NScat}"; if ($this->debug) $wgOut->addWikiText("<"."pre>$sql<"."/pre>");

return $sql; }

function neighboursOnly{ return false ;}

// This function is only needed for PHP prior to version 5. function file_put_contents($n,$d) { $f=@fopen($n,"wb"); if (!$f) { return false; }    else { fwrite($f,$d); fclose($f); return true; }   }    // This function is only needed for PHP prior to version 5. function file_get_contents($n) { $f=@fopen($n,"rb"); if (!$f) { return false; }    else { $s=filesize($n); $d=false; if ($s) $d=fread($f, $s) ; fclose($f); return $d; }   }      } class xyCategoryGraph extends xyCategoriesPage{

function neighboursOnly{ return true ;} function getSQLCategories( $title ) { global $wgOut;

$id  = $title->getArticleID; $text = $title->getDBkey;

$NScat = NS_CATEGORY; $dbr =& wfGetDB(DB_SLAVE); // Use the following for MediaWiki 1.9: // $text = $dbr->addQuotes($text); $text = "'".wfStrencode($text)."'";

$categorylinks = $dbr->tableName('categorylinks'); $page         = $dbr->tableName('page'); $sql = "SELECT DISTINCT\n". "   page_title AS cat,\n". "   page_is_redirect AS redirect,\n". "   0                AS virtual\n". " FROM $page as a\n". " left JOIN $categorylinks as b\n". " ON a.page_id=b.cl_from\n". " left join $categorylinks as c\n". " ON a.page_title=c.cl_to\n". " WHERE\n". "   page_namespace = {$NScat} AND\n". " (  c.cl_from    = $id OR\n".      "     a.page_id    = $id OR\n".      "     b.cl_to      = {$text} )\n"; if ($this->debug) $wgOut->addWikiText("<"."pre>$sql<"."/pre>"); return $sql; }

function getSQLCategoryLinks( $title ) { global $wgOut; $id  = $title->getArticleID; $text = $title->getDBkey;

$NScat = NS_CATEGORY; $dbr =& wfGetDB(DB_SLAVE); // Use the following for MediaWiki 1.9: // $text = $dbr->addQuotes($text); $text = "'".wfStrencode($text)."'"; $categorylinks = $dbr->tableName('categorylinks'); $page         = $dbr->tableName('page'); $sql = "SELECT\n". "   page_title AS cat_from, \n". "   cl_to as cat_to\n". " FROM $page\n". " INNER JOIN $categorylinks\n". " ON page_id=cl_from\n". " WHERE\n". "   ( page_id=$id  OR\n".      "    cl_to=$text ) AND\n". "   page_namespace={$NScat}"; if ($this->debug) $wgOut->addWikiText("<"."pre>$sql<"."/pre>"); return $sql; } }  function xyCategoryGraphHook($cat) { global $wgOut, $xyCategoriesMaxAge; $wgOut->setSquidMaxage( $xyCategoriesMaxAge ); $title = $cat->getTitle; $dbKey = $title->getDBkey; $cap = new xyCategoryGraph; $age = $cap->cacheAge($dbKey); if (!$age || $age > $xyCategoriesMaxAge ) { $cap->doQuery($title); $cap->doDot($dbKey); };   $cap->showImg($dbKey); return true; } ?> 

Category Page
Change the hook for the category page to show all up- and downstream connected categories.

Special Page Category Errors
Show errors in categories:
 * cycles