Rev Author Line No. Line
185 miho 1 <?php
2 # vim:et:ts=3:sts=3:sw=3:fdm=marker:
3  
4 // WebSVN - Subversion repository viewing via the web using PHP
5 // Copyright © 2004-2006 Tim Armes, Matt Sicker
6 //
7 // This program is free software; you can redistribute it and/or modify
8 // it under the terms of the GNU General Public License as published by
9 // the Free Software Foundation; either version 2 of the License, or
10 // (at your option) any later version.
11 //
12 // This program is distributed in the hope that it will be useful,
13 // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 // GNU General Public License for more details.
16 //
17 // You should have received a copy of the GNU General Public License
18 // along with this program; if not, write to the Free Software
19 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 //
21 // --
22 //
23 // svn-look.inc
24 //
25 // Svn bindings
26 //
27 // These binding currently use the svn command line to achieve their goal. Once a proper
28 // SWIG binding has been produced for PHP, there'll be an option to use that instead.
29  
30 require_once("include/utils.inc");
31  
32 // {{{ Classes for retaining log information ---
33  
34 $debugxml = false;
35  
36 Class SVNMod
37 {
38 var $action = '';
39 var $copyfrom = '';
40 var $copyrev = '';
41 var $path = '';
42 }
43  
44 Class SVNLogEntry
45 {
46 var $rev = 1;
47 var $author = '';
48 var $date = '';
49 var $committime;
50 var $age = '';
51 var $msg = '';
52 var $path = '';
53  
54 var $mods;
55 var $curMod;
56 }
57  
58 Class SVNLog
59 {
60 var $entries; // Array of entries
61 var $curEntry; // Current entry
62  
63 var $path = ''; // Temporary variable used to trace path history
64  
65 // findEntry
66 //
67 // Return the entry for a given revision
68  
69 function findEntry($rev)
70 {
71 foreach ($this->entries as $index => $entry)
72 {
73 if ($entry->rev == $rev)
74 return $index;
75  
76 }
77 }
78 }
79  
80 // }}}
81  
82 // {{{ XML parsing functions---
83  
84 $curLog = 0;
85 $curTag = '';
86  
87 // {{{ startElement
88  
89 function startElement($parser, $name, $attrs)
90 {
91 global $curLog, $curTag, $debugxml;
92  
93 switch ($name)
94 {
95 case "LOGENTRY":
96 if ($debugxml) print "Creating new log entry\n";
97 $curLog->curEntry = new SVNLogEntry;
98 $curLog->curEntry->mods = array();
99  
100 $curLog->curEntry->path = $curLog->path;
101  
102 if (sizeof($attrs))
103 {
104 while (list($k, $v) = each($attrs))
105 {
106 switch ($k)
107 {
108 case "REVISION":
109 if ($debugxml) print "Revision $v\n";
110 $curLog->curEntry->rev = $v;
111 break;
112 }
113 }
114 }
115 break;
116  
117 case "PATH":
118 if ($debugxml) print "Creating new path\n";
119 $curLog->curEntry->curMod = new SVNMod;
120  
121 if (sizeof($attrs))
122 {
123 while (list($k, $v) = each($attrs))
124 {
125 switch ($k)
126 {
127 case "ACTION":
128 if ($debugxml) print "Action $v\n";
129 $curLog->curEntry->curMod->action = $v;
130 break;
131  
132 case "COPYFROM-PATH":
133 if ($debugxml) print "Copy from: $v\n";
134 $curLog->curEntry->curMod->copyfrom = $v;
135 break;
136  
137 case "COPYFROM-REV":
138 $curLog->curEntry->curMod->copyrev = $v;
139 break;
140 }
141 }
142 }
143  
144 $curTag = $name;
145 break;
146  
147 default:
148 $curTag = $name;
149 break;
150 }
151 }
152  
153 // }}}
154  
155 // {{{ endElement
156  
157 function endElement($parser, $name)
158 {
159 global $curLog, $debugxml, $curTag;
160  
161 switch ($name)
162 {
163 case "LOGENTRY":
164 if ($debugxml) print "Ending new log entry\n";
165 $curLog->entries[] = $curLog->curEntry;
166 break;
167  
168 case "PATH":
169 if ($debugxml) print "Ending path\n";
170 $curLog->curEntry->mods[] = $curLog->curEntry->curMod;
171 break;
172  
173 case "MSG":
174 $curLog->curEntry->msg = trim($curLog->curEntry->msg);
175 if ($debugxml) print "Completed msg = '".$curLog->curEntry->msg."'\n";
176 break;
177 }
178  
179 $curTag = "";
180 }
181  
182 // }}}
183  
184 // {{{ characterData
185  
186 function characterData($parser, $data)
187 {
188 global $curLog, $curTag, $lang, $debugxml;
189  
190 switch ($curTag)
191 {
192 case "AUTHOR":
193 if ($debugxml) print "Author: $data\n";
194 if (empty($data)) return;
195 $curLog->curEntry->author .= htmlentities($data, ENT_COMPAT, "UTF-8");
196 break;
197  
198 case "DATE":
199 if ($debugxml) print "Date: $data\n";
200 $data = trim($data);
201 if (empty($data)) return;
202  
203 sscanf($data, "%d-%d-%dT%d:%d:%d.", $y, $mo, $d, $h, $m, $s);
204  
205 $mo = substr("00".$mo, -2);
206 $d = substr("00".$d, -2);
207 $h = substr("00".$h, -2);
208 $m = substr("00".$m, -2);
209 $s = substr("00".$s, -2);
210  
211 $curLog->curEntry->date = "$y-$mo-$d $h:$m:$s GMT";
212  
213 $committime = strtotime($curLog->curEntry->date);
214 $curLog->curEntry->committime = $committime;
215 $curtime = time();
216  
217 // Get the number of seconds since the commit
218 $agesecs = $curtime - $committime;
219 if ($agesecs < 0) $agesecs = 0;
220  
221 $curLog->curEntry->age = datetimeFormatDuration($agesecs, true, true);
222  
223 break;
224  
225 case "MSG":
226 if ($debugxml) print "Msg: '$data'\n";
227 $curLog->curEntry->msg .= htmlentities($data, ENT_COMPAT, "UTF-8");
228 break;
229  
230 case "PATH":
231 if ($debugxml) print "Path name: '$data'\n";
232 $data = trim($data);
233 if (empty($data)) return;
234  
235 $curLog->curEntry->curMod->path .= $data;
236  
237 // The XML returned when a file is renamed/branched in inconsistant. In the case
238 // of a branch, the path information doesn't include the leafname. In the case of
239 // a rename, it does. Ludicrous.
240  
241 if (!empty($curLog->path))
242 {
243 $pos = strrpos($curLog->path, "/");
244 $curpath = substr($curLog->path, 0, $pos);
245 $leafname = substr($curLog->path, $pos + 1);
246 }
247 else
248 {
249 $curpath = "";
250 $leafname = "";
251 }
252  
253 if ($curLog->curEntry->curMod->action == "A")
254 {
255 if ($debugxml) print "Examining added path '".$curLog->curEntry->curMod->copyfrom."' - Current path = '$curpath', leafname = '$leafname'\n";
256 if ($data == $curLog->path) // For directories and renames
257 {
258 if ($debugxml) print "New path for comparison: '".$curLog->curEntry->curMod->copyfrom."'\n";
259 $curLog->path = $curLog->curEntry->curMod->copyfrom;
260 }
261 else if ($data == $curpath || $data == $curpath."/") // Logs of files that have moved due to branching
262 {
263 if ($debugxml) print "New path for comparison: '".$curLog->curEntry->curMod->copyfrom."/$leafname'\n";
264 $curLog->path = $curLog->curEntry->curMod->copyfrom."/$leafname";
265 }
266 }
267 break;
268 }
269 }
270  
271 // }}}
272  
273 // }}}
274  
275 // Function returns true if the give entry in a directory tree is at the top level
276  
277 function _topLevel($entry)
278 {
279 // To be at top level, there must be one space before the entry
280 return (strlen($entry) > 1 && $entry{0} == " " && $entry{1} != " ");
281 }
282  
283 // Function to sort two given directory entries. Directories go at the top
284  
285 function _dirSort($e1, $e2)
286 {
287 $isDir1 = $e1{strlen($e1) - 1} == "/";
288 $isDir2 = $e2{strlen($e2) - 1} == "/";
289  
290 if ($isDir1 && !$isDir2) return -1;
291 if ($isDir2 && !$isDir1) return 1;
292  
293 return strnatcasecmp($e1, $e2);
294 }
295  
296 // Return the revision string to pass to a command
297  
298 function _revStr($rev)
299 {
300 if ($rev > 0)
301 return "-r $rev";
302 else
303 return "";
304 }
305  
306 // {{{ encodePath
307  
308 // Function to encode a URL without encoding the /'s
309  
310 function encodePath($uri)
311 {
312 global $config;
313  
314 $uri = str_replace(DIRECTORY_SEPARATOR, "/", $uri);
315  
316 $parts = explode('/', $uri);
317 for ($i = 0; $i < count($parts); $i++)
318 {
319 if ( function_exists("mb_detect_encoding") && function_exists("mb_convert_encoding"))
320 {
321 $parts[$i] = mb_convert_encoding($parts[$i], "UTF-8", mb_detect_encoding($parts[$i]));
322 }
323  
324 $parts[$i] = rawurlencode($parts[$i]);
325 }
326  
327 $uri = implode('/', $parts);
328  
329 // Quick hack. Subversion seems to have a bug surrounding the use of %3A instead of :
330  
331 $uri = str_replace("%3A" ,":", $uri);
332  
333 // Correct for Window share names
334 if ( $config->serverIsWindows==true )
335 {
336 if ( substr($uri, 0,2)=="//" )
337 $uri="\\".substr($uri, 2, strlen($uri));
338 }
339  
340 return $uri;
341 }
342  
343 // }}}
344  
345 // The SVNRepository Class
346  
347 Class SVNRepository
348 {
349 var $repConfig;
350  
351 function SVNRepository($repConfig)
352 {
353 $this->repConfig = $repConfig;
354 }
355  
356 // {{{ dirContents
357  
358 function dirContents($path, $rev = 0)
359 {
360 global $config, $locwebsvnreal;
361  
362 $revstr = _revStr($rev);
363  
364 $tree = array();
365  
366 if ($rev == 0)
367 {
368 $headlog = $this->getLog("/", "", "", true, 1);
369 $rev = $headlog->entries[0]->rev;
370 }
371  
372 $path = encodepath($this->repConfig->path.$path);
373 $output = runCommand($config->svn." list $revstr ".$this->repConfig->svnParams().quote($path), true);
374  
375 foreach ($output as $entry)
376 {
377 if ($entry != "")
378 $tree[] = $entry;
379 }
380  
381 // Sort the entries into alphabetical order with the directories at the top of the list
382 usort($tree, "_dirSort");
383  
384 return $tree;
385 }
386  
387 // }}}
388  
389 // {{{ highlightLine
390 //
391 // Distill line-spanning syntax highlighting so that each line can stand alone
392 // (when invoking on the first line, $attributes should be an empty array)
393 // Invoked to make sure all open syntax highlighting tags (<font>, <i>, <b>, etc.)
394 // are closed at the end of each line and re-opened on the next line
395  
396 function highlightLine($line, &$attributes)
397 {
398 $hline = "";
399  
400 // Apply any highlighting in effect from the previous line
401 foreach($attributes as $attr)
402 {
403 $hline.=$attr['text'];
404 }
405  
406 // append the new line
407 $hline.=$line;
408  
409 // update attributes
410 for ($line = strstr($line, "<"); $line; $line = strstr(substr($line,1), "<"))
411 {
412 // if this closes a tag, remove most recent corresponding opener
413 if (substr($line,1,1) == "/")
414 {
415 $tagNamLen = strcspn($line, "> \t", 2);
416 $tagNam = substr($line,2,$tagNamLen);
417 foreach(array_reverse(array_keys($attributes)) as $k)
418 {
419 if ($attributes[$k]['tag'] == $tagNam)
420 {
421 unset($attributes[$k]);
422 break;
423 }
424 }
425 }
426 else
427 // if this opens a tag, add it to the list
428 {
429 $tagNamLen = strcspn($line, "> \t", 1);
430 $tagNam = substr($line,1,$tagNamLen);
431 $tagLen = strcspn($line, ">") + 1;
432 $attributes[] = array('tag' => $tagNam, 'text' => substr($line,0,$tagLen));
433 }
434 }
435  
436 // close any still-open tags
437 foreach(array_reverse($attributes) as $attr)
438 {
439 $hline.="</".$attr['tag'].">";
440 }
441  
442 return($hline);
443 }
444  
445 // }}}
446  
447 // {{{ getFileContents
448 //
449 // Dump the content of a file to the given filename
450  
451 function getFileContents($path, $filename, $rev = 0, $pipe = "", $perLineHighlighting = false)
452 {
453 global $config, $extEnscript;
454  
455 $revstr = _revStr($rev);
456  
457 // If there's no filename, we'll just deliver the contents as it is to the user
458 if ($filename == "")
459 {
460 $path = encodepath($this->repConfig->path.$path);
461 passthru(quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." $pipe", false));
462 return;
463 }
464  
465 // Get the file contents info
466  
467 $ext = strrchr($path, ".");
468 $l = @$extEnscript[$ext];
469  
470 if ($l == "php")
471 {
472 // Output the file to the filename
473 $path = encodepath($this->repConfig->path.$path);
474 $cmd = quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." > $filename", false);
475 @exec($cmd);
476  
477 // Get the file as a string (memory hogging, but we have no other options)
478 $content = highlight_file($filename, true);
479  
480 // Destroy the previous version, and replace it with the highlighted version
481 $f = fopen($filename, "w");
482 if ($f)
483 {
484 // The highlight file function doesn't deal with line endings very nicely at all. We'll have to do it
485 // by hand.
486  
487 // Remove the first line generated by highlight()
488 $pos = strpos($content, "\n");
489 $content = substr($content, $pos+1);
490  
491 $content = explode("<br />", $content);
492  
493 if ($perLineHighlighting)
494 {
495 // If we need each line independently highlighted (e.g. for diff or blame)
496 // hen we'll need to filter the output of the highlighter
497 // to make sure tags like <font>, <i> or <b> don't span lines
498  
499 // $attributes is used to remember what highlighting attributes
500 // are in effect from one line to the next
501 $attributes = array(); // start with no attributes in effect
502  
503 foreach ($content as $line)
504 {
505 fputs($f, $this->highlightLine(rtrim($line),$attributes)."\n");
506 }
507 }
508 else
509 {
510 foreach ($content as $line)
511 {
512 fputs($f, rtrim($line)."\n");
513 }
514 }
515  
516 fclose($f);
517 }
518 }
519 else
520 {
521 if ($config->useEnscript)
522 {
523 // Get the files, feed it through enscript, then remove the enscript headers using sed
524 //
525 // Note that the sec command returns only the part of the file between <PRE> and </PRE>.
526 // It's complicated because it's designed not to return those lines themselves.
527  
528 $path = encodepath($this->repConfig->path.$path);
529 $cmd = quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." | ".
530 $config->enscript." --language=html ".
531 ($l ? "--color --pretty-print=$l" : "")." -o - | ".
532 $config->sed." -n ".$config->quote."1,/^<PRE.$/!{/^<\\/PRE.$/,/^<PRE.$/!p;}".$config->quote." > $filename", false);
533 @exec($cmd);
534 }
535 else
536 {
537 $path = encodepath(str_replace(DIRECTORY_SEPARATOR, "/", $this->repConfig->path.$path));
538 $cmd = quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." > $filename", false);
539 @exec($cmd);
540 }
541 }
542 }
543  
544 // }}}
545  
546 // {{{ listFileContents
547 //
548 // Print the contents of a file without filling up Apache's memory
549  
550 function listFileContents($path, $rev = 0)
551 {
552 global $config, $extEnscript;
553  
554 $revstr = _revStr($rev);
555 $pre = false;
556  
557 // Get the file contents info
558  
559 $ext = strrchr($path, ".");
560 $l = @$extEnscript[$ext];
561  
562 // Deal with php highlighting internally
563 if ($l == "php")
564 {
565 $tmp = tempnam("temp", "wsvn");
566  
567 // Output the file to a temporary file
568 $path = encodepath($this->repConfig->path.$path);
569 $cmd = quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." > $tmp", false);
570 @exec($cmd);
571 highlight_file($tmp);
572 unlink($tmp);
573 }
574 else
575 {
576 if ($config->useEnscript)
577 {
578 $path = encodepath($this->repConfig->path.$path);
579 $cmd = quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." | ".
580 $config->enscript." --language=html ".
581 ($l ? "--color --pretty-print=$l" : "")." -o - | ".
582 $config->sed." -n ".$config->quote."/^<PRE.$/,/^<\\/PRE.$/p".$config->quote." 2>&1", false);
583  
584 if (!($result = popen($cmd, "r")))
585 return;
586 }
587 else
588 {
589 $path = encodepath($this->repConfig->path.$path);
590 $cmd = quoteCommand($config->svn." cat $revstr ".$this->repConfig->svnParams().quote($path)." 2>&1", false);
591  
592 if (!($result = popen($cmd, "r")))
593 return;
594  
595 $pre = true;
596 }
597  
598 if ($pre)
599 echo "<PRE>";
600  
601 while (!feof($result))
602 {
603 $line = fgets($result, 1024);
604 if ($pre) $line = replaceEntities($line, $this->repConfig);
605  
606 print hardspace($line);
607 }
608  
609 if ($pre)
610 echo "</PRE>";
611  
612 pclose($result);
613 }
614 }
615  
616 // }}}
617  
618 // {{{ getBlameDetails
619 //
620 // Dump the blame content of a file to the given filename
621  
622 function getBlameDetails($path, $filename, $rev = 0)
623 {
624 global $config;
625  
626 $revstr = _revStr($rev);
627  
628 $path = encodepath($this->repConfig->path.$path);
629 $cmd = quoteCommand($config->svn." blame $revstr ".$this->repConfig->svnParams().quote($path)." > $filename", false);
630  
631 @exec($cmd);
632 }
633  
634 // }}}
635  
636 // {{{ getProperty
637  
638 function getProperty($path, $property, $rev = 0)
639 {
640 global $config;
641  
642 $revstr = _revStr($rev);
643  
644 $path = encodepath($this->repConfig->path.$path);
645 $ret = runCommand($config->svn." propget $property $revstr ".$this->repConfig->svnParams().quote($path), true);
646  
647 // Remove the surplus newline
648 if (count($ret))
649 unset($ret[count($ret) - 1]);
650  
651 return implode("\n", $ret);
652 }
653  
654 // }}}
655  
656 // {{{ exportDirectory
657 //
658 // Exports the directory to the given location
659  
660 function exportDirectory($path, $filename, $rev = 0)
661 {
662 global $config;
663  
664 $revstr = _revStr($rev);
665  
666 $path = encodepath($this->repConfig->path.$path);
667 $cmd = quoteCommand($config->svn." export $revstr ".$this->repConfig->svnParams().quote($path)." ".quote($filename), false);
668  
669 @exec($cmd);
670 }
671  
672 // }}}
673  
674 // {{{ getLog
675  
676 function getLog($path, $brev = "", $erev = 1, $quiet = false, $limit = 2)
677 {
678 global $config, $curLog, $vars, $lang;
679  
680 $xml_parser = xml_parser_create("UTF-8");
681 xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, true);
682 xml_set_element_handler($xml_parser, "startElement", "endElement");
683 xml_set_character_data_handler($xml_parser, "characterData");
684  
685 // Since directories returned by svn log don't have trailing slashes (:-(), we need to remove
686 // the trailing slash from the path for comparison purposes
687  
688 if ($path{strlen($path) - 1} == "/" && $path != "/")
689 $path = substr($path, 0, -1);
690  
691 $curLog = new SVNLog;
692 $curLog->entries = array();
693 $curLog->path = $path;
694  
695 $revStr = "";
696  
697 if ($brev && $erev)
698 $revStr = "-r$brev:$erev";
699 else if ($brev)
700 $revStr = "-r$brev:1";
701  
702 if (($config->subversionMajorVersion > 1 || $config->subversionMinorVersion >=2) && $limit != 0)
703 $revStr .= " --limit $limit";
704  
705 // Get the log info
706 $path = encodepath($this->repConfig->path.$path);
707 $info = "--verbose";
708 if ($quiet)
709 $info = "--quiet";
710  
711 $cmd = quoteCommand($config->svn." log --xml $info $revStr ".$this->repConfig->svnParams().quote($path), false);
712  
713 if ($handle = popen($cmd, "r"))
714 {
715 $firstline = true;
716 while (!feof($handle))
717 {
718 $line = fgets($handle);
719 if (!xml_parse($xml_parser, $line, feof($handle)))
720 {
721 if (xml_get_error_code($xml_parser) != 5)
722 {
723 die(sprintf("XML error: %s (%d) at line %d column %d byte %d<br>cmd: %s<nr>",
724 xml_error_string(xml_get_error_code($xml_parser)),
725 xml_get_error_code($xml_parser),
726 xml_get_current_line_number($xml_parser),
727 xml_get_current_column_number($xml_parser),
728 xml_get_current_byte_index($xml_parser),
729 $cmd));
730 }
731 else
732 {
733 $vars["error"] = $lang["UNKNOWNREVISION"];
734 return 0;
735 }
736 }
737 }
738 pclose($handle);
739 }
740  
741 xml_parser_free($xml_parser);
742 return $curLog;
743 }
744  
745 // }}}
746  
747 }
748  
749 // {{{ initSvnVersion
750  
751 function initSvnVersion(&$major, &$minor)
752 {
753 global $config;
754  
755 $ret = runCommand($config->svn_noparams." --version", false);
756  
757 if (preg_match("~([0-9]?)\.([0-9]?)\.([0-9]?)~",$ret[0],$matches))
758 {
759 $major = $matches[1];
760 $minor = $matches[2];
761 }
762  
763 $config->setSubversionMajorVersion($major);
764 $config->setSubversionMinorVersion($minor);
765 }
766  
767 // }}}
768  
769 ?>