<?php

#
# $Revision: 1.6 $
#
# Converted from PERL to PHP and adapted for MediaWiki by
#             Stephane D'Alu       <sdalu@sdalu.com>
#  Fixes by   Ozgur D. Gurcan      <ozgur.gurcan@cea.fr>
#
# Based on: cvsweb-3.0.1
#
# Written in their spare time by
#             Bill Fenner          <fenner@FreeBSD.org>   (original work)
# extended by Henner Zeller        <zeller@think.de>,
#             Henrik Nordstrom     <hno@hem.passagen.se>
#             Ken Coar             <coar@Apache.Org>
#             Dick Balaska         <dick@buckosoft.com>
#             Akinori MUSHA        <knu@FreeBSD.org>
#             Jens-Uwe Mager       <jum@helios.de>
#             Ville Skyttä         <scop@FreeBSD.org>
#             Vassilii Khachaturov <vassilii@tarunz.org>
#
# Based on:
# * Bill Fenners cvsweb.cgi revision 1.28 available from:
#   http://www.FreeBSD.org/cgi/cvsweb.cgi/www/en/cgi/cvsweb.cgi
#
# Copyright (c) 1996-1998 Bill Fenner
#           (c) 1998-1999 Henner Zeller
#           (c) 1999      Henrik Nordstrom
#           (c) 2000-2002 Akinori MUSHA
#           (c) 2002-2004 Ville Skyttä
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#


if ( !isset( $wgCVSwebMime ) )
    $wgCVSwebMime = array( "html"  => "text/html",
			   "shtml" => "text/html",
			   "gif"   => "image/gif",
			   "jpeg"  => "image/jpeg",
			   "jpg"   => "image/jpeg",
			   "png"   => "image/png",
			   "xpm"   => "image/xpm" );



  /**
   * RANT:
   *  - among all the functions in PHP (more or less usefull), you cannot
   *    find one being able to read a *whole* line
   *  - you can't pass a class method as a callback, and you don't even
   *    have anonymous functions.
   *  - Does PHP means Pathetic & Hideous Programming ?
   */


# Register extension
$wgExtensionFunctions[] = "wfCVSwebSetup";

# This extension act as a special page
require_once( 'includes/SpecialPage.php' );

$wgCVSwebPath    = "$wgScriptPath/extensions/MW-CVSweb";
$wgCVSwebImgPath = "$wgCVSwebPath/images";

class Xargs {
    function Xargs($cmd, $args, $limit=4000) {
	$this->cmd    = $cmd;
	$this->cmdsz  = strlen($this->cmd);
	$this->args   = array_map('escapeshellarg', $args);
	$this->argssz = array_map('strlen',         $this->args);
	$this->limit  = $limit;
	$this->h      = null;

	if (!count($args))
	    exit("No argument given");
    }

    function open() {
	$cmd   = $this->cmd;
	$cmdsz = $this->cmdsz;
	
	while(count($this->args)) {
	    if ($cmdsz + 1 + $this->argssz[0] > $this->limit)
		break;
	    $cmdsz += 1   + array_shift($this->argssz);
	    $cmd   .= ' ' . array_shift($this->args);
	}

	if ($cmd == $this->cmd)
	    exit("Limit for Xargs is too short");

	$this->h = popen($cmd , "r");
    }

    function getline() {
	if (feof($this->h)) {
	    if (!count($this->args))
		return null;
	    $this->close();
	    $this->open();
	}

	$line = fgets($this->h);
	while ($line[strlen($line)-1] !== "\n") {
	    if (($subline = $this->getline()) == null)
		break;
	    $line .= $subline;
	}
	return $line;
    }

    function close() {
	pclose($this->h);
	$this->h = null;
    }
}

function wfCVSwebSetup() {

class CVSwebPage extends SpecialPage {
    function CVSwebPage() {
	SpecialPage::SpecialPage( 'CVSweb' );
    }


    /*
     * Process the request (called by MediaWiki)
     */
    function execute( $par ) {
	global $wgLang, $wgOut;
	global $wgTitle, $wgRequest;
	global $wgCVSwebCVSROOT, $wgCVSwebCmd, $wgCVSwebWithGraph;
	#global $wgMessageCache;

	$this->sanityCheck();

	# Store configuration parameters
	$this->mCVSROOT   = $wgCVSwebCVSROOT;
	$this->mCmd       = $wgCVSwebCmd;

	# Retrieve URL information
	$this->mPage      = $wgTitle->getPrefixedDBKey();
	$this->mPath      = $par;
	$this->mAbsPath   = '/' . $this->mPath;
	$this->mIsCVSroot = $this->mPath == '';

        # Fix probleme occuring with an empty mPath
	# I commented these out to fix a redirection problem -ODG
	#if ($this->mPage[strlen($this->mPage)-1] != '/'){
	#    $this->mPage = substr($this->mPage, 0, -1);
	#elseif ($this->mPath == '')
	#    $this->redirect($this->buildURL('/'));
	#}
	# Full path (including the CVSROOT)

	$this->mFullPath  = $this->mCVSROOT . '/' . $this->mPath;

	# Infer module name
	$this->mModule    = explode('/', $this->mPath);
	$this->mModule    = $this->mModule[0];	    
	
	# Query string (tending to be sticky)
	$this->mShowAttic = $wgRequest->getBool('showattic');
	$this->mTag       = $wgRequest->getVal('tag');
	$this->mRev       = $wgRequest->getVal('rev');

 	# Deal with easy requests.
	if ($wgRequest->getBool('configstatus')) {
	    $this->displayConfigStatus();
	    return;
	}

	# If we are dealing with a directory, but the trailing '/' is
	# missing, redirect to the correct URL
	if ((!$this->mIsCVSroot) && @is_dir($this->mFullPath) && 
	    ($this->mFullPath[strlen($this->mFullPath)-1] != '/')) {
	    $this->redirect($this->buildURL($this->mAbsPath . '/'));
	    return;
	}

	# If we have an RCS filename (ie: ,v) we try to be nice and
	# redirect to the correct URL if such a file exist (even in
	# the Attic)
	if (preg_match('/^(.+),v$/', $this->mAbsPath, $res))
	    if (@is_file($this->mFullPath) ||
		@is_file($this->atticPath($this->mFullPath))) {
		$this->redirect($this->buildURL($res[1]));
		return;
	    }

	# If the requested file is not present here, but in the Attic,
	# redirect to the Attic.
	if (!@is_dir($this->mFullPath) &&
	    !@is_file($this->mFullPath . ',v') &&
	    @is_file($this->atticPath($this->mFullPath . ',v'))) {
	    $this->redirect($this->buildURL($this->atticPath($this->mAbsPath)));
	    return;
	}

	# Page title
	$wgOut->setPageTitle( 'CVSweb: ' . ($this->mModule 
              ? $wgLang->ucFirst(wfMsg('cvsweb_module')) . ' ' . $this->mModule
	      : $wgLang->ucfirst(wfMsg('cvsweb_repository')) ));

	# Dispatch to: directory, file, log, graph, ...
	$fullRCSname = $this->mFullPath . ',v';
	if (@is_dir($this->mFullPath)) {
	    if (@is_readable($this->mFullPath))
		$this->displayDirectory();
	    else 
		$accesserror = 'cvsweb_accessdir_forbidden';
	} elseif (@is_file($fullRCSname)) {
	    if (@is_readable($fullRCSname)) {
		$withGraph=$wgCVSwebWithGraph && $wgRequest->getBool('graph');
		if ($wgRequest->getBool('download')) {
		    if ($withGraph)
			$this->downloadGraph();
		    else
			$this->downloadFile();
		} elseif ($withGraph) {
		    $this->displayGraph();
		} elseif ($this->mRev) {
		    if ($prev = $wgRequest->getVal('prev'))
			$this->displayDiff($prev);
		    else
			$this->displayFile();
		} else {
		    $this->displayLog();
		}
	    } else {
		$accesserror = 'cvsweb_accessfile_forbidden';
	    }
	} else {
	    $accesserror = 'cvsweb_access_notpossible';
	}

	# Display an error page in case of access error.
	if ($accesserror)
	    $this->errorpage( $wgLang->ucfirst(wfMsg( 'cvsweb_access_error' )),
			      wfMsg( $accesserror,$this->mAbsPath ), 'error' );
    }










    function getline($h) {
	if (feof($h))
	    return null;
	
	$line = fgets($h);
	while ($line[strlen($line)-1] !== "\n") {
	    if (($subline = $this->getline($h)) == null)
		break;
	    $line .= $subline;
	}
	return $line;
    }




    function sanityCheck() {
	global $wgCVSwebCmd, $wgCVSwebCVSROOT;
	global $wgEmergencyContact;

	# Check that the two mandatory variables are defined
	if ( !isset( $wgCVSwebCVSROOT ) || !isset( $wgCVSwebCmd ) )
	    $this->errorpage( wfMsg( 'cvsweb_configerror' ), 
			      wfMsg( 'cvsweb_reqconfig' ) );

	# PHP requirement
	if ( !function_exists( 'preg_match') )
	    $this->errorpage( wfMsg( 'cvsweb_configerror' ),
			      wfMsg( 'cvsweb_requirement' ) );

	# Check required programs (cvs, rlog, cvsgraph, ...)
	$requiredprog = array('cvs', 'rlog');
	if ($wgCVSwebWithGraph)
	    $requiredprog[] = 'cvsgraph';

	foreach($requiredprog as $prog) {
	    if (($info = $wgCVSwebCmd[$prog]) == null) 
		$errtype = 'cvsweb_notdefined';
	    elseif (!@file_exists($info[0]))
		$errtype = 'cvsweb_notfound';
	    elseif (!@is_executable($info[0]))
		$errtype = 'cvsweb_notexecutable';
	    elseif (!@is_file($info[0]))
		$errtype = 'cvsweb_notafile';
	    else
		continue;

	    $path = $path ? $path : '---';
	    $errfile .= "<tr><td>$prog</td><td>$path</td><td>$errtype</td></tr>\n";
	}
	if (!@is_dir($wgCVSwebCVSROOT) || !@is_executable($wgCVSwebCVSROOT))
	    $msg .= "<li>The CVSROOT <tt>$wgCVSwebCVSROOT</tt> is not accessible.</li>\n";

	if ($errfile) {
	    $msg .= "<li>Undefined or access error for the following commands:<table><tr style=\"text-align: left;\"><th>Command</th><th>Path</th><th>Error</th></tr>\n" . $errfile . "</table></li>\n";
	}


	if ($msg)
	    $this->errorpage('cvsweb_configerror', "
<p>The <b>CVSweb</b> feature is currently disabled due to one are several
errors in the configuration:</p>
<ul>$msg</ul>

<p>If you see this message on a production system you could wish to contact <a href=\"mailto:$wgEmergencyContact\">$wgEmergencyContact</a>.</p>
");
    }





    function displayConfigStatus() {
	global $wgOut;
	global $wgUser;
	global $wgCVSwebCVSROOT, $wgCVSwebCmd, $wgCVSwebImgPath;

	# Ensure that we have the rights
	if (!$wgUser->isSysop) {
	    $wgOut->sysopRequired();
	    return;
	}

	# 
	$wgOut->setPageTitle( "CVSweb configuration status" );
	$wgOut->setHTMLTitle( "CVSweb" );
	$wgOut->setRobotpolicy( "noindex,nofollow" );
	$wgOut->setArticleRelated( false );

	# Info layout (beg)
	$wgOut->addHTML( "<table><tr><td style=\"vertical-align: top;\"><img src=\"$wgCVSwebImgPath/info.png\"/></td><td>" );

	$wgOut->addHTML( "<table><tr><td><b>CVSROOT</b>:</td><td><tt>$wgCVSwebCVSROOT</tt></td></tr></table>" );

	# Display command list with options
	$wgOut->addHTML( "<table>" );
	$wgOut->addHTML( "<tr style=\"text-align: left;\"><th>Command</th><th>Path</th><th>Options</th></tr>" );
	foreach($wgCVSwebCmd as $prog => $path)
	    $wgOut->addHTML( "<tr><td><tt>$prog</tt></td><td><tt>{$path[0]}</tt></td><td><tt>{$path[1]}</tt></td></tr>" );
	$wgOut->addHTML( "</table>" );

	# Info layout (end)
	$wgOut->addHTML( "</td></tr></table> ");

    }



    function displayLog() {
	global $wgOut, $wgLang;
	global $wgScriptPath;
	global $wgCVSwebPath;
	

	$this->getFullLog($curbranch, $revisions, $state, $revsym,
			  $allrevisions, $keywordsubst, $log,
			  $difflines, $date, $author);

	# Display the information header
	$alltags = array();
	foreach (array_values($revsym) as $taglist)
	    $alltags = array_merge($alltags, $taglist);
	$alltags = array_unique($alltags);

	$branchname = array_key_exists($curbranch, $revsym)
	    ? implode(', ', $revsym[$curbranch]) : $curbranch;

	$this->genInfoHeader('', $alltags, null, $branchname, $keywordsubst);
	
	# If there is no such tag for that file, say it loud
	#  and stop here
	if ($this->mTag && !in_array($this->mTag, $alltags)) {
	    $wgOut->addHTML('<i style="color: red">' .
			    $wgLang->ucFirst( wfMsg ( 'cvsweb_nosuchtag' ) ) .
			    '</i>');
	    return;
	}

	foreach($revisions as $r) {
	    $br  = preg_replace('/\.\d+$/', '', $r);
	    $brp = preg_replace('/\.?\d+$/', '', $br);

	    # Put anchors for revisions, tags and branches.
	    $wgOut->addHTML('<a name="rev' . $r . '"></a>');
	    if (array_key_exists($r, $revsym))
		foreach ($revsym[$r] as $tag)
		    $wgOut->addHTML('<a name="' . $tag . '"></a>');
	    if (array_key_exists($br, $revsym))
		foreach ($revsym[$br] as $tag)
		    $wgOut->addHTML('<a name="' . $tag . '"></a>');



	    $wgOut->addHTML('<table class="cvsweb-rlog">');
	    $wgOut->addHTML('<tr><td style="width: 6em;"><b>Revision:</b></td><td>' . $r . ' | ' . "<a href=\"" . $this->buildURL("", array( 'rev' => $r, 'download' => 1 )) ."\">download</a> | <a href=\"" . $this->buildURL("", array( 'rev' => $r )) . "\">view</a></td></tr>\n");
	    $wgOut->addHTML('<tr><td><b>Modified:</b></td><td>' . 
			    gmstrftime("%b %d %Y %H:%M:%S", $date[$r]) .
			    ' UTC by ' . $author[$r] . 
			    ' &nbsp;&nbsp;&nbsp; <i>(' . 
			    $this->readableTime(time() - $date[$r], true) . 
			    ' ago)</i></td></tr>');
	    if (array_key_exists($br, $revsym)) {
		$list = array();
		foreach ($revsym[$br] as $tag)
		    $list[] = '<a href="' . 
			$this->buildURL('', array('tag' => $tag)) .
			'">' . $tag . '</a>';
		$wgOut->addHTML('<tr><td><b>Branches:</b></td><td>' .
				implode(', ', $list) . '</td></tr>');
	    }
	    if (array_key_exists($r, $revsym)) {
		$list = array();
		foreach ($revsym[$r] as $tag)
		    $list[] = '<a href="' . 
			$this->buildURL('', array('tag' => $tag)) .
			'">' . $tag . '</a>';

		$wgOut->addHTML('<tr><td><b>CVS&nbsp;Tags:</b></td><td>' . 
				implode(', ', $list) . '</td></tr>');
	    }
	    if (array_key_exists($r, $difflines))
		$wgOut->addHTML('<tr><td><b>Changes:</b></td><td>' . 
				$difflines[$r] . '</td></tr>');
	    $wgOut->addHTML('</table>');

	    $wgOut->addHTML("<pre>");
	    $wgOut->addHTML(htmlspecialchars($log[$r]));
	    $wgOut->addHTML("</pre><hr/>\n");
	    
	}
    }


    function displayDiff() {
	$this->errorpage('Not implemented yet', '<p>This feature has not been implemented yet. Please fell free to contribute and send your patch.</p>');
    }




    function checkCheckout($handle, $vers) {
	if ((($this->getline($handle)) === null) ||
	    (($this->getline($handle)) === null) ||
	    (($this->getline($handle)) === null) ||
	    (($this->getline($handle)) === null) ||
	    (($this->getline($handle)) === null))
	    $this->fatalerror("Error");
    }


    function buildURL($path, $uquery = array()) {
	global $wgScript;

	$query = array();

	if ($path[0] == '/')
	    $urlpath = $wgScript . '/' . $this->mPage . $path;
	else
	    $urlpath = $path;

	if ($uquery !== null) {
	    # 
	    $query['showattic'] = $this->mShowAttic;
	    $query['tag']       = $this->mTag;

	    $query = array_merge($query, $uquery);
	}
	
	#
	$query_string = '';
	foreach($query as $key => $value)
	    if($value)
		$query_string .= "&$key=$value";
	if ($query_string != '')
	    $query_string[0] = "?";
	
	return $urlpath . $query_string;
    }

    /*== Display ===========================================================*/

    /*
     * Display the content of the directory
     */
    function displayDirectory() {
	global $wgOut, $wgLang;
	global $wgCVSwebImgPath;	

	$files = array();
	$dirs  = array();


	# Retrieve files in the Attic if we need to show them
	#  in the parent directory.
	$atticdir = $this->atticPath($this->mFullPath);
	if ($this->mShowAttic && is_dir($atticdir) && file_exists($atticdir)) {
	    $dir = dir($atticdir);
	    while (($entry = $dir->read()) != false)
		$entries[] = 'Attic/' . $entry;
	    $dir->close();
	}

	# Retrieve file for the directory
	$dir = dir($this->mFullPath);
	while (($entry = $dir->read()) != false)
	    $entries[] = $entry;
	$dir->close();

	# Remove useless files and directories.
	# Normally files shouldn't be present here and in the Attic
	#  (if both are presents ignore the Attic)
	foreach($entries as $entry) {
	    $basename = basename($entry);
	    # Skip self and parent directory
	    if (($basename == '.') || ($basename == '..'))
		continue;
	    # Skip CVS lock and stale NFS files
	    if (preg_match('/^#cvs\\.|^,|^\\.nfs/', $basename))
		continue;

	    # Sort entries between directories and files 
	    #  and only keep RCS files (ie: ending with ,v)
	    $info = array(is_readable($this->mFullPath . $entry), $entry);
	    if (is_dir($this->mFullPath . $entry))
		$dirs [$basename] = $info;
	    elseif (preg_match('/,v$/', $entry))
		$files[$basename] = $info;
	}

	# Sort files and directories
	ksort($files);
	ksort($dirs);

	# Retrieve information about the RCS files
	#  (don't ask for file that we cannot access)
	$filelist = array();
	foreach($files as $name => $info)
	    if ($info[0])
		$filelist[] = $this->mFullPath . $info[1];
	$alltags  = array();      
	$fileinfo = count($filelist) ? $this->getLogs($filelist, $alltags)
	                             : array();

	# Display the information header
	$this->genInfoHeader("./", array_keys($alltags), 
			     $this->mShowAttic, null, null);

	# Display the content of README.cvs if present
	# So that we can have a per directory welcome message
	if (@is_readable($this->mFullPath . 'README.cvs.html,v')) {
	    $readme = 'README.cvs.html';
	    $quote  = null;
	} elseif (@is_readable($this->mFullPath . 'README.cvs,v')) {
	    $readme  = 'README.cvs';
	    $quote   = 'htmlspecialchars';
	}

	if ($readme) {
	    # Build command
	    $cmd = $this->getCmd('cvs');
	    $cmd .= " -d {$this->mCVSROOT}";
	    $cmd .= ' co -p';
	    if ($this->mRev)
		$cmd .= ' -r' . escapeshellarg($this->mRev);
	    $cmd .= ' ' . escapeshellarg($this->mPath . $readme);

	    # Checkout requested version
	    if (($h = @popen($cmd, 'r')) == null)
		$this->fatalerror("Unable to launch: <q><tt>$cmd</tt></q>");
	    while ($line = $this->getline($h))
		$msg .= $line;
	    pclose($h);
	    
	    # Display README
	    if ($quote)
		$wgOut->addHTML("<pre>" . $quote($msg) . "</pre>\n");
	    else
		$wgOut->addHTML($msg);
	    $wgOut->addHTML("<hr/>");
	}

	# Hold the preformated directory cells
	#  (needed if we want to make severals columns)
	$dircells = array();

	# Create 'parent' entry (but no need to go above '/')
	#  (if 'parent' is the repository call it 'Repo')

	if ($this->mAbsPath != '/') {
	    $ppath      = dirname($this->mAbsPath) . '/';
	    $ppath      = $ppath == '//' ? '/' : $ppath; # Fix double slash
	    $dircells[] = $this->mkDirEntry('parent', $this->buildURL($ppath),
					    true,
					    $ppath == '/' ? 'repo' : 'parent');
	}
	# Ensure that Attic is first.
	if (!$this->mShowAttic && $dirs['Attic'])
	    $dircells[] = $this->mkDirEntry('Attic',
					    $this->buildURL('Attic/'), 
					    $dirs['Attic'][0], 'attic');
	unset($dirs['Attic']);

	# Display information about directories

	# a workaround for a redirection problem - ODG

	foreach($dirs as $name => $info){
	    $nomodir="{$info[1]}";
	    if($nomodir[0]!='/'){
	       $nomodir='/'.$nomodir;
            }
	    $dircells[] = $this->mkDirEntry($name, 
					    $this->buildURL($nomodir),
						    $info[0]);
	}
	# Dump directory table
	$colmax = 3;
	if (($dirbreak = ceil(count($dircells) / $colmax)) < 4)
	    if (($dirbreak = count($dircells)) > 4)
		$dirbreak = 4;
	for ($i = 0 ; $i < $colmax ; $i++)
	    $col[$i] = array_splice($dircells, 0, $dirbreak);

	$wgOut->addHTML('<table class="cvsweb-dirlist">');
	for ($i = 0 ; $i < $dirbreak ; $i++) {
	    $wgOut->addHTML("<tr>");
	    for ($j = 0 ; $j < $colmax ; $j++)
		$wgOut->addHTML("<td>" . $col[$j][$i] . "</td>");
	    $wgOut->addHTML("<tr>\n");
	}
	$wgOut->addHTML('</table>');

	# Create HTML entries for files
	$fileentries = array();
	foreach($files as $name => $info) {
	    $cofile = substr($name, 0, strlen($name)-2);
	    if ($cells  = $this->mkFileEntry($cofile, $info, 
					     $fileinfo[$cofile])) {
		$tr = "<tr>";
		foreach($cells as $cell)
		    $tr .= "<td>$cell</td>";
		$tr .= "</tr>\n";
		$fileentries[] = $tr;
	    }
	}

	# Draw a separation between directories and files.
	if (count($dirscells) || count($fileentries))
	    $wgOut->addHTML('<hr/>');

	# Display information about CVS files
	# $files only contains RCS files (ie: ending with ,v)
	if (count($fileentries)) {
	    $wgOut->addHTML('<table class="cvsweb-filelist">');
	    $wgOut->addHTML('<tr><th></th><th>' . 
		$wgLang->ucFirst(wfMsg('cvsweb_file')).'</th><th>'. 
		$wgLang->ucFirst(wfMsg('cvsweb_rev')).'</th><th></th><th>'. 
		$wgLang->ucFirst(wfMsg('cvsweb_age')).'</th><th>'. 
		$wgLang->ucFirst(wfMsg('cvsweb_author')).'</th><th>'. 
		$wgLang->ucFirst(wfMsg('cvsweb_lastlog')).'</th></tr>'."\n");
	    foreach($fileentries as $e)
		$wgOut->addHTML($e);
	    $wgOut->addHTML("</table>\n");
	}
    }


    /*
     * Display the content of the file
     *  (if not possible, display an error page)
     */
    function displayFile() {
	global $wgOut, $wgLang;

	# Get information from the log
	$this->getFullLog($curbranch, $revisions, $state, $revsym,
			  $allrevisions, $keywordsubst, $log,
			  $difflines, $date, $author);

	# Check for unknown revision
	if (!in_array($this->mRev, $allrevisions))
	    $this->errorpage($wgLang->ucFirst(wfMsg('cvsweb_nosuchversion')),
			     wfMsg( 'cvsweb_nosuchversion_txt',
				    htmlspecialchars($this->mRev),
				    htmlspecialchars($this->mPath)));

	# Display the information header
	$alltags = array();
	foreach (array_values($revsym) as $taglist)
	    $alltags = array_merge($alltags, $taglist);
	$alltags = array_unique($alltags);

	$branchname = array_key_exists($curbranch, $revsym)
	    ? implode(', ', $revsym[$curbranch]) : $curbranch;

	$this->genInfoHeader('', $alltags, null, $branchname, $keywordsubst);

	# Retrieve mime type
	$mimetype = $this->getMimeType();	

	# And decide of the best way to view the file
	#  => Images
	if (preg_match('|^image/|', $mimetype)) {
	    $wgOut->addHTML("<img src=\"" . 
			    $this->buildURL('',array('rev'      => $this->mRev,
						     'download' => 1)) .
			    "\" alt=\"" .htmlspecialchars($this->mPath) .
			    "\"/>\n");
	#  => Text and non binary file
	} elseif ((preg_match('|^text/|', $mimetype)) ||
		  (!preg_match('/b/', $keywordsubst))) {
            # Build command
	    $cmd = $this->getCmd('cvs');
	    $cmd .= " -d {$this->mCVSROOT}";
	    $cmd .= ' co -p';
	    if ($this->mRev)
		$cmd .= ' -r' . escapeshellarg($this->mRev);
	    $cmd .= ' ' . escapeshellarg($this->mPath);

            # Checkout requested version
	    if (($h = @popen("$cmd 2>&1", 'r')) == null)
		$this->fatalerror("Unable to launch: <q><tt>$cmd</tt></q>");
	    $this->checkCheckout($h, $this->mRev);

            # Display the content of the checkout
	    $wgOut->addHTML("<pre>");
	    while (($line = $this->getline($h)) !== null)
		$wgOut->addHTML(htmlspecialchars($line));
	    $wgOut->addHTML("</pre>\n");

	    # Close
	    pclose($h);
	#  => Others
	} else {
	    $url = $this->buildURL('', array('rev'      => $this->mRev, 
					     'download' => 1));
	    $this->errorPage($wgLang->ucFirst( wfMsg( 'cvsweb_notviewable')),
			     wfMsg('cvsweb_notviewable_txt', 
				   htmlspecialchars($this->mPath), $url));
	}
    }


    /*
     * Display the graph of revisions
     */
    function displayGraph() {
	global $wgOut;
	$mapname = 'CvsGraphMap';

	# Get information about the file
	#  (add raise an error in case of access problems)
	$this->getFullLog($curbranch, $revisions, $state, $revsym, 
			  $allrevisions, $keywordsubst, $log, 
			  $difflines, $date, $author);

	# Build imagemap command
	$urlb = $this->buildURL('', array('tag'  => '%(%t%)' ));
	$urlr = $this->buildURL('', array('rev'  => '%(%R%)' ));
	$urld = $this->buildURL('', array('rev'  => '%(%R%)', 
					  'prev' => '%(%P%)' ));
	$cmd  = $this->getCmd('cvsgraph');
	$cmd .= ' -r ' . $this->mCVSROOT;
	$cmd .= ' -m ' . escapeshellarg(dirname($this->mPath));
	$cmd .= ' -i';
	$cmd .= ' -M ' . $mapname;
	$cmd .= ' -x x';
	$cmd .= ' ' . escapeshellarg('-Otitle=""');
	$cmd .= ' ' . escapeshellarg('-Omargin_top=10');
	$cmd .= ' ' . escapeshellarg('-Omap_branch_href="href=\"'.$urlb.'\""');
	$cmd .= ' ' . escapeshellarg('-Omap_rev_href="href=\"'.$urlr.'\""');
	$cmd .= ' ' . escapeshellarg('-Omap_diff_href="href=\"'.$urld.'\""');
	$cmd .= ' ' . escapeshellarg(basename($this->mPath) . ',v');

        # Retrieve the imagemap through a pipe
	if (($h = @popen($cmd, 'r')) == null)
	    $this->fatalerror("Unable to launch: <q><tt>$cmd</tt></q>");
	while(($line = $this->getline($h)) !== null)
	    $imagemap .= preg_replace('/(<map\s+id="([^"]+)")\s*>/', 
				     '$1 name="$2">', $line);
	pclose($h);

	# Display the information header
	$alltags = array();
	foreach (array_values($revsym) as $taglist)
	    $alltags = array_merge($alltags, $taglist);
	$alltags = array_unique($alltags);

	$this->genInfoHeader( '', $alltags, null, null, null );

	# Display the graph
	$graphimg = $this->buildURL('', array('download' => 1, 'graph' => 1));
	$wgOut->addHTML("<center><tt>" . 
			htmlspecialchars(basename($this->mPath)) . 
			"</tt><br/><img src=\"$graphimg\" usemap=\"#$mapname\"/></center>");
	$wgOut->addHTML($imagemap);
    }

    /*== Download ==========================================================*/

    /*
     * Send the graph of revisions.
     */
    function downloadGraph() {
	# Build command
	$cmd = $this->getCmd('cvsgraph');
	$cmd .= ' '    . escapeshellarg('-Otitle=""');
	$cmd .= ' '    . escapeshellarg('-Omargin_top=10');
	$cmd .= ' -r ' . $this->mCVSROOT;
	$cmd .= ' -m ' . escapeshellarg(dirname($this->mPath));
	$cmd .= ' '    . escapeshellarg(basename($this->mPath) . ',v');

	# Open a pipe to the command
	if (($h = @popen($cmd, "r")) == null)
	    $this->fatalerror("Unable to launch: <q><tt>$cmd</tt></q>");

	# Send the data
	# => From now, we can't display media wiki page anymore!
	header("Content-type: image/png");
	fpassthru($h);

	# Close and stop here!
	pclose($h);
	wfAbruptExit();
    }


    /*
     * Send the requested file
     */
    function downloadFile() {
	# Try to guess mime type
	if (($mimetype = $this->getMimeType()) == null) {
	    $this->getFullLog($curbranch, $revisions, $state, $revsym, 
			      $allrevisions, $keywordsubst, $log,
			      $difflines, $date, $author);
	    $mimetype = preg_match('/b/', $keywordsubst) 
		? 'application/octet-stream' : 'text/plain';
	}
	
        # Build command
	$cmd = $this->getCmd('cvs');
	$cmd .= " -d {$this->mCVSROOT}";
	$cmd .= ' co -p';
	if ($this->mRev)
	    $cmd .= ' -r' . escapeshellarg($this->mRev);
	$cmd .= ' ' . escapeshellarg($this->mPath);

	# Open a pipe to the command
	if (($h = @popen($cmd, "r")) == null)
	    $this->fatalerror("Unable to launch: <q><tt>$cmd</tt></q>");

	# Send the data
	# => From now, we can't display media wiki page anymore!
	header("Content-type: $mimetype");
	fpassthru($h);

	# Close and stop here!
	pclose($h);
	wfAbruptExit();
    }

    /*======================================================================*/

    /*
     * Try to establish the mime type of the file
     *  (unfortunately its only based on the extention)
     */
    function getMimeType() {
	global $wgCVSwebMime, $wgCVSwebMimeFile;

	# Isolate extension
	if (!preg_match('/\.([^\.]+)$/', $this->mPath, $res))
	    return null;
	$ext = $res[1];

	# Try the provide mime hash
	if (array_key_exists($ext, $wgCVSwebMime))
	    return $wgCVSwebMime[$ext];

	# If defined look at the file description of mime types
	if ($wgCVSwebMimeFile != null) {
	    $regex = '/^\s*(\S+\/\S+)\s.+\b' . preg_quote($ext) . '\b/';

	    if (($h = @fopen($wgCVSwebMimeFile, 'r')) == null)
		$this->fatalerror("Unable to open: <q><tt>$wgCVSwebMimeFile</tt></q>");
	    while(($line = $this->getline($h)) !== null)
		if (preg_match($regex, $line, $res)) {
		    fclose($h);
		    return $wgCVSwebMime[$ext] = $res[0];
		}
	    fclose($h);
	}
	
	# Try a fallback based on the mime hash
	if (array_key_exists('*', $wgCVSwebMime))
	    return $wgCVSwebMime['*'];

	# Give up
	return null;
    }


    /*
     * Generate the information header
     */
    function genInfoHeader($action, $tags, $showattic, $branch, $keysubst) {
	global $wgOut;
	global $wgLang;
	global $wgCVSwebWithGraph;

	$knownTag = in_array($this->mTag, $tags);

	foreach ($tags as $tag)
	    $taglist .= "<option " . ($tag == $this->mTag 
				      ? 'selected="selected"' : '') .
	                "value=\"$tag\">$tag</option>\n";



	$wgOut->addHTML('<form method="get" action="' . $action . '">' .
			'<table class="cvsweb-info"><tr><td><b>' . 
			$wgLang->ucFirst(wfMsg('cvsweb_location')) . 
			':</b></td><td colspan="2" style="width:100%;">' . 
			$this->clickablePath() . 
			"</td></tr>\n");
	$wgOut->addHTML('<tr><td><label for="tag"><b>' .
			$wgLang->ucFirst(wfMsg( $knownTag 
						? 'cvsweb_curtag' 
						: 'cvsweb_availabletags')) .
			':</b></label></td><td><select id="tag" name="tag" onchange="this.form.submit()"><option value="">' .
			$wgLang->ucFirst( wfMsg('cvsweb_alltags')). ' / ' . 
			$wgLang->ucFirst(wfMsg('cvsweb_dfltbranch')) . 
			'</option>' . $taglist . '</select></td><td style="text-align: right;">');

	if ($showattic !== null)
	    $wgOut->addHTML('<input id="showattic" name ="showattic" type="checkbox" ' . ($showattic ? 'checked="checked"' : '') . 'value="1" onchange="this.form.submit()"/><label for="showattic">' . wfMsg('cvsweb_showattic') . '</label> | ');

	$wgOut->addHTML('<input type="submit" value="Apply"></td></tr>');

	if ($this->mRev)
	    $wgOut->addHTML("<tr><td><b>" . 
			    $wgLang->ucFirst(wfMsg('cvsweb_revision')) .
			    ":</b></td><td>" . htmlspecialchars($this->mRev) .
			    " | <a href=\"" . $this->buildURL("", array( 'rev' => $this->mRev, 'download' => 1 )) ."\">download</a>" .
			    ($wgCVSwebWithGraph ? " | <a href=\"" . $this->buildURL('', array( 'graph' => 1)) . "\">graph</a>" : "") . "</td></tr>\n");

	if (!$knownTag && $this->mTag)
	    $wgOut->addHTML("<tr><td><b>" . 
			    $wgLang->ucFirst(wfMsg('cvsweb_curtag')) . 
			    ":</b></td><td>" . htmlspecialchars($this->mTag) .
			    "</td></tr>\n");

	if ($branch    !== null)
	    $wgOut->addHTML("<tr><td><b>" . 
			    $wgLang->ucFirst(wfMsg('cvsweb_dfltbranch')) . 
			    ":</b></td><td>" . htmlspecialchars($branch) .
			    "</td></tr>\n");

	if ($keysubst  !== null)
	    $wgOut->addHTML("<tr><td><b>" .
			    $wgLang->ucFirst(wfMsg('cvsweb_keysubst')) . 
			    ":</b></td><td>" . htmlspecialchars($keysubst) .
			    "</td></tr>\n");

	$wgOut->addHTML('</table></form><hr/>');
    }


    /*
     * Make a directory entry (used by displayDirecotry)
     */
    function mkDirEntry($entry, $url, $canAccess = true, $type = 'dir') {
	global $wgLang;
	global $wgCVSwebImgPath;

	switch($type) {
	case 'parent':
	    $img = 'back.png';
	    $txt ='<b><i>'.$wgLang->ucfirst(wfMsg('cvsweb_parent')).'</i></b>';
	    break;
	case 'repo':
	    $img = 'back.png';
	    $txt = '<b><i>'.$wgLang->ucFirst(wfMsg('cvsweb_repo')).'</i></b>';
	    break;
	case 'attic':
	    $img = 'folder.sec.png';
	    $txt = '<b>Attic</b>';
	    break;
	case 'dir':
	    $img = 'dir.png';
	    $txt = htmlspecialchars($entry);
	    break;
	}
	
	return $canAccess 
	    ? "<a href=\"$url\"><img src=\"$wgCVSwebImgPath/$img\"/></a>&nbsp;<a href=\"$url\">$txt</a>"
	    : "<i><img src=\"$wgCVSwebImgPath/$img\"/>&nbsp;$txt</i>";
    }


    /*
     * Make a file entry (used by displayDirecotry)
     */
    function mkFileEntry($entry, $info, $data) {
	global $wgCVSwebImgPath;
	$path   = substr($info[1], 0, strlen($info[1])-2);
	$img    = strncmp('Attic/', $path, 6) ? 'text.png' : 'broken.png';

	if ($data == null) {
	    return null;
	} elseif ($info[0]) {
	    $rev      = $data[0];
	    $author   = htmlspecialchars($data[3]);
	    $log      = htmlspecialchars($data[2]);
	    $age      = $this->readableTime(time() - $data[1]);
	    $url      = $this->buildURL($path);
	    $urlrev   = $this->buildURL($path, array( 'rev'   => $rev ));
	    $urlgraph = $this->buildURL($path, array( 'graph' => 1 ));

	    return array("<a href=\"$url\"><img src=\"$wgCVSwebImgPath/$img\"/></a>",
			 "<a href=\"$url\">$entry</a>",
			 "<a href=\"$urlrev\">$rev</a>",
			 "<a href=\"$urlgraph\"><img src=\"$wgCVSwebImgPath/minigraph.png\" alt=\"G\"/></a>",
			 $age, $author, $log);
	} else {
	    return array("<img src=\"$wgCVSwebImgPath/$img\"/>", $entry,
			 '---', '---', '---', '---', 
			 '<i>' . wfMsg('cvsweb_filedenied') . '</i>');
	}
    }


    /*
     * Transform a path, to a path containing a reference to the Attic.
     */
    function atticPath($path) {
	$epath    = explode('/', $path);
	$filename = array_pop($epath);
	return implode('/', $epath) . "/Attic/$filename";
    }


    /*
     * Fetch the full path of program 'prog' and add it the additional
     * options if necessary.
     */
    function getCmd($prog) {
	$cmdinfo = $this->mCmd[$prog];
	if ($cmdinfo == null)
	    $this->fatalerror( wfMsg( "cvsweb_unexp_program") );
	return $cmdinfo[1] ? $cmdinfo[0] . ' ' . $cmdinfo[1] : $cmdinfo[0];
    }


    /*
     * Returns a htmlified path (each element is a link).
     */
    function clickablePath() {
	global $wgLang;

	$epath       = explode( '/', $this->mPath );
	$filename    = array_pop( $epath );
	$partialpath = array();
	$clickpath   = array();

	# Build the path of directories
	foreach($epath as $entry) {
	    $partialpath[] = $entry;
	    $clickpath[] = '<a href="'. 
		$this->buildURL('/' . implode('/', $partialpath) . '/') . '">' 
		. $entry . '</a>';
	}

	# If we have a filename, add it, otherwise create a dummy entry
	# for the directory final '/'
	if ($filename) {
	    $partialpath[] = $filename;
	    $clickpath[] = '<a href="' . 
		$this->buildURL('/' . implode('/', $partialpath)) . '">' 
		. $filename . '</a>';
	} else {
	    $clickpath[] = '';
	}

	# Add the repository link
	array_unshift($clickpath, '<a href="' . $this->buildURL( '/' ) .
		      '"><b><i>' . $wgLang->ucfirst( wfMsg( "cvsweb_repo" ) ) .
		      '</i></b></a>');

	# Return the clickable path
	return implode('&nbsp;/&nbsp;', $clickpath);
    }


    /*
     * Allow comparison of version number.
     */
    function revcmp($rev1, $rev2) {
	if (preg_match('/[^\d.]/', $rev1) || preg_match('/[^\d.]/', $rev2))
	    return 0;
	$r1 = explode('.', $rev1);
	$r2 = explode('.', $rev2);
	while (($a = array_shift($r1)) && ($b = array_shift($r2)))
	    if ($a != $b)
		return $a - $b;
	if (count($r1)) return  1;
	if (count($r2)) return -1;
	return 0;
    }

    /*
     * Returns an approximation of a delay.
     * ::TODO:: need to be localized
     */
    function readableTime($secs, $longapprox = false) {
        # This function works correctly for time >= 2 seconds.
	if ($secs < 2)
	    return 'very little time';

	# Initialise data
	$desc   = array( 1        => 'second',
			 60       => 'minute',
			 3600     => 'hour',
			 86400    => 'day',
			 604800   => 'week',
			 2628000  => 'month',
			 31536000 => 'year' );
	$breaks = array_keys($desc);

	# Compute break
	for ($i = 0 ; $i < count($breaks) && ($secs >= 2 * $breaks[$i]) ; $i++)
	    ;
	$i--;
	$break  = $breaks[$i];

	# First approximation
	$retval = floor($secs / $break);
	if ($retval > 1) $retval = "$retval&nbsp;{$desc[$break]}s";
	else             $retval = "$retval&nbsp;{$desc[$break]}";

	# Second approximation
	if ($longapprox && $i > 0) {
	    $rest = $secs % $break;
	    $i--;
	    $break = $breaks[$i];
	    $resttime = floor($rest / $break);
	    if     ($resttime > 1)
		$retval = "$retval, $resttime&nbsp;{$desc[$break]}s";
	    elseif ($resttime == 1)
		$retval = "$retval, $resttime&nbsp;{$desc[$break]}";
	}

	# Returns value
	return $retval;
    }

    /*== Parser from cvsweb 3.0.1 ==========================================*/

    function getFullLog(&$curbranch, &$revdisplayorder, &$state,
			&$revsym, &$allrevisions, &$keywordsubst,
			&$log, &$difflines, &$date, &$author) {
	$allrevisions = array();
	$revsym       = array();
	$difflines    = array();
	$author       = array();
	$curbranch = null;
	$symnames = 0;

	# Create rlog command
	$cmd = $this->getCmd('rlog');
	if ($this->mRev) 
	    $cmd .= ' ' . escapeshellarg("-r{$this->mRev}");
	$cmd .= ' ' . escapeshellarg($this->mFullPath . ',v');
	
	if (($h = @popen($cmd, "r")) == null)
	    $this->fatalerror("Unable to launch: <q><tt>$cmd</tt></q>");
  

	while (($line = $this->getline($h)) !== null) {
	    if ($symnames)
		if (preg_match('/^\s+([^:]+):\s+([\d\.]+)/', $line, $res)) {
		    $symrev{$res[1]} = $res[2];
		    continue;
		} else {
		    $symnames = 0;
		}
	    
	    if       (preg_match('/^head:\s+([\d\.]+)/',   $line, $res)) {
		$head = $res[1];
	    } elseif (preg_match('/^branch:\s+([\d\.]+)/', $line, $res)) {
		$curbranch = $res[1];
	    } elseif (preg_match('/^symbolic names/',      $line, $res)) {
		$symnames = 1;
	    } elseif (preg_match('/^keyword substitution: (.+)$/',$line,$res)){
		$keywordsubst = $res[1];
	    } elseif (preg_match('/^-----/',               $line)) {
		break;
	    }
	}
	
	if ($curbranch == null)
	    $curbranch = preg_replace('/\.\d+$/', '', $head); 

        # each log entry is of the form:
        # ----------------------------
        # revision 3.7.1.1
        # date: 1995/11/29 22:15:52; author: fenner;  state: Exp;  lines: +5 -3
        # log info
	# ----------------------------

        # For a locked revision, the first line after the separator 
        # becomes smth like
        # revision 9.19       locked by: vassilii;

	while (!preg_match('/^={77}$/', $line)) {
	    if (($line = $this->getline($h)) == null)
		break;
	    if (preg_match('/^revision (\d+(?:\.\d+)+)/', $line, $res)) {
		$rev = $res[1];
		array_unshift($allrevisions, $rev);
	    } elseif (preg_match('/^={77}$|^-{28}$/', $line)) {
		continue;
	    } else {
                # The rlog output is syntactically ambiguous.  We must
                # have guessed wrong about where the end of the last log
                # message was.
                # Since this is likely to happen when people put rlog output
                # in their commit messages, don't even bother keeping
                # these lines since we don't know what revision they go with
                # any more.
		continue;
	    }
	    $line = $this->getline($h);
	    if (preg_match('|^date:\s+(\d+)/(\d+)/(\d+)\s+(\d+):(\d+):(\d+);\s+author:\s+(\S+);\s+state:\s+(\S+);\s+(lines:\s+([0-9\s+-]+))?|',$line,$res)) {
		$yr = $res[1];
		if ($yr > 100) $yr -= 1900;
		$date  [$rev] = gmmktime($res[4], $res[5], $res[6], 
				       $res[2], $res[3] - 1, $yr);
		$author[$rev] = $res[7];
		$state [$rev] = $res[8];
		if ($res[10])
		    $difflines[$rev] = $res[10];
	    } else {
		$this->fatalerror("Error while parsing RCS output");
	    }

	    while (($line = $this->getline($h)) != null) {
		if (preg_match('/^branches:\s/', $line))
		    continue;
		if (preg_match('/^={77}$|^-{28}$/', $line))
		    break;
		$log[$rev] .= $line;
	    }
	}
	pclose($h);
	usort($allrevisions, create_function('$a,$b', 
			     'return CVSwebPage::revcmp($a,$b);'));
	$revorder = array_reverse($allrevisions);

        # HEAD is an artificial tag which is simply the highest tag
	# number on the main branch, unless there is a branch tag in 
	# the RCS file in which case it's the highest revision on that
	# branch.  Find it by looking through @revorder; it is the first
	# commit listed on the appropriate branch. This is not neccesary
	# the same revision as marked as head in the RCS file.

	$headrev = $curbranch || "1";
	$symrev['MAIN'] = preg_replace('/(\d+)$/', '0.$1', $headrev);

	foreach ($revorder as $rev)
	    if (preg_match('/^(\S*)\.\d+$/', $rev, $res) && 
		($headrev == $res[1])) {
		$symrev['HEAD'] = $rev;
		break;
	    }
	if (!array_key_exists('HEAD', $symrev))
	    $symrev['HEAD'] = preg_match('/\.\d+$/', '', $headrev);

        # Now that we know all of the revision numbers, we can associate
        # absolute revision numbers with all of the symbolic names, and
        # pass them to the form so that the same association doesn't have
        # to be built then.
	$symrevkeys = array_keys($symrev);
	sort($symrevkeys);
	foreach (array_reverse($symrevkeys) as $k) {
	    $rev = $symrev[$k];
	    if (preg_match('/^((.*)\.)?\b0\.(\d+)$/', $rev, $res)) {
		$branchnames[]= $k;

	        # A revision number of A.B.0.D really translates into
	        # "the highest current revision on branch A.B.D".
	        #
	        # If there is no branch A.B.D, then it translates into
	        # the head A.B .
	        #
	        # This reasoning also applies to the main branch A.B,
	        # with the branch number 0.A, with the exception that
	        # it has no head to translate to if there is nothing on
	        # the branch, but I guess this can never happen?
	        #
	        # (the code below gracefully forgets about the branch
	        # if it should happen)
		$head      = $res[2];
		$branch    = $res[3];
		$branchrev = $head . ($head != '' ? '.' : '') . $branch;
		$rev       = $head;

		$regex = '/^' . preg_quote($branchrev) . '\b/';
		
		foreach ($revorder as $r) {
		    if (preg_match($regex, $r)) {
			$rev = $branchrev;
			break;
		    }
		}
		if ($rev == '')
		    continue;
		
		if (($rev != $head) && ($head != '')) {
		    if ($branchpoint[$head])
			$branchpoint[$head] .= ', ';
		    $branchpoint[$head] .= $k;
		}
	    }
	    if (! array_key_exists($rev, $revsym))
		$revsym[$rev] = array();
	    $revsym[$rev][]= $k;
	}
	
	if ($onlyonbranch = $this->mTag) {
	    $onlyonbranch = $symrev[$onlyonbranch];
	    $tmponlyonbranch = preg_replace('/\b0\./', '', $onlyonbranch);

	    if ($onlyonbranch != $tmponlyonbranch) {
		$onlyonbranch = $tmponlyonbranch;
		$onlybranchpoint = preg_replace('/\.\d+$/', '', $onlyonbranch);
	    } else {
		$onlybranchpoint = $onlyonbranch;
	    }
	    
	    if (($onlyonbranch == '') || ($onlybranchpoint == ''))
		$this->fatalerror("Tag {$this->mTag} not defined");
	}
	
	$revisions = array();
	foreach ($allrevisions as $k) {
	    $br  = preg_replace('/\.\d+$/', '', $k);
	    $brp = preg_replace('/\.\d+$/', '', $br);
	    if ($onlyonbranch
		&& ($br != $onlyonbranch)
		&& ($k  != $onlybranchpoint)) {
		continue;
	    }
	    array_unshift($revisions, $k);
	}

	/*
	if ($logsort == "date") {
	    
            # Sort the revisions in commit order an secondary sort on revision
	    # (secondary sort needed for imported sources, or the first main
            # revision gets before the same revision on the 1.1.1 branch)
	    @revdisplayorder =
		sort { $date{$b} <=> $date{$a} || -revcmp($a, $b) } @revisions;
	} elsif ($logsort eq "rev") {
	    
            # Sort the revisions in revision order, highest first
	    @revdisplayorder = reverse sort { revcmp($a, $b) } @revisions;
	} else {
            # No sorting. Present in the same order as rlog / cvs log
	    @revdisplayorder = @revisions;
	}
	*/
	$revdisplayorder = $revisions;
	return null;
    }


    function getLogs($filelst, &$alltags) {
	global $wgOut;

	if ($alltags == null)
	    $alltags = array();

	$alltags = array();

	# Ensure that the file list is correctly quoted for shell processing
	if (!is_array($filelst))
	    $filelst = array($filelst);


	$dirname = $this->mCVSROOT . $this->mPath;
	$tag     = $this->mTag;

	$cmd = $this->getCmd('rlog');
	if ($tag == '')
	    $cmd .= ' -r';

	$state = 'start';

	$h = new Xargs($cmd, $filelst);
	$h->open();
	while (($line = $h->getline()) !== null) {
	    while(true) {
		switch ($state) {
		case 'start':
		    $rev          = '';
		    $revwanted    = '';
		    $branch       = '';
		    $branchpoint  = '';
		    $filename     = '';
		    $log          = '';
		    $revision     = '';
		    $symrev       = array();
		    $filetags     = array();
		    $keywordsubst = '';
		    $state        = 'head';
		    break;
		    
		case 'head':
		    if       (preg_match("/^Working file: (.+)$/",
				   $line, $res)) {
			$filename = $res[1];
		    } elseif (preg_match("/^head: (.+)$/",
					 $line, $res)) {
			$head = $res[1];
		    } elseif (preg_match("/^branch: (.+)$/",
					 $line, $res)) {
			$branch = $res[1];
		    } elseif (preg_match("/^keyword substitution: (.+)$/",
					 $line, $res)) {
			$keywordsubst = $res[1];
		    } elseif (preg_match("/^symbolic names:/", $line)) {
			$state = 'tags';
			if ($branch === '')
			    $branch = preg_replace('/\.\d+$/', '', $head);
			$branch = preg_replace('/(\d+)$/', '0.$1', $branch);
			$symrev['MAIN']  = $branch;
			$symrev['HEAD']  = $branch;
			$alltags['MAIN'] = 1;
			$alltags['HEAD'] = 1;
			$filetags[]      = 'MAIN';
			$filetags[]      = 'HEAD';
		    } elseif (preg_match('/^-{28}$/', $line)) {
			$state = 'log';
			$rev   = '';
			$date  = '';
			$log   = '';
			if (!strncmp($filename, $dirname, strlen($dirname)))
			    $filename = substr($filename, strlen($dirname));
		    }
		    continue 3; # process next line

		case 'tags':
		    if (preg_match('/^\s+([^:]+):\s+([\d\.]+)\s*$/',
				   $line, $res)) {
			$filetags[]       = $res[1];
			$symrev [$res[1]] = $res[2];
			$alltags[$res[1]] = 1;
			continue 3; # process next line
		    } elseif (preg_match('/^\S/', $line)) {
			if ($tag != '') {
			    if (array_key_exists($tag, $symrev) ||
				($tag == 'HEAD')) {
				$ftag        = $tag == 'HEAD' ? 'MAIN' : $tag;
				$revwanted   = $symrev[$ftag];
				$branch      = preg_replace('/\b0\./',   '', 
							    $revwanted);
				$branchpoint = preg_replace('/\.?\d+$/', '', 
							    $branch);
				if ($revwanted != $branch)
				    $revwanted = '';
			    } elseif ($tag != 'HEAD') {
				$state = 'skip';
				continue 3; # process next line
			    }
			}
			$state = 'head';
			break;
		    }
		    /* NOT REACHED */

		case 'log':
		    if (preg_match('/^={77}$|^-{28}$/', $line)) {
			$revbranch = preg_replace('/\.\d+$/', '', $rev);
			if (($revwanted == '') && ($branch != '') &&
			    ($branch == $revbranch) || ($tag == ''))
			    $revwanted = $rev;
			if (($revwanted != '' 
			     ? $rev == $revwanted 
			     : ($branchpoint != '' 
				? $rev == $branchpoint 
				: false))) {
			    $finfo = array($rev, $date, $log, $author,
					   $filename, $keywordsubst);
			    $name = preg_replace('/\/.*/', '', $filename);
			    $fileinfo[$name] = $finfo;
			    if ($rev == $revwanted) {
				$state = 'done';
				break 2;
			    }
			}
			$rev  = '';
			$date = '';
			$log  = '';
		    } elseif (($date == '') &&
			      preg_match('%^date:\s+(\d+)/(\d+)/(\d+)\s+(\d+):(\d+):(\d+);%', 
					 $line, $res)) {
			$yr = $res[1];
			if ($yr > 100) $yr -= 1900;
			$date = gmmktime($res[4], $res[5], $res[6], 
					 $res[2], $res[3] - 1, $yr);
			preg_match('/author: ([^;]+)/', $line, $res);
			$author = $res[1];
			$state = 'log';
			$log = '';
		    } elseif (($rev == '') &&
			      preg_match('/^revision (\d+(?:\.\d+)+).*$/', 
					 $line, $res)) {
			$rev = $res[1];
		    } else {
			$log .= $line;
		    }
		    continue 3; # process next line
		    
		case 'done':
		case 'skip':
		    break 2;
		}
	    }

	    if (preg_match('/^={77}$/', $line))
		$state = 'start';
	}
	$h->close();
	return $fileinfo;
    }


    /*== Enhanced function from OutputPage =================================*/

    /*
     * Perform a redirect
     */
    function redirect($url, $code = 301) {
	global $wgOut;
	$wgOut->redirect($url, $code);
    }


    /*
     * Display a fatal error page (ie: internal error)
     * Calling this method abort the execution flow.
     */
    function fatalerror($message) {
	global $wgOut;
	global $wgCVSwebImgPath;

	$wgOut->setPageTitle( 'CVSweb: ' . wfMsg( "internalerror" ) );
	$wgOut->setRobotpolicy( "noindex,nofollow" );
	$wgOut->setArticleRelated( false );
	$wgOut->enableClientCache( false );

	$wgOut->mBodytext = "<table><tr><td style=\"vertical-align: top;\"><img src=\"$wgCVSwebImgPath/warning.png\"/></td><td>$message</td></tr></table>";

	$wgOut->output();
	wfAbruptExit();
    }


    /*
     * Display an error page.
     * Calling this method abort the execution flow.
     */
    function errorpage( $title, $msg, $type = 'warning' ) {
	global $wgOut;
	global $wgCVSwebImgPath;
	global $wgScript;
    
	$wgOut->setPageTitle( 'CVSweb: ' . $title );
	$wgOut->setHTMLTitle( wfMsg( "errorpagetitle" ) );
	$wgOut->setRobotpolicy( "noindex,nofollow" );
	$wgOut->setArticleRelated( false );
	$wgOut->enableClientCache( false );

	$wgOut->mBodytext = "<table><tr><td style=\"vertical-align: top;\"><img src=\"$wgCVSwebImgPath/$type.png\"/></td><td>$msg</td></tr></table>";

	# Establish the referer
	#  (and cut the http://server/ to avoid external link icon)
	$serverurl = 'http://' . $_SERVER['SERVER_NAME'] . '/';
	if ($referer = $_SERVER['HTTP_REFERER']) {
	    if (!strncmp($referer, $serverurl, strlen($serverurl)))
		$referer = substr($referer, strlen($serverurl)-1);
	} else {
	    $referer = $this->buildURL('/', null);
	}

	# Is referer from Special:CVSweb
	$serverurl = $wgScript . '/' . $this->mPage . '/';
	if (!strncmp($referer, $serverurl, strlen($serverurl)))
	    $where = $this->mPage;
	else
	    $where = wfMsg('cvsweb_referer');

	$wgOut->addHTML(wfMsg("cvsweb_returnto", $referer, $where));
	$wgOut->output();
	wfAbruptExit();
    }
}

    


SpecialPage::addPage( new CVSwebPage );

global $wgMessageCache;
$wgMessageCache->addMessages(
array("cvsweb"               => "Web interface to CVS",
      "cvsweb_notafile"      => "not a file",
      "cvsweb_notexecutable" => "not executable",
      "cvsweb_notfound"      => "not found",
      "cvsweb_notdefined"    => "not defined",
      "cvsweb_configerror"   => "Configuration error",
      "cvsweb_repo"          => "repo",
      "cvsweb_repository"    => "repository",
      "cvsweb_module"        => "module",
      "cvsweb_parent"        => "parent",
      "cvsweb_program"       => "program",
      "cvsweb_path"          => "path",
      "cvsweb_option"        => "option",
      "cvsweb_error"         => "error",
      "cvsweb_apply"         => "apply",
      "cvsweb_access_error"  => "access error",
      "cvsweb_filedenied"    => "permission denied",
      "cvsweb_showattic"     => "show files in the Attic",
      "cvsweb_curtag"        => "current&nbsp;tag",
      "cvsweb_availabletags" => "available&nbsp;tags",
      "cvsweb_location"      => "location",
      "cvsweb_keysubst"      => "keyword&nbsp;subst.",
      "cvsweb_dfltbranch"    => "default&nbsp;branch",
      "cvsweb_location"      => "location",
      "cvsweb_alltags"       => "all tags",
      "cvsweb_file"          => "file",
      "cvsweb_rev"           => "rev.",
      "cvsweb_age"           => "age",
      "cvsweb_author"        => "author",
      "cvsweb_lastlog"       => "last log entry",
      "cvsweb_download"      => "download",
      "cvsweb_view"          => "view",
      "cvsweb_graph"         => "graph",
      "cvsweb_revision"      => "revision",
      "cvsweb_referer"       => "referer",
      "cvsweb_notviewable"   => "not viewable",
      "cvsweb_notviewable_txt" => "<p>The requested file <q><tt>\$1</tt></q> can't be display inside a page, but you can still <a href=\"\$2\">download</a> it.",

      "cvsweb_returnto"      => "<p>Return to <a href=\"\$1\">\$2</a></p>\n",
      "cvsweb_nosuchversion" => "no such version",
      "cvsweb_nosuchversion_txt" => "<p>The version <q>\$1</q> you requested for the file <tt>\$2</tt> doesn't exist.</p>",
      "cvsweb_nosuchtag"     => "no such tag",
      "cvsweb_requirement"   => "<p>PERL regulars expressions should be compiled in PHP.</p>",
      "cvsweb_reqconfig"     =>  "
<p>The <b>CVSweb</b> feature is currently disabled.</p>
<p>To enable it you need to defines the following variables in your 
<tt>LocalSetting.php</tt>:
  <ul>
    <li><tt>\$wgCVSwebCVSROOT</tt>: Location of the CVSROOT directory</li>
    <li><tt>\$wgCVSwebCmd</tt>: Mapping of the command used [ fullpath, options ]</li>
  </ul>
</p>
 ",
      "cvsweb_unexp_program" => "<p>Trying to request a program (<tt>$prog</tt>), that wasn't supposed to be used.</p>",
      "cvsweb_accessdir_forbidden" => "The directory <q><tt>\$1</tt></q> exists, but its <b>access is forbidden</b> by the underlying file system.",
      "cvsweb_accessfile_forbidden" => "The file <q><tt>\$1</tt></q> exists, but its <b>access is forbidden</b> by the underlying file system.",
      "cvsweb_access_notpossible" => "The file or directory requested <q><tt>\$1</tt></q> either <b>doesn't exist</b> or its <b>access is forbidden</b> (perhaps due to one of its parent directories)."
 ));

}

?>
