Extension:Graphical Category Browser

From MediaWiki.org
Jump to: navigation, search
MediaWiki extensions manual - list
Crystal Clear action run.png
Graphical Category Browser

Release status: beta

Implementation Special page
Description A graphical browser for categories
Author(s) Xypron
Last version 0.1 (06-08-2006)
MediaWiki 1.7.x
License No license specified
Download see below

Check usage (experimental)

Contents

[edit] Why is this extension necessary?

Showing the interconnection of categories as a graph enhances navigability of the site.

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

I get them run under M$ Windows. Eaccerlator provides a possibility. --Ozz 10:32, 24 March 2008 (UTC)

  • Getting white spaces? Try removing the wfStrencode() function from lines 390 and 423. Leave the $text variable by itself. I am not sure what the effects of this are, but it allows the maps to be shown, so it might be worth it. 156.26.172.217 20:25, 11 March 2009 (UTC)

[edit] 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.

[edit] Installation

require_once("$IP/extensions/xyCategoryBrowser.php");

[edit] The Script

<?php
//Path to the GraphViz dot executable (Linux)
$xyDotPath = "/usr/local/bin/dot";
//Path to the GraphViz dot executable (Windows)
//$xyDotPath = "\"C:\\Program Files\\ATT\\Graphviz\\bin\\dot.exe\"";
 
$xyCategoriesMaxAge = 36;
// Path for the file cache relative to this script.
$xyCategoriesCache = "/../images/xyGraphvizCache/";
 
if(!defined('MEDIAWIKI')){
  // Serve the PNG image
  $cap = new xyCategoryGraph();
  if ($cap->serveFile()) die();
  header("HTTP/1.1 404 Not Found");
  die("<H1>404 Not Found </H1>");
  }
 
//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("<IMG src=\"$URLpng\" usemap=\"#map1\" alt=\"$title\"><MAP name=\"map1\">$map</MAP>");
      $wgOut->addWikiText(
        wfMsg('xyrenderedwith')." [http://www.graphviz.org/ 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;
    }
 
?>

[edit] To do

[edit] Category Page

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

[edit] Special Page Category Errors

Show errors in categories:

  • cycles
  • ...
Personal tools
Namespaces
Variants
Actions
Site
Support
Download
Development
Communication
Print/export
Toolbox