<?php
# vim:et:ts=3:sts=3:sw=3:fdm=marker:

// WebSVN - Subversion repository viewing via the web using PHP
// Copyright © 2004-2006 Tim Armes, Matt Sicker
//
// 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
//
// --
//
// bugtraq.inc
//
// Functions for accessing the bugtraq properties and replacing issue IDs
// with URLs.
//
// For more information about bugtraq, see 
// http://svn.collab.net/repos/tortoisesvn/trunk/doc/issuetrackers.txt

class Bugtraq
{
   // {{{ Properties

   var $msgstring;
   var $urlstring;
   var $logregex;
   var $append;

   var $firstPart;
   var $firstPartLen;
   var $lastPart;
   var $lastPartLen;

   var $propsfound = false;

   // }}}

   // {{{ __construct($rep, $svnrep, $path)

   function Bugtraq($rep, $svnrep, $path)
   {
      global $config;
 
      if ($rep->getBugtraq())
      {
         $pos = strrpos($path, "/");
         $parent = substr($path, 0, $pos + 1);
         $this->append = true;
         
         $enoughdata = false;
         while(!$enoughdata && (strpos($parent, "/") !== false))
         {
            if (empty($this->msgstring)) $this->msgstring = $svnrep->getProperty($parent, 'bugtraq:message');
            if (empty($this->logregex)) $this->logregex = $svnrep->getProperty($parent, 'bugtraq:logregex');
            if (empty($this->urlstring)) $this->urlstring = $svnrep->getProperty($parent, 'bugtraq:url');
            if (empty($this->append)) $this->append = ($svnrep->getProperty($parent, 'bugtraq:append') == "true");
                  
            $parent = substr($parent, 0, -1);        // Remove the trailing slash
            $pos = strrpos($parent, "/");            // Find the last trailing slash
            $parent = substr($parent, 0, $pos + 1);  // Find the previous parent directory
            $enoughdata = ((!empty($this->msgstring) || !empty($this->logregex)) && !empty($this->urlstring));   
         }
         
         $this->msgstring = trim(@$this->msgstring);
         $this->urlstring = trim(@$this->urlstring);
            
         if ($enoughdata && !empty($this->msgstring))
            $this->initPartInfo();
                        
         if ($enoughdata)
            $this->propsfound = true;
      }
   }

   // }}}

   // {{{ initPartInfo()
   
   function initPartInfo()
   {
      if (($bugidpos = strpos($this->msgstring, "%BUGID%")) !== false && strpos($this->urlstring, "%BUGID%") !== false)
      {
         // Get the textual parts of the message string for comparison purposes
         $this->firstPart = substr($this->msgstring, 0, $bugidpos);
         $this->firstPartLen = strlen($this->firstPart);
         $this->lastPart = substr($this->msgstring, $bugidpos + 7);
         $this->lastPartLen = strlen($this->lastPart);
      }  
   }

   // }}}

   // {{{ replaceIDs($message)

   function replaceIDs($message)
   {
      if ($this->propsfound)
      {
         // First we search for the message string
         
         $logmsg  = "";
         $message = rtrim($message);
         
         if ($this->append)
         {         
            // Just compare the last line            
            if (($offset = strrpos($message, "\n")) !== false)
            {
               $logmsg = substr($message, 0, $offset + 1);
               $bugLine = substr($message, $offset + 1);
            }
            else
               $bugLine = $message;
         }
         else
         {
            if (($offset = strpos($message, "\n")) !== false)
            {
               $bugLine = substr($message, 0, $offset);
               $logmsg = substr($message, $offset);
            }
            else
               $bugLine = $message;            
         }

         // Make sure that our line really is an issue tracker message

         if (((strncmp($bugLine, $this->firstPart, $this->firstPartLen) == 0)) &&
             strcmp(substr($bugLine, -$this->lastPartLen, $this->lastPartLen), $this->lastPart) == 0)
         {
            // Get the issues list
            if ($this->lastPartLen > 0)
               $issues = substr($bugLine, $this->firstPartLen, -$this->lastPartLen);
            else
               $issues = substr($bugLine, $this->firstPartLen);
            
            // Add each reference to the first part of the line
            $line = $this->firstPart;
            while ($pos = strpos($issues, ","))
            {
               $issue  = trim(substr($issues, 0, $pos));
               $issues = substr($issues, $pos + 1);
               
               $line .= "<a href=\"".str_replace("%BUGID%", $issue, $this->urlstring)."\">$issue</a>, ";
            }
            $line .= "<a href=\"".str_replace("%BUGID%", trim($issues), $this->urlstring)."\">".trim($issues)."</a>".$this->lastPart;
            
            if ($this->append)
               $message = $logmsg.$line;
            else
               $message = $line.$logmsg;
         }
      
         // Now replace all other instances of bug IDs that match the regex

         if ($this->logregex)
         {
            $message = rtrim($message);
            $line = "";
            $allissues = "";
            
            $lines = split("\n", $this->logregex);
            $regex_all = "~".$lines[0]."~";
            $regex_single = @$lines[1];
            
            if (empty($regex_single))
            {
               // If the property only contains one line, then the pattern is only designed
               // to find one issue number at a time.  e.g. [Ii]ssue #?(\d+).  In this case
               // we need to replace the matched issue ID with the link.
               
               if ($numMatches = preg_match_all($regex_all, $message, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE))       
               {
                  $addedOffset = 0;
                  for ($match = 0; $match < $numMatches; $match++)
                  {
                     $issue = $matches[$match][1][0];
                     $issueOffset = $matches[$match][1][1];
                 
                     $issueLink = "<a href=\"".str_replace("%BUGID%", $issue, $this->urlstring)."\">".$issue."</a>";
                     $message = substr_replace($message, $issueLink, $issueOffset + $addedOffset, strlen($issue));
                     $addedOffset += strlen($issueLink) - strlen($issue);
                  }
               }   
            }     
            else
            {
               // It the property contains two lines, then the first is a pattern for extracting
               // multiple issue numbers, and the second is a pattern extracting each issue
               // number from the multiple match.  e.g. [Ii]ssue #?(\d+)(,? ?#?(\d+))+ and (\d+)
               
               while (preg_match($regex_all, $message, $matches, PREG_OFFSET_CAPTURE))       
               {
                  $completeMatch = $matches[0][0];
                  $completeMatchOffset = $matches[0][1];
                  
                  $replacement = $completeMatch;
                                    
                  if ($numMatches = preg_match_all("~".$regex_single."~", $replacement, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE))       
                  {
                     $addedOffset = 0;
                     for ($match = 0; $match < $numMatches; $match++)
                     {
                        $issue = $matches[$match][1][0];
                        $issueOffset = $matches[$match][1][1];

                        $issueLink = "<a href=\"".str_replace("%BUGID%", $issue, $this->urlstring)."\">".$issue."</a>";
                        $replacement = substr_replace($replacement, $issueLink, $issueOffset + $addedOffset, strlen($issue));                     
                        $addedOffset += strlen($issueLink) - strlen($issue);
                     }             
                  }
                  
                  $message = substr_replace($message, $replacement, $completeMatchOffset, strlen($completeMatch));
               }   
            }
         }
      }

      return $message;
   }

   // }}}

}

// The BugtraqTestable class is a derived class that is used to test the matching
// abilities of the Bugtraq class.  In particular, it allows for the initialisation of the
// class without the need for a repository.

class BugtraqTestable extends Bugtraq
{
   // {{{ __construct()

   function BugtraqTestable()
   {
      // This constructor serves to assure that the parent constructor is not
      // called.
   }

   // }}}

   // {{{ setUpVars($message, $url, $regex, $append)

   function setUpVars($message, $url, $regex, $append)
   {
      $this->msgstring = $message;
      $this->urlstring = $url;
      $this->logregex = $regex;
      $this->append = $append;
      $this->propsfound = true;

      $this->initPartInfo();
   }

   // }}}

   // {{{ setMessage($message)
   
   function setMessage($message)
   {
      $this->msgstring = $message;
   }

   // }}}

   // {{{ setUrl($url)

   function setUrl($url)
   {
      $this->urlstring = $url;
   }

   // }}}

   // {{{ setRegex($regex)

   function setRegEx($regex)
   {
      $this->logregex = $regex;
   }

   // }}}

   // {{{ setAppend($append)

   function setAppend($append)
   {
      $this->append = $append;
   }

   // }}}

   // {{{ printVars()
   
   function printVars()
   {
      echo "msgstring = ".$this->msgstring."\n";
      echo "urlstring = ".$this->urlstring."\n";
      echo "logregex = ".$this->logregex."\n";
      echo "append = ".$this->append."\n";
                
      echo "firstPart = ".$this->firstPart."\n";
      echo "firstPartLen = ".$this->firstPartLen."\n";
      echo "lastPart = ".$this->lastPart."\n";
      echo "lastPartLen = ".$this->lastPartLen."\n";
   }

   // }}}
}

// {{{ test_bugtraq()

function test_bugtraq()
{
   $tester = new BugtraqTestable;
   
   $tester->setUpVars("BugID: %BUGID%",
                      "http://bugtracker/?id=%BUGID%",
                      "[Ii]ssue #?(\d+)",
                      true);
                      
   //$tester->printVars();
   
   $res = $tester->replaceIDs("BugID: 789\n".
                              "This is a test message that refers to issue #123 and\n".
                              "issue #456.\n".
                              "BugID: 789");
                              
   echo nl2br($res)."<p>";
   
   $res = $tester->replaceIDs("BugID: 789, 101112\n".
                              "This is a test message that refers to issue #123 and\n".
                              "issue #456.\n".
                              "BugID: 789, 101112");

   echo nl2br($res)."<p>";
   
   $tester->setAppend(false);
   
   $res = $tester->replaceIDs("BugID: 789\n".
                              "This is a test message that refers to issue #123 and\n".
                              "issue #456.\n".
                              "BugID: 789");
                              
   echo nl2br($res)."<p>";
   
   $res = $tester->replaceIDs("BugID: 789, 101112\n".
                              "This is a test message that refers to issue #123 and\n".
                              "issue #456.\n".
                              "BugID: 789, 101112");

   echo nl2br($res)."<p>";
   
   $tester->setUpVars("BugID: %BUGID%",
                      "http://bugtracker/?id=%BUGID%",
                      "[Ii]ssues?:?(\s*(,|and)?\s*#\d+)+\n(\d+)",
                      true);
   
   $res = $tester->replaceIDs("BugID: 789, 101112\n".
                              "This is a test message that refers to issue #123 and\n".
                              "issues #456, #654 and #321.\n".
                              "BugID: 789, 101112");

   echo nl2br($res)."<p>";

   $tester->setUpVars("Test: %BUGID%",
                      "http://bugtracker/?id=%BUGID%",
                      "\s*[Cc]ases*\s*[IDs]*\s*[#: ]+((\d+[ ,:;#]*)+)\n(\d+)",
                      true);
   
   $res = $tester->replaceIDs("Cosmetic change\n".
                              "CaseIDs: 48");

   echo nl2br($res)."<p>";
}

// }}}

?>