<?php
// WebSVN - Subversion repository viewing via the web using PHP
// Copyright (C) 2004-2006 Tim Armes
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
// --
//
// svn-look.php
//
// Svn bindings
//
// These binding currently use the svn command line to achieve their goal.  Once a proper
// SWIG binding has been produced for PHP, there'll be an option to use that instead.

require_once("include/utils.php");

// {{{ Classes for retaining log information ---

$debugxml = false;

class SVNMod {
  var $action = '';
  var $copyfrom = '';
  var $copyrev = '';
  var $path = '';
}

class SVNListEntry {
  var $rev = 1;
  var $author = '';
  var $date = '';
  var $committime;
  var $age = '';
  var $file = '';
  var $isdir = false;
}

class SVNList {
  var $entries; // Array of entries
  var $curEntry; // Current entry

  var $path = ''; // The path of the list
}

class SVNLogEntry {
  var $rev = 1;
  var $author = '';
  var $date = '';
  var $committime;
  var $age = '';
  var $msg = '';
  var $path = '';

  var $mods;
  var $curMod;
}

function SVNLogEntry_compare($a, $b) {
  return strnatcasecmp($a->path, $b->path);
}

class SVNLog {
  var $entries; // Array of entries
  var $curEntry; // Current entry

  var $path = ''; // Temporary variable used to trace path history

  // findEntry
  //
  // Return the entry for a given revision

  function findEntry($rev) {
    foreach ($this->entries as $index => $entry) {
      if ($entry->rev == $rev) {
        return $index;
      }
    }
  }
}

// }}}

// {{{ XML parsing functions---

$curTag = '';

$curList = 0;

// {{{ listStartElement

function listStartElement($parser, $name, $attrs) {
  global $curList, $curTag, $debugxml;

  switch ($name) {
    case "LIST":
      if ($debugxml) print "Starting list\n";

      if (sizeof($attrs)) {
        while (list($k, $v) = each($attrs)) {
          switch ($k) {
            case "PATH":
              if ($debugxml) print "Path $v\n";
              $curList->path = $v;
              break;
          }
        }
      }
      break;

    case "ENTRY":
      if ($debugxml) print "Creating new entry\n";
      $curList->curEntry = new SVNListEntry;

      if (sizeof($attrs)) {
        while (list($k, $v) = each($attrs)) {
          switch ($k) {
            case "KIND":
              if ($debugxml) print "Kind $v\n";
              $curList->curEntry->isdir = ($v == 'dir');
              break;
          }
        }
      }
      break;

    case "COMMIT":
      if ($debugxml) print "Commit\n";

      if (sizeof($attrs)) {
        while (list($k, $v) = each($attrs)) {
          switch ($k) {
            case "REVISION":
              if ($debugxml) print "Revision $v\n";
              $curList->curEntry->rev = $v;
              break;
          }
        }
      }
      break;

    default:
      $curTag = $name;
      break;
  }
}

// }}}

// {{{ listEndElement

function listEndElement($parser, $name) {
  global $curList, $debugxml, $curTag;

  switch ($name) {
    case "ENTRY":
      if ($debugxml) print "Ending new list entry\n";
      if ($curList->curEntry->isdir) {
      	$curList->curEntry->file .= '/';
      }
      $curList->entries[] = $curList->curEntry;
      $curList->curEntry = null;
      break;
  }

  $curTag = "";
}

// }}}

// {{{ listCharacterData

function listCharacterData($parser, $data) {
  global $curList, $curTag, $lang, $debugxml;

  switch ($curTag) {
    case "NAME":
      if ($debugxml) print "Name: $data\n";
      if (empty($data)) return;
      $curList->curEntry->file .= $data;
      break;

    case "AUTHOR":
      if ($debugxml) print "Author: $data\n";
      if (empty($data)) return;
      $curList->curEntry->author .= htmlentities($data, ENT_COMPAT, "UTF-8");
      break;

    case "DATE":
      if ($debugxml) print "Date: $data\n";
      $data = trim($data);
      if (empty($data)) return;

      sscanf($data, "%d-%d-%dT%d:%d:%d.", $y, $mo, $d, $h, $m, $s);

      $mo = substr("00".$mo, -2);
      $d = substr("00".$d, -2);
      $h = substr("00".$h, -2);
      $m = substr("00".$m, -2);
      $s = substr("00".$s, -2);

      $curList->curEntry->date = "$y-$mo-$d $h:$m:$s GMT";

      $committime = strtotime($curList->curEntry->date);
      $curList->curEntry->committime = $committime;
      $curtime = time();

      // Get the number of seconds since the commit
      $agesecs = $curtime - $committime;
      if ($agesecs < 0) $agesecs = 0;

      $curList->curEntry->age = datetimeFormatDuration($agesecs, true, true);

      break;
  }
}

// }}}

$curLog = 0;

// {{{ logStartElement

function logStartElement($parser, $name, $attrs) {
  global $curLog, $curTag, $debugxml;

  switch ($name) {
    case "LOGENTRY":
      if ($debugxml) print "Creating new log entry\n";
      $curLog->curEntry = new SVNLogEntry;
      $curLog->curEntry->mods = array();

      $curLog->curEntry->path = $curLog->path;

      if (sizeof($attrs)) {
        while (list($k, $v) = each($attrs)) {
          switch ($k) {
            case "REVISION":
              if ($debugxml) print "Revision $v\n";
              $curLog->curEntry->rev = $v;
              break;
          }
        }
      }
      break;

    case "PATH":
      if ($debugxml) print "Creating new path\n";
      $curLog->curEntry->curMod = new SVNMod;

      if (sizeof($attrs)) {
        while (list($k, $v) = each($attrs)) {
          switch ($k) {
            case "ACTION":
              if ($debugxml) print "Action $v\n";
              $curLog->curEntry->curMod->action = $v;
              break;

            case "COPYFROM-PATH":
              if ($debugxml) print "Copy from: $v\n";
              $curLog->curEntry->curMod->copyfrom = $v;
              break;

            case "COPYFROM-REV":
              $curLog->curEntry->curMod->copyrev = $v;
              break;
          }
        }
      }

      $curTag = $name;
      break;

    default:
      $curTag = $name;
      break;
  }
}

// }}}

// {{{ logEndElement

function logEndElement($parser, $name) {
  global $curLog, $debugxml, $curTag;

  switch ($name) {
    case "LOGENTRY":
      if ($debugxml) print "Ending new log entry\n";
      $curLog->entries[] = $curLog->curEntry;
      break;

    case "PATH":
      if ($debugxml) print "Ending path\n";
      $curLog->curEntry->mods[] = $curLog->curEntry->curMod;
      break;

    case "MSG":
      $curLog->curEntry->msg = trim($curLog->curEntry->msg);
      if ($debugxml) print "Completed msg = '".$curLog->curEntry->msg."'\n";
      break;
  }

  $curTag = "";
}

// }}}

// {{{ logCharacterData

function logCharacterData($parser, $data) {
  global $curLog, $curTag, $lang, $debugxml;

  switch ($curTag) {
    case "AUTHOR":
      if ($debugxml) print "Author: $data\n";
      if (empty($data)) return;
      $curLog->curEntry->author .= htmlentities($data, ENT_COMPAT, "UTF-8");
      break;

    case "DATE":
      if ($debugxml) print "Date: $data\n";
      $data = trim($data);
      if (empty($data)) return;

      sscanf($data, "%d-%d-%dT%d:%d:%d.", $y, $mo, $d, $h, $m, $s);

      $mo = substr("00".$mo, -2);
      $d = substr("00".$d, -2);
      $h = substr("00".$h, -2);
      $m = substr("00".$m, -2);
      $s = substr("00".$s, -2);

      $curLog->curEntry->date = "$y-$mo-$d $h:$m:$s GMT";

      $committime = strtotime($curLog->curEntry->date);
      $curLog->curEntry->committime = $committime;
      $curtime = time();

      // Get the number of seconds since the commit
      $agesecs = $curtime - $committime;
      if ($agesecs < 0) $agesecs = 0;

      $curLog->curEntry->age = datetimeFormatDuration($agesecs, true, true);

      break;

    case "MSG":
      if ($debugxml) print "Msg: '$data'\n";
      $curLog->curEntry->msg .= htmlentities($data, ENT_COMPAT, "UTF-8");
      break;

    case "PATH":
      if ($debugxml) print "Path name: '$data'\n";
      $data = trim($data);
      if (empty($data)) return;

      $curLog->curEntry->curMod->path .= $data;

      // The XML returned when a file is renamed/branched in inconsistant.  In the case
      // of a branch, the path information doesn't include the leafname.  In the case of
      // a rename, it does.  Ludicrous.

      if (!empty($curLog->path)) {
        $pos = strrpos($curLog->path, "/");
        $curpath = substr($curLog->path, 0, $pos);
        $leafname = substr($curLog->path, $pos + 1);
      } else {
        $curpath = "";
        $leafname = "";
      }

      if ($curLog->curEntry->curMod->action == "A") {
        if ($debugxml) print "Examining added path '".$curLog->curEntry->curMod->copyfrom."' - Current path = '$curpath', leafname = '$leafname'\n";
        if ($data == $curLog->path) { // For directories and renames
          if ($debugxml) print "New path for comparison: '".$curLog->curEntry->curMod->copyfrom."'\n";
          $curLog->path = $curLog->curEntry->curMod->copyfrom;
        } else if ($data == $curpath || $data == $curpath."/") { // Logs of files that have moved due to branching
          if ($debugxml) print "New path for comparison: '".$curLog->curEntry->curMod->copyfrom."/$leafname'\n";
          $curLog->path = $curLog->curEntry->curMod->copyfrom."/$leafname";
        }
      }
      break;
  }
}

// }}}

// }}}

// {{{ internal functions (_topLevel and _listSort)

// Function returns true if the give entry in a directory tree is at the top level

function _topLevel($entry) {
  // To be at top level, there must be one space before the entry
  return (strlen($entry) > 1 && $entry{0} == " " && $entry{1} != " ");
}

// Function to sort two given directory entries.  Directories go at the top

function _listSort($e1, $e2) {
  $isDir1 = $e1->file{strlen($e1->file) - 1} == "/";
  $isDir2 = $e2->file{strlen($e2->file) - 1} == "/";

  if ($isDir1 && !$isDir2) return -1;
  if ($isDir2 && !$isDir1) return 1;

  return strnatcasecmp($e1->file, $e2->file);
}

// }}}

// {{{ encodePath

// Function to encode a URL without encoding the /'s

function encodePath($uri) {
  global $config;

  $uri = str_replace(DIRECTORY_SEPARATOR, "/", $uri);

  $parts = explode('/', $uri);
  for ($i = 0; $i < count($parts); $i++) {
    if ( function_exists("mb_detect_encoding") && function_exists("mb_convert_encoding")) {
      $parts[$i] = mb_convert_encoding($parts[$i], "UTF-8", mb_detect_encoding($parts[$i]));
    }

    $parts[$i] = rawurlencode($parts[$i]);
  }

  $uri = implode('/', $parts);

  // Quick hack.  Subversion seems to have a bug surrounding the use of %3A instead of :

  $uri = str_replace("%3A" ,":", $uri);

  // Correct for Window share names
  if ( $config->serverIsWindows==true ) {
    if (substr($uri, 0,2) == "//") {
      $uri = "\\".substr($uri, 2, strlen($uri));
    }

    if (substr($uri, 0,10)=="file://///" ) {
      $uri="file:///\\".substr($uri, 10, strlen($uri));
    }
  }

  return $uri;
}

// }}}

// The SVNRepository class

class SVNRepository {
  var $repConfig;

  function SVNRepository($repConfig) {
    $this->repConfig = $repConfig;
  }

  // {{{ highlightLine
  //
  // Distill line-spanning syntax highlighting so that each line can stand alone
  // (when invoking on the first line, $attributes should be an empty array)
  // Invoked to make sure all open syntax highlighting tags (<font>, <i>, <b>, etc.)
  // are closed at the end of each line and re-opened on the next line

  function highlightLine($line, &$attributes) {
    $hline = "";

    // Apply any highlighting in effect from the previous line
    foreach ($attributes as $attr) {
      $hline.=$attr['text'];
    }

    // append the new line
    $hline.=$line;

    // update attributes
    for ($line = strstr($line, "<"); $line; $line = strstr(substr($line,1), "<")) {
      if (substr($line,1,1) == "/") { // if this closes a tag, remove most recent corresponding opener
        $tagNamLen = strcspn($line, "> \t", 2);
        $tagNam = substr($line,2,$tagNamLen);
        foreach (array_reverse(array_keys($attributes)) as $k) {
          if ($attributes[$k]['tag'] == $tagNam) {
            unset($attributes[$k]);
            break;
          }
        }
      } else { // if this opens a tag, add it to the list
        $tagNamLen = strcspn($line, "> \t", 1);
        $tagNam = substr($line,1,$tagNamLen);
        $tagLen = strcspn($line, ">") + 1;
        $attributes[] = array('tag' => $tagNam, 'text' => substr($line,0,$tagLen));
      }
    }

    // close any still-open tags
    foreach (array_reverse($attributes) as $attr) {
      $hline.="</".$attr['tag'].">";
    }

    // XXX: this just simply replaces [ and ] with their entities to prevent
    //    it from being parsed by the template parser; maybe something more
    //    elegant is in order?
    $hline = str_replace('[', '&#91;', str_replace(']', '&#93;', $hline) );
    return $hline;
  }

  // }}}

  // {{{ getFileContents
  //
  // Dump the content of a file to the given filename

  function getFileContents($path, $filename, $rev = 0, $pipe = "", $perLineHighlighting = false) {
    global $config, $extEnscript;

    // If there's no filename, we'll just deliver the contents as it is to the user
    if ($filename == "") {
      $path = encodepath($this->repConfig->path.$path);
      passthru(quoteCommand($config->svn." cat ".$this->repConfig->svnParams().quote($path).' -r '.$rev.' '.$pipe));
      return;
    }

    // Get the file contents info

    $ext = strrchr($path, ".");
    $l = @$extEnscript[$ext];

    if ($l == "php") {
      // Output the file to the filename
      $path = encodepath($this->repConfig->path.$path);
      $cmd = quoteCommand($config->svn." cat ".$this->repConfig->svnParams().quote($path).' -r '.$rev.' > '.quote($filename));
      @exec($cmd);

      // Get the file as a string (memory hogging, but we have no other options)
      $content = '<PRE>' . highlight_file($filename, true) . '</PRE>';

      // Destroy the previous version, and replace it with the highlighted version
      $f = fopen($filename, "w");
      if ($f) {
        // The highlight file function doesn't deal with line endings very nicely at all.  We'll have to do it
        // by hand.

        // Remove the first line generated by highlight()
        $pos = strpos($content, "\n");
        $content = substr($content, $pos+1);

        $content = explode("<br />", $content);

        if ($perLineHighlighting) {
          // If we need each line independently highlighted (e.g. for diff or blame)
          //  hen we'll need to filter the output of the highlighter
          // to make sure tags like <font>, <i> or <b> don't span lines

          // $attributes is used to remember what highlighting attributes
          // are in effect from one line to the next
          $attributes = array(); // start with no attributes in effect

          foreach ($content as $line) {
            fputs($f, $this->highlightLine(rtrim($line),$attributes)."\n");
          }
        } else {
          foreach ($content as $line) {
            fputs($f, rtrim($line)."\n");
          }
        }

        fclose($f);
      }

    } else {
      if ($l !== null && $config->useGeshi) {
        $this->applyGeshi($path, $filename, $rev, $l);

      } else if ($config->useEnscript) {
        // Get the files, feed it through enscript, then remove the enscript headers using sed
        //
        // Note that the sec command returns only the part of the file between <PRE> and </PRE>.
        // It's complicated because it's designed not to return those lines themselves.

        $path = encodepath($this->repConfig->path.$path);
        $cmd = quoteCommand($config->svn." cat ".$this->repConfig->svnParams().quote($path).' -r '.$rev.' | '.
                      $config->enscript." --language=html ".
                      ($l ? "--color --pretty-print=$l" : "")." -o - | ".
                      $config->sed." -n ".$config->quote."1,/^<PRE.$/!{/^<\\/PRE.$/,/^<PRE.$/!p;}".$config->quote." > $filename");
        @exec($cmd);

      } else {
        $path = encodepath(str_replace(DIRECTORY_SEPARATOR, "/", $this->repConfig->path.$path));
        $cmd = quoteCommand($config->svn." cat ".$this->repConfig->svnParams().quote($path).' -r '.$rev.' > '.quote($filename));
        @exec($cmd);
      }
    }
  }

  // }}}

  // {{{ applyGeshi
  //
  // perform syntax highlighting using geshi

  function applyGeshi($path, $filename, $rev = 0, $l, $return = false) {
    global $config;

    // Output the file to the filename
    $path = encodepath($this->repConfig->path.$path);
    $cmd = quoteCommand($config->svn." cat ".$this->repConfig->svnParams().quote($path).' -r '.$rev.' > '.quote($filename));
    @exec($cmd);

    $source = file_get_contents($filename);
    require_once 'lib/geshi.php';
    $geshi = new GeSHi($source, $l);
    if ($return) {
      return $geshi->parse_code();
    } else {
      $code = $geshi->parse_code();
      $code = preg_replace("/^<pre.*?>/", '', $code);
      $code = preg_replace("/<\/pre>$/", '', $code);
      $f = @fopen($filename, 'w');
      fwrite($f, $code);
      fclose($f);
    }
  }

  // }}}

  // {{{ listFileContents
  //
  // Print the contents of a file without filling up Apache's memory

  function listFileContents($path, $rev = 0) {
    global $config, $extEnscript;

    $pre = false;

    // Get the file contents info

    $ext = strrchr($path, ".");
    $l = @$extEnscript[$ext];

    // Deal with php highlighting internally
    if ($l == "php") {
      $tmp = tempnam("temp", "wsvn");

      // Output the file to a temporary file
      $path = encodepath($this->repConfig->path.$path);
      $cmd = quoteCommand($config->svn." cat ".$this->repConfig->svnParams().quote($path).' -r '.$rev.' > '.$tmp);
      @exec($cmd);
      $tmpStr = file_get_contents($tmp);
      $tmpStr = str_replace(array("\r\n"), array("\n"), $tmpStr);
      print '<PRE>';
      highlight_string($tmpStr);
      print '</PRE>';
      @unlink($tmp);
    } else if ($l !== null && $config->useGeshi) {
      $tmp = tempnam("temp", "wsvn");
      print $this->applyGeshi($path, $tmp, $rev, $l, true);
      unlink($tmp);
    } else {
      if ($config->useEnscript) {
        $path = encodepath($this->repConfig->path.$path);
        $cmd = quoteCommand($config->svn." cat ".$this->repConfig->svnParams().quote($path).' -r '.$rev.' | '.
          $config->enscript." --language=html ".
          ($l ? "--color --pretty-print=$l" : "")." -o - | ".
          $config->sed." -n ".$config->quote."/^<PRE.$/,/^<\\/PRE.$/p".$config->quote
        );
      } else {
        $path = encodepath($this->repConfig->path.$path);
        $cmd = quoteCommand($config->svn." cat ".$this->repConfig->svnParams().quote($path).' -r '.$rev);
        $pre = true;
      }

      if ($result = popen($cmd, "r")) {
        if ($pre) echo "<PRE>";

        while (!feof($result)) {
          $line = fgets($result, 1024);
          if ($pre) $line = replaceEntities($line, $this->repConfig);

          print hardspace($line);
        }

        if ($pre) echo "</PRE>";

        pclose($result);
      }
    }
  }

  // }}}

  // {{{ getBlameDetails
  //
  // Dump the blame content of a file to the given filename

  function getBlameDetails($path, $filename, $rev = 0) {
    global $config;

    $path = encodepath($this->repConfig->path.$path);
    $cmd = quoteCommand($config->svn." blame ".$this->repConfig->svnParams().quote($path).' -r '.$rev.' > '.quote($filename));

    @exec($cmd);
  }

  // }}}

  // {{{ getProperty

  function getProperty($path, $property, $rev = 0) {
    global $config;

    $path = encodepath($this->repConfig->path.$path);

    if ($rev > 0) {
      $rev = ' -r '.$rev;
    } else {
      $rev = '';
    }

    $ret = runCommand($config->svn." propget $property ".$this->repConfig->svnParams().quote($path).$rev, true);

    // Remove the surplus newline
    if (count($ret)) {
      unset($ret[count($ret) - 1]);
    }

    return implode("\n", $ret);
  }

  // }}}

  // {{{ exportDirectory
  //
  // Exports the directory to the given location

  function exportDirectory($path, $filename, $rev = 0) {
    global $config;

    $path = encodepath($this->repConfig->path.$path);
    $cmd = quoteCommand($config->svn." export ".$this->repConfig->svnParams().quote($path).' -r '.$rev.' '.quote($filename));

    @exec($cmd);
  }

  // }}}

  // {{{ getList

  function getList($path, $rev = 0) {
    global $config, $curList, $vars, $lang;

    $xml_parser = xml_parser_create("UTF-8");
    xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, true);
    xml_set_element_handler($xml_parser, "listStartElement", "listEndElement");
    xml_set_character_data_handler($xml_parser, "listCharacterData");

    // Since directories returned by svn log don't have trailing slashes (:-(), we need to remove
    // the trailing slash from the path for comparison purposes

    if ($path{strlen($path) - 1} == "/" && $path != "/") {
      $path = substr($path, 0, -1);
    }

    $curList = new SVNList;
    $curList->entries = array();
    $curList->path = $path;

    // Get the list info
    $path = encodepath($this->repConfig->path.$path);

    if ($rev == 0) {
      $headlog = $this->getLog("/", "", "", true, 1);
      if (isset($headlog->entries[0])) $rev = $headlog->entries[0]->rev;
    }
    $revStr = "-r $rev";

    $cmd = quoteCommand($config->svn." list --xml $revStr ".$this->repConfig->svnParams().quote($path));

    $descriptorspec = array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w'));

    $resource = proc_open($cmd, $descriptorspec, $pipes);
    $error = "";

    if (!is_resource($resource)) {
      echo "<p>".$lang['BADCMD'].": <code>".$cmd."</code></p>";
      exit;
    }

    $handle = $pipes[1];
    $firstline = true;
    while (!feof($handle)) {
      $line = fgets($handle);
      if (!xml_parse($xml_parser, $line, feof($handle))) {
        if (xml_get_error_code($xml_parser) != 5) {
          // errors can contain sensitive info! don't echo this ~J
          error_log(sprintf("XML error: %s (%d) at line %d column %d byte %d\ncmd: %s",
                  xml_error_string(xml_get_error_code($xml_parser)),
                  xml_get_error_code($xml_parser),
                  xml_get_current_line_number($xml_parser),
                  xml_get_current_column_number($xml_parser),
                  xml_get_current_byte_index($xml_parser),
                  $cmd));
          exit;
        } else {
          $vars["error"] = $lang["UNKNOWNREVISION"];
          return 0;
        }
      }
    }

    while (!feof($pipes[2])) {
      $error .= fgets($pipes[2]);
    }

    $error = toOutputEncoding(trim($error));

    fclose($pipes[0]);
    fclose($pipes[1]);
    fclose($pipes[2]);

    proc_close($resource);

    if (!empty($error)) {
      echo "<p>".$lang['BADCMD'].": <code>".$cmd."</code></p><p>".nl2br($error)."</p>";
      exit;
    }

    xml_parser_free($xml_parser);

    // Sort the entries into alphabetical order with the directories at the top of the list
    usort($curList->entries, "_listSort");

    return $curList;
  }

  // }}}

  // {{{ getLog

  function getLog($path, $brev = "", $erev = 1, $quiet = false, $limit = 2) {
    global $config, $curLog, $vars, $lang;

    $xml_parser = xml_parser_create("UTF-8");
    xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, true);
    xml_set_element_handler($xml_parser, "logStartElement", "logEndElement");
    xml_set_character_data_handler($xml_parser, "logCharacterData");

    // Since directories returned by svn log don't have trailing slashes (:-(), we need to remove
    // the trailing slash from the path for comparison purposes

    if ($path{strlen($path) - 1} == "/" && $path != "/") {
      $path = substr($path, 0, -1);
    }

    $curLog = new SVNLog;
    $curLog->entries = array();
    $curLog->path = $path;

    $revStr = "";

    if ($brev && $erev) {
      $revStr = "-r$brev:$erev";
    } else if ($brev) {
      $revStr = "-r$brev:1";
    }

    if (($config->subversionMajorVersion > 1 || $config->subversionMinorVersion >=2) && $limit != 0) {
      $revStr .= " --limit $limit";
    }

    // Get the log info
    $path = encodepath($this->repConfig->path.$path);
    $info = "--verbose";
    if ($quiet) $info = "--quiet";

    $cmd = quoteCommand($config->svn." log --xml $info $revStr ".$this->repConfig->svnParams().quote($path));

    $descriptorspec = array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w'));

    $resource = proc_open($cmd, $descriptorspec, $pipes);
    $error = "";

    if (!is_resource($resource)) {
      echo "<p>".$lang['BADCMD'].": <code>".$cmd."</code></p>";
      exit;
    }

    $handle = $pipes[1];
    $firstline = true;
    while (!feof($handle)) {
      $line = fgets($handle);
      if (!xml_parse($xml_parser, $line, feof($handle))) {
        if (xml_get_error_code($xml_parser) != 5) {
          // errors can contain sensitive info! don't echo this ~J
          error_log(sprintf("XML error: %s (%d) at line %d column %d byte %d\ncmd: %s",
                  xml_error_string(xml_get_error_code($xml_parser)),
                  xml_get_error_code($xml_parser),
                  xml_get_current_line_number($xml_parser),
                  xml_get_current_column_number($xml_parser),
                  xml_get_current_byte_index($xml_parser),
                  $cmd));
          exit;
        } else {
          $vars["error"] = $lang["UNKNOWNREVISION"];
          return 0;
        }
      }
    }

    while (!feof($pipes[2])) {
      $error .= fgets($pipes[2]);
    }

    $error = toOutputEncoding(trim($error));

    fclose($pipes[0]);
    fclose($pipes[1]);
    fclose($pipes[2]);

    proc_close($resource);

    if (!empty($error)) {
      echo "<p>".$lang['BADCMD'].": <code>".$cmd."</code></p><p>".nl2br($error)."</p>";
      exit;
    }

    xml_parser_free($xml_parser);

    foreach ($curLog->entries as $entryKey => $entry) {
      $fullModAccess = true;
      $anyModAccess = (count($entry->mods) == 0);
      foreach ($entry->mods as $modKey => $mod) {
        $access = $this->repConfig->hasReadAccess($mod->path);
        if ($access) {
          $anyModAccess = true;
        } else {
          // hide modified entry when access is prohibited
          unset($curLog->entries[$entryKey]->mods[$modKey]);
          $fullModAccess = false;
        }
      }
      if (!$fullModAccess) {
        // hide commit message when access to any of the entries is prohibited
        $curLog->entries[$entryKey]->msg = '';
      }
      if (!$anyModAccess) {
        // hide author and date when access to all of the entries is prohibited
        $curLog->entries[$entryKey]->author = '';
        $curLog->entries[$entryKey]->date = '';
        $curLog->entries[$entryKey]->committime = '';
        $curLog->entries[$entryKey]->age = '';
      }
    }

    return $curLog;
  }

  // }}}

}

// {{{ initSvnVersion

function initSvnVersion(&$major, &$minor) {
  global $config;

  $ret = runCommand($config->svn_noparams." --version", false);

  if (preg_match("~([0-9]?)\.([0-9]?)\.([0-9]?)~",$ret[0],$matches)) {
    $major = $matches[1];
    $minor = $matches[2];
  }

  $config->setSubversionMajorVersion($major);
  $config->setSubversionMinorVersion($minor);
}

// }}}
