1<?php 2// WebSVN - Subversion repository viewing via the web using PHP 3// Copyright (C) 2004-2006 Tim Armes 4// 5// This program is free software; you can redistribute it and/or modify 6// it under the terms of the GNU General Public License as published by 7// the Free Software Foundation; either version 2 of the License, or 8// (at your option) any later version. 9// 10// This program is distributed in the hope that it will be useful, 11// but WITHOUT ANY WARRANTY; without even the implied warranty of 12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13// GNU General Public License for more details. 14// 15// You should have received a copy of the GNU General Public License 16// along with this program; if not, write to the Free Software 17// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 18// 19// -- 20// 21// svn-look.php 22// 23// Svn bindings 24// 25// These binding currently use the svn command line to achieve their goal. Once a proper 26// SWIG binding has been produced for PHP, there'll be an option to use that instead. 27 28require_once 'include/utils.php'; 29 30// {{{ Classes for retaining log information --- 31 32$debugxml = false; 33 34class SVNInfoEntry { 35 var $rev = 1; 36 var $path = ''; 37 var $isdir = null; 38} 39 40class SVNMod { 41 var $action = ''; 42 var $copyfrom = ''; 43 var $copyrev = ''; 44 var $path = ''; 45 var $isdir = null; 46} 47 48class SVNListEntry { 49 var $rev = 1; 50 var $author = ''; 51 var $date = ''; 52 var $committime; 53 var $age = ''; 54 var $file = ''; 55 var $isdir = null; 56} 57 58class SVNList { 59 var $entries; // Array of entries 60 var $curEntry; // Current entry 61 62 var $path = ''; // The path of the list 63} 64 65class SVNLogEntry { 66 var $rev = 1; 67 var $author = ''; 68 var $date = ''; 69 var $committime; 70 var $age = ''; 71 var $msg = ''; 72 var $path = ''; 73 var $precisePath = ''; 74 75 var $mods; 76 var $curMod; 77} 78 79function SVNLogEntry_compare($a, $b) { 80 return strnatcasecmp($a->path, $b->path); 81} 82 83class SVNLog { 84 var $entries; // Array of entries 85 var $curEntry; // Current entry 86 87 var $path = ''; // Temporary variable used to trace path history 88 89 // findEntry 90 // 91 // Return the entry for a given revision 92 93 function findEntry($rev) { 94 foreach ($this->entries as $index => $entry) { 95 if ($entry->rev == $rev) { 96 return $index; 97 } 98 } 99 } 100} 101 102// }}} 103 104// {{{ XML parsing functions--- 105 106$curTag = ''; 107 108$curInfo = 0; 109 110// {{{ infoStartElement 111 112function infoStartElement($parser, $name, $attrs) { 113 global $curInfo, $curTag, $debugxml; 114 115 switch ($name) { 116 case 'INFO': 117 if ($debugxml) print 'Starting info'."\n"; 118 break; 119 120 case 'ENTRY': 121 if ($debugxml) print 'Creating info entry'."\n"; 122 123 if (count($attrs)) { 124 foreach ($attrs as $k => $v) { 125 switch ($k) { 126 case 'KIND': 127 if ($debugxml) print 'Kind '.$v."\n"; 128 $curInfo->isdir = ($v == 'dir'); 129 break; 130 case 'REVISION': 131 if ($debugxml) print 'Revision '.$v."\n"; 132 $curInfo->rev = $v; 133 break; 134 } 135 } 136 } 137 break; 138 139 default: 140 $curTag = $name; 141 break; 142 } 143} 144 145// }}} 146 147// {{{ infoEndElement 148 149function infoEndElement($parser, $name) { 150 global $curInfo, $debugxml, $curTag; 151 152 switch ($name) { 153 case 'ENTRY': 154 if ($debugxml) print 'Ending info entry'."\n"; 155 if ($curInfo->isdir) { 156 $curInfo->path .= '/'; 157 } 158 break; 159 } 160 161 $curTag = ''; 162} 163 164// }}} 165 166// {{{ infoCharacterData 167 168function infoCharacterData($parser, $data) { 169 global $curInfo, $curTag, $debugxml; 170 171 switch ($curTag) { 172 case 'URL': 173 if ($debugxml) print 'URL: '.$data."\n"; 174 $curInfo->path = $data; 175 break; 176 177 case 'ROOT': 178 if ($debugxml) print 'Root: '.$data."\n"; 179 $curInfo->path = urldecode(substr($curInfo->path, strlen($data))); 180 break; 181 } 182} 183 184// }}} 185 186$curList = 0; 187 188// {{{ listStartElement 189 190function listStartElement($parser, $name, $attrs) { 191 global $curList, $curTag, $debugxml; 192 193 switch ($name) { 194 case 'LIST': 195 if ($debugxml) print 'Starting list'."\n"; 196 197 if (count($attrs)) { 198 foreach ($attrs as $k => $v) { 199 switch ($k) { 200 case 'PATH': 201 if ($debugxml) print 'Path '.$v."\n"; 202 $curList->path = $v; 203 break; 204 } 205 } 206 } 207 break; 208 209 case 'ENTRY': 210 if ($debugxml) print 'Creating new entry'."\n"; 211 $curList->curEntry = new SVNListEntry; 212 213 if (count($attrs)) { 214 foreach ($attrs as $k => $v) { 215 switch ($k) { 216 case 'KIND': 217 if ($debugxml) print 'Kind '.$v."\n"; 218 $curList->curEntry->isdir = ($v == 'dir'); 219 break; 220 } 221 } 222 } 223 break; 224 225 case 'COMMIT': 226 if ($debugxml) print 'Commit'."\n"; 227 228 if (count($attrs)) { 229 foreach ($attrs as $k => $v) { 230 switch ($k) { 231 case 'REVISION': 232 if ($debugxml) print 'Revision '.$v."\n"; 233 $curList->curEntry->rev = $v; 234 break; 235 } 236 } 237 } 238 break; 239 240 default: 241 $curTag = $name; 242 break; 243 } 244} 245 246// }}} 247 248// {{{ listEndElement 249 250function listEndElement($parser, $name) { 251 global $curList, $debugxml, $curTag; 252 253 switch ($name) { 254 case 'ENTRY': 255 if ($debugxml) print 'Ending new list entry'."\n"; 256 if ($curList->curEntry->isdir) { 257 $curList->curEntry->file .= '/'; 258 } 259 $curList->entries[] = $curList->curEntry; 260 $curList->curEntry = null; 261 break; 262 } 263 264 $curTag = ''; 265} 266 267// }}} 268 269// {{{ listCharacterData 270 271function listCharacterData($parser, $data) { 272 global $curList, $curTag, $debugxml; 273 274 switch ($curTag) { 275 case 'NAME': 276 if ($debugxml) print 'Name: '.$data."\n"; 277 if ($data === false || $data === '') return; 278 $curList->curEntry->file .= $data; 279 break; 280 281 case 'AUTHOR': 282 if ($debugxml) print 'Author: '.$data."\n"; 283 if ($data === false || $data === '') return; 284 if (function_exists('mb_detect_encoding') && function_exists('mb_convert_encoding')) 285 $data = mb_convert_encoding($data, 'UTF-8', mb_detect_encoding($data)); 286 $curList->curEntry->author .= $data; 287 break; 288 289 case 'DATE': 290 if ($debugxml) print 'Date: '.$data."\n"; 291 $data = trim($data); 292 if ($data === false || $data === '') return; 293 294 $committime = parseSvnTimestamp($data); 295 $curList->curEntry->committime = $committime; 296 $curList->curEntry->date = strftime('%Y-%m-%d %H:%M:%S', $committime); 297 $curList->curEntry->age = datetimeFormatDuration(max(time() - $committime, 0), true, true); 298 break; 299 } 300} 301 302// }}} 303 304$curLog = 0; 305 306// {{{ logStartElement 307 308function logStartElement($parser, $name, $attrs) { 309 global $curLog, $curTag, $debugxml; 310 311 switch ($name) { 312 case 'LOGENTRY': 313 if ($debugxml) print 'Creating new log entry'."\n"; 314 $curLog->curEntry = new SVNLogEntry; 315 $curLog->curEntry->mods = array(); 316 317 $curLog->curEntry->path = $curLog->path; 318 319 if (count($attrs)) { 320 foreach ($attrs as $k => $v) { 321 switch ($k) { 322 case 'REVISION': 323 if ($debugxml) print 'Revision '.$v."\n"; 324 $curLog->curEntry->rev = $v; 325 break; 326 } 327 } 328 } 329 break; 330 331 case 'PATH': 332 if ($debugxml) print 'Creating new path'."\n"; 333 $curLog->curEntry->curMod = new SVNMod; 334 335 if (count($attrs)) { 336 foreach ($attrs as $k => $v) { 337 switch ($k) { 338 case 'ACTION': 339 if ($debugxml) print 'Action '.$v."\n"; 340 $curLog->curEntry->curMod->action = $v; 341 break; 342 343 case 'COPYFROM-PATH': 344 if ($debugxml) print 'Copy from: '.$v."\n"; 345 $curLog->curEntry->curMod->copyfrom = $v; 346 break; 347 348 case 'COPYFROM-REV': 349 $curLog->curEntry->curMod->copyrev = $v; 350 break; 351 352 case 'KIND': 353 if ($debugxml) print 'Kind '.$v."\n"; 354 $curLog->curEntry->curMod->isdir = ($v == 'dir'); 355 break; 356 } 357 } 358 } 359 360 $curTag = $name; 361 break; 362 363 default: 364 $curTag = $name; 365 break; 366 } 367} 368 369// }}} 370 371// {{{ logEndElement 372 373function logEndElement($parser, $name) { 374 global $curLog, $debugxml, $curTag; 375 376 switch ($name) { 377 case 'LOGENTRY': 378 if ($debugxml) print 'Ending new log entry'."\n"; 379 $curLog->entries[] = $curLog->curEntry; 380 break; 381 382 case 'PATH': 383 // The XML returned when a file is renamed/branched in inconsistent. 384 // In the case of a branch, the path doesn't include the leafname. 385 // In the case of a rename, it does. Ludicrous. 386 387 if (!empty($curLog->path)) { 388 $pos = strrpos($curLog->path, '/'); 389 $curpath = substr($curLog->path, 0, $pos); 390 $leafname = substr($curLog->path, $pos + 1); 391 } else { 392 $curpath = ''; 393 $leafname = ''; 394 } 395 396 $curMod = $curLog->curEntry->curMod; 397 if ($curMod->action == 'A') { 398 if ($debugxml) print 'Examining added path "'.$curMod->copyfrom.'" - Current path = "'.$curpath.'", leafname = "'.$leafname.'"'."\n"; 399 if ($curMod->path == $curLog->path) { 400 // For directories and renames 401 $curLog->path = $curMod->copyfrom; 402 } else if ($curMod->path == $curpath || $curMod->path == $curpath.'/') { 403 // Logs of files that have moved due to branching 404 $curLog->path = $curMod->copyfrom.'/'.$leafname; 405 } else { 406 $curLog->path = str_replace($curMod->path, $curMod->copyfrom, $curLog->path); 407 } 408 if ($debugxml) print 'New path for comparison: "'.$curLog->path.'"'."\n"; 409 } 410 411 if ($debugxml) print 'Ending path'."\n"; 412 $curLog->curEntry->mods[] = $curLog->curEntry->curMod; 413 break; 414 415 case 'MSG': 416 $curLog->curEntry->msg = trim($curLog->curEntry->msg); 417 if ($debugxml) print 'Completed msg = "'.$curLog->curEntry->msg.'"'."\n"; 418 break; 419 } 420 421 $curTag = ''; 422} 423 424// }}} 425 426// {{{ logCharacterData 427 428function logCharacterData($parser, $data) { 429 global $curLog, $curTag, $debugxml; 430 431 switch ($curTag) { 432 case 'AUTHOR': 433 if ($debugxml) print 'Author: '.$data."\n"; 434 if ($data === false || $data === '') return; 435 if (function_exists('mb_detect_encoding') && function_exists('mb_convert_encoding')) 436 $data = mb_convert_encoding($data, 'UTF-8', mb_detect_encoding($data)); 437 $curLog->curEntry->author .= $data; 438 break; 439 440 case 'DATE': 441 if ($debugxml) print 'Date: '.$data."\n"; 442 $data = trim($data); 443 if ($data === false || $data === '') return; 444 445 $committime = parseSvnTimestamp($data); 446 $curLog->curEntry->committime = $committime; 447 $curLog->curEntry->date = strftime('%Y-%m-%d %H:%M:%S', $committime); 448 $curLog->curEntry->age = datetimeFormatDuration(max(time() - $committime, 0), true, true); 449 break; 450 451 case 'MSG': 452 if ($debugxml) print 'Msg: '.$data."\n"; 453 if (function_exists('mb_detect_encoding') && function_exists('mb_convert_encoding')) 454 $data = mb_convert_encoding($data, 'UTF-8', mb_detect_encoding($data)); 455 $curLog->curEntry->msg .= $data; 456 break; 457 458 case 'PATH': 459 if ($debugxml) print 'Path name: '.$data."\n"; 460 $data = trim($data); 461 if ($data === false || $data === '') return; 462 463 $curLog->curEntry->curMod->path .= $data; 464 break; 465 } 466} 467 468// }}} 469 470// }}} 471 472// {{{ internal functions (_topLevel and _listSort) 473 474// Function returns true if the give entry in a directory tree is at the top level 475 476function _topLevel($entry) { 477 // To be at top level, there must be one space before the entry 478 return (strlen($entry) > 1 && $entry[0] == ' ' && $entry[ 1 ] != ' '); 479} 480 481// Function to sort two given directory entries. 482// Directories go at the top if config option alphabetic is not set 483 484function _listSort($e1, $e2) { 485 global $config; 486 487 $file1 = $e1->file; 488 $file2 = $e2->file; 489 $isDir1 = ($file1[strlen($file1) - 1] == '/'); 490 $isDir2 = ($file2[strlen($file2) - 1] == '/'); 491 492 if (!$config->isAlphabeticOrder()) { 493 if ($isDir1 && !$isDir2) return -1; 494 if ($isDir2 && !$isDir1) return 1; 495 } 496 497 if ($isDir1) $file1 = substr($file1, 0, -1); 498 if ($isDir2) $file2 = substr($file2, 0, -1); 499 500 return strnatcasecmp($file1, $file2); 501} 502 503// }}} 504 505// {{{ encodePath 506 507// Function to encode a URL without encoding the /'s 508 509function encodePath($uri) { 510 global $config; 511 512 $uri = str_replace(DIRECTORY_SEPARATOR, '/', $uri); 513 if (function_exists('mb_detect_encoding') && function_exists('mb_convert_encoding')) { 514 $uri = mb_convert_encoding($uri, 'UTF-8', mb_detect_encoding($uri)); 515 } 516 517 $parts = explode('/', $uri); 518 $partscount = count($parts); 519 for ($i = 0; $i < $partscount; $i++) { 520 // do not urlencode the 'svn+ssh://' part! 521 if ($i != 0 || $parts[$i] != 'svn+ssh:') { 522 $parts[$i] = rawurlencode($parts[$i]); 523 } 524 } 525 526 $uri = implode('/', $parts); 527 528 // Quick hack. Subversion seems to have a bug surrounding the use of %3A instead of : 529 530 $uri = str_replace('%3A', ':', $uri); 531 532 // Correct for Window share names 533 if ($config->serverIsWindows) { 534 if (substr($uri, 0, 2) == '//') { 535 $uri = '\\'.substr($uri, 2, strlen($uri)); 536 } 537 538 if (substr($uri, 0, 10) == 'file://///' ) { 539 $uri = 'file:///\\'.substr($uri, 10, strlen($uri)); 540 } 541 } 542 543 return $uri; 544} 545 546// }}} 547 548function _equalPart($str1, $str2) { 549 $len1 = strlen($str1); 550 $len2 = strlen($str2); 551 $i = 0; 552 while ($i < $len1 && $i < $len2) { 553 if (strcmp($str1[$i], $str2[$i]) != 0) { 554 break; 555 } 556 $i++; 557 } 558 if ($i == 0) { 559 return ''; 560 } 561 return substr($str1, 0, $i); 562} 563 564function _logError($string) { 565 $string = preg_replace("/--password '.*'/", "--password '[...]'", $string); 566 error_log($string); 567} 568 569// The SVNRepository class 570 571class SVNRepository { 572 var $repConfig; 573 var $geshi = null; 574 575 function __construct($repConfig) { 576 $this->repConfig = $repConfig; 577 } 578 579 // {{{ highlightLine 580 // 581 // Distill line-spanning syntax highlighting so that each line can stand alone 582 // (when invoking on the first line, $attributes should be an empty array) 583 // Invoked to make sure all open syntax highlighting tags (<font>, <i>, <b>, etc.) 584 // are closed at the end of each line and re-opened on the next line 585 586 function highlightLine($line, &$attributes) { 587 $hline = ''; 588 589 // Apply any highlighting in effect from the previous line 590 foreach ($attributes as $attr) { 591 $hline .= $attr['text']; 592 } 593 594 // append the new line 595 $hline .= $line; 596 597 // update attributes 598 for ($line = strstr($line, '<'); $line; $line = strstr(substr($line, 1), '<')) { 599 if (substr($line, 1, 1) == '/') { 600 // if this closes a tag, remove most recent corresponding opener 601 $tagNamLen = strcspn($line, '> '."\t", 2); 602 $tagNam = substr($line, 2, $tagNamLen); 603 foreach (array_reverse(array_keys($attributes)) as $k) { 604 if ($attributes[$k]['tag'] == $tagNam) { 605 unset($attributes[$k]); 606 break; 607 } 608 } 609 } else { 610 // if this opens a tag, add it to the list 611 $tagNamLen = strcspn($line, '> '."\t", 1); 612 $tagNam = substr($line, 1, $tagNamLen); 613 $tagLen = strcspn($line, '>') + 1; 614 $attributes[] = array('tag' => $tagNam, 'text' => substr($line, 0, $tagLen)); 615 } 616 } 617 618 // close any still-open tags 619 foreach (array_reverse($attributes) as $attr) { 620 $hline .= '</'.$attr['tag'].'>'; 621 } 622 623 // XXX: this just simply replaces [ and ] with their entities to prevent 624 // it from being parsed by the template parser; maybe something more 625 // elegant is in order? 626 $hline = str_replace('[', '[', str_replace(']', ']', $hline) ); 627 return $hline; 628 } 629 630 // }}} 631 632 // Private function to simplify creation of common SVN command string text. 633 function svnCommandString($command, $path, $rev, $peg) { 634 global $config; 635 return $config->getSvnCommand().$this->repConfig->svnCredentials().' '.$command.' '.($rev ? '-r '.$rev.' ' : '').quote(encodePath($this->getSvnPath($path)).'@'.($peg ? $peg : '')); 636 } 637 638 // Private function to simplify creation of enscript command string text. 639 function enscriptCommandString($path) { 640 global $config, $extEnscript; 641 642 $filename = basename($path); 643 $ext = strrchr($path, '.'); 644 645 $lang = false; 646 if (array_key_exists($filename, $extEnscript)) { 647 $lang = $extEnscript[$filename]; 648 } else if ($ext && array_key_exists($ext, $extEnscript)) { 649 $lang = $extEnscript[$ext]; 650 } 651 652 $cmd = $config->enscript.' --language=html'; 653 if ($lang !== false) { 654 $cmd .= ' --color --'.(!$config->getUseEnscriptBefore_1_6_3() ? 'highlight' : 'pretty-print').'='.$lang; 655 } 656 $cmd .= ' -o -'; 657 return $cmd; 658 } 659 660 // {{{ getFileContents 661 // 662 // Dump the content of a file to the given filename 663 664 function getFileContents($path, $filename, $rev = 0, $peg = '', $pipe = '', $highlight = 'file') { 665 global $config; 666 assert ($highlight == 'file' || $highlight == 'no' || $highlight == 'line'); 667 668 $highlighted = false; 669 670 // If there's no filename, just deliver the contents as-is to the user 671 if ($filename == '') { 672 $cmd = $this->svnCommandString('cat', $path, $rev, $peg); 673 passthruCommand($cmd.' '.$pipe); 674 return $highlighted; 675 } 676 677 // Get the file contents info 678 679 $tempname = $filename; 680 if ($highlight == 'line') { 681 $tempname = tempnamWithCheck($config->getTempDir(), ''); 682 } 683 $highlighted = true; 684 $shouldTrimOutput = false; 685 $explodeStr = "\n"; 686 if ($highlight != 'no' && $config->useGeshi && $geshiLang = $this->highlightLanguageUsingGeshi($path)) { 687 $this->applyGeshi($path, $tempname, $geshiLang, $rev, $peg); 688 // Geshi outputs in HTML format, enscript does not 689 $shouldTrimOutput = true; 690 $explodeStr = "<br />"; 691 } else if ($highlight != 'no' && $config->useEnscript) { 692 // Get the files, feed it through enscript, then remove the enscript headers using sed 693 // Note that the sed command returns only the part of the file between <PRE> and </PRE>. 694 // It's complicated because it's designed not to return those lines themselves. 695 $cmd = $this->svnCommandString('cat', $path, $rev, $peg); 696 $cmd = $cmd.' | '.$this->enscriptCommandString($path).' | '. 697 $config->sed.' -n '.$config->quote.'1,/^<PRE.$/!{/^<\\/PRE.$/,/^<PRE.$/!p;}'.$config->quote.' > '.$tempname; 698 } else { 699 $highlighted = false; 700 $cmd = $this->svnCommandString('cat', $path, $rev, $peg); 701 $cmd = $cmd.' > '.quote($filename); 702 } 703 704 if (isset($cmd)) { 705 $error = ''; 706 $output = runCommand($cmd, true, $error); 707 708 if (!empty($error)) { 709 global $lang; 710 _logError($lang['BADCMD'].': '.$cmd); 711 _logError($error); 712 713 global $vars; 714 $vars['warning'] = nl2br(escape(toOutputEncoding($error))); 715 } 716 } 717 718 if ($highlighted && $highlight == 'line') { 719 // If we need each line independently highlighted (e.g. for diff or blame) 720 // then we'll need to filter the output of the highlighter 721 // to make sure tags like <font>, <i> or <b> don't span lines 722 723 $dst = fopen($filename, 'w'); 724 if ($dst) { 725 $content = file_get_contents($tempname); 726 $content = explode($explodeStr, $content); 727 728 // $attributes is used to remember what highlighting attributes 729 // are in effect from one line to the next 730 $attributes = array(); // start with no attributes in effect 731 732 foreach ($content as $line) { 733 if ($shouldTrimOutput) { 734 $line = trim($line); 735 } 736 fputs($dst, $this->highlightLine($line, $attributes)."\n"); 737 } 738 fclose($dst); 739 } 740 } 741 if ($tempname != $filename) { 742 @unlink($tempname); 743 } 744 return $highlighted; 745 } 746 747 // }}} 748 749 // {{{ highlightLanguageUsingGeshi 750 // 751 // check if geshi can highlight the given extension and return the language 752 753 function highlightLanguageUsingGeshi($path) { 754 global $extGeshi; 755 756 $filename = basename($path); 757 $ext = strrchr($path, '.'); 758 if (substr($ext, 0, 1) == '.') $ext = substr($ext, 1); 759 760 foreach ($extGeshi as $language => $extensions) { 761 if (in_array($filename, $extensions) || in_array($ext, $extensions)) { 762 if ($this->geshi === null) { 763 if (!defined('USE_AUTOLOADER')) { 764 require_once 'geshi.php'; 765 } 766 $this->geshi = new GeSHi(); 767 } 768 $this->geshi->set_language($language); 769 if ($this->geshi->error() === false) { 770 return $language; 771 } 772 } 773 } 774 return ''; 775 } 776 777 // }}} 778 779 // {{{ applyGeshi 780 // 781 // perform syntax highlighting using geshi 782 783 function applyGeshi($path, $filename, $language, $rev, $peg = '', $return = false) { 784 // Output the file to the filename 785 $error = ''; 786 $cmd = $this->svnCommandString('cat', $path, $rev, $peg).' > '.quote($filename); 787 $output = runCommand($cmd, true, $error); 788 789 if (!empty($error)) { 790 global $lang; 791 _logError($lang['BADCMD'].': '.$cmd); 792 _logError($error); 793 794 global $vars; 795 $vars['warning'] = 'Unable to cat file: '.nl2br(escape(toOutputEncoding($error))); 796 return; 797 } 798 799 $source = file_get_contents($filename); 800 if ($this->geshi === null) { 801 if (!defined('USE_AUTOLOADER')) { 802 require_once 'geshi.php'; 803 } 804 $this->geshi = new GeSHi(); 805 } 806 $this->geshi->set_source($source); 807 $this->geshi->set_language($language); 808 $this->geshi->set_header_type(GESHI_HEADER_NONE); 809 $this->geshi->set_overall_class('geshi'); 810 $this->geshi->set_tab_width($this->repConfig->getExpandTabsBy()); 811 812 if ($return) { 813 return $this->geshi->parse_code(); 814 } else { 815 $f = @fopen($filename, 'w'); 816 fwrite($f, $this->geshi->parse_code()); 817 fclose($f); 818 } 819 } 820 821 // }}} 822 823 // {{{ listFileContents 824 // 825 // Print the contents of a file without filling up Apache's memory 826 827 function listFileContents($path, $rev = 0, $peg = '') { 828 global $config; 829 830 if ($config->useGeshi && $geshiLang = $this->highlightLanguageUsingGeshi($path)) { 831 $tempname = tempnamWithCheck($config->getTempDir(), 'websvn'); 832 if ($tempname !== false) { 833 print toOutputEncoding($this->applyGeshi($path, $tempname, $geshiLang, $rev, $peg, true)); 834 @unlink($tempname); 835 } 836 } else { 837 $pre = false; 838 $cmd = $this->svnCommandString('cat', $path, $rev, $peg); 839 if ($config->useEnscript) { 840 $cmd .= ' | '.$this->enscriptCommandString($path).' | '. 841 $config->sed.' -n '.$config->quote.'/^<PRE.$/,/^<\\/PRE.$/p'.$config->quote; 842 } else { 843 $pre = true; 844 } 845 846 if ($result = popenCommand($cmd, 'r')) { 847 if ($pre) 848 echo '<pre>'; 849 while (!feof($result)) { 850 $line = fgets($result, 1024); 851 $line = toOutputEncoding($line); 852 if ($pre) { 853 $line = escape($line); 854 } 855 print hardspace($line); 856 } 857 if ($pre) 858 echo '</pre>'; 859 pclose($result); 860 } 861 } 862 } 863 864 // }}} 865 866 // {{{ listReadmeContents 867 // 868 // Parse the README.md file 869 function listReadmeContents($path, $rev = 0, $peg = '') { 870 global $config; 871 872 $file = "README.md"; 873 874 if ($this->isFile($path.$file) != True) 875 { 876 return; 877 } 878 879 if (!$config->getUseParsedown()) 880 { 881 return; 882 } 883 884 // Autoloader handles most of the time 885 if (!defined('USE_AUTOLOADER')) { 886 require_once 'Parsedown.php'; 887 } 888 889 $mdParser = new Parsedown(); 890 $cmd = $this->svnCommandString('cat', $path.$file, $rev, $peg); 891 892 if (!($result = popenCommand($cmd, 'r'))) 893 { 894 return; 895 } 896 897 echo('<div id="wrap">'); 898 while (!feof($result)) 899 { 900 $line = fgets($result, 1024); 901 echo $mdParser->text($line); 902 } 903 echo('</div>'); 904 pclose($result); 905 906 } 907 908 // }}} 909 910 // {{{ getBlameDetails 911 // 912 // Dump the blame content of a file to the given filename 913 914 function getBlameDetails($path, $filename, $rev = 0, $peg = '') { 915 $error = ''; 916 $cmd = $this->svnCommandString('blame', $path, $rev, $peg).' > '.quote($filename); 917 $output = runCommand($cmd, true, $error); 918 919 if (!empty($error)) { 920 global $lang; 921 _logError($lang['BADCMD'].': '.$cmd); 922 _logError($error); 923 924 global $vars; 925 $vars['warning'] = 'No blame info: '.nl2br(escape(toOutputEncoding($error))); 926 } 927 } 928 929 // }}} 930 931 function getProperties($path, $rev = 0, $peg = '') { 932 $cmd = $this->svnCommandString('proplist', $path, $rev, $peg); 933 $ret = runCommand($cmd, true); 934 $properties = array(); 935 if (is_array($ret)) { 936 foreach ($ret as $line) { 937 if (substr($line, 0, 1) == ' ') { 938 $properties[] = ltrim($line); 939 } 940 } 941 } 942 return $properties; 943 } 944 945 // {{{ getProperty 946 947 function getProperty($path, $property, $rev = 0, $peg = '') { 948 $cmd = $this->svnCommandString('propget '.$property, $path, $rev, $peg); 949 $ret = runCommand($cmd, true); 950 // Remove the surplus newline 951 if (count($ret)) { 952 unset($ret[count($ret) - 1]); 953 } 954 return implode("\n", $ret); 955 } 956 957 // }}} 958 959 // {{{ exportDirectory 960 // 961 // Exports the directory to the given location 962 963 function exportRepositoryPath($path, $filename, $rev = 0, $peg = '') { 964 $cmd = $this->svnCommandString('export', $path, $rev, $peg).' '.quote($filename); 965 $retcode = 0; 966 execCommand($cmd, $retcode); 967 if ($retcode != 0) { 968 global $lang; 969 _logError($lang['BADCMD'].': '.$cmd); 970 } 971 return $retcode; 972 } 973 974 // }}} 975 976 // {{{ _xmlParseCmdOutput 977 978 function _xmlParseCmdOutput($cmd, $startElem, $endElem, $charData) { 979 $error = ''; 980 $lines = runCommand($cmd, false, $error); 981 $linesCnt = count($lines); 982 $xml_parser = xml_parser_create('UTF-8'); 983 984 xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, true); 985 xml_set_element_handler($xml_parser, $startElem, $endElem); 986 xml_set_character_data_handler($xml_parser, $charData); 987 988 for ($i = 0; $i < $linesCnt; ++$i) { 989 $line = $lines[$i] . "\n"; 990 $isLast = $i == ($linesCnt - 1); 991 992 if (xml_parse($xml_parser, $line, $isLast)) { 993 continue; 994 } 995 996 $errorMsg = sprintf('XML error: %s (%d) at line %d column %d byte %d'."\n".'cmd: %s', 997 xml_error_string(xml_get_error_code($xml_parser)), 998 xml_get_error_code($xml_parser), 999 xml_get_current_line_number($xml_parser), 1000 xml_get_current_column_number($xml_parser), 1001 xml_get_current_byte_index($xml_parser), 1002 $cmd); 1003 1004 if (xml_get_error_code($xml_parser) == 5) { 1005 break; 1006 } 1007 1008 // errors can contain sensitive info! don't echo this ~J 1009 _logError($errorMsg); 1010 exit; 1011 } 1012 1013 xml_parser_free($xml_parser); 1014 if (empty($error)) { 1015 return; 1016 } 1017 1018 $error = toOutputEncoding(nl2br(str_replace('svn: ', '', $error))); 1019 global $lang; 1020 _logError($lang['BADCMD'].': '.$cmd); 1021 _logError($error); 1022 1023 global $vars; 1024 if (strstr($error, 'found format')) { 1025 $vars['error'] = 'Repository uses a newer format than Subversion '.$config->getSubversionVersion().' can read. ("'.nl2br(escape(toOutputEncoding(substr($error, strrpos($error, 'Expected'))))).'.")'; 1026 } else if (strstr($error, 'No such revision')) { 1027 $vars['warning'] = 'Revision '.escape($rev).' of this resource does not exist.'; 1028 } else { 1029 $vars['error'] = $lang['BADCMD'].': <code>'.escape(stripCredentialsFromCommand($cmd)).'</code><br />'.nl2br(escape(toOutputEncoding($error))); 1030 } 1031 } 1032 1033 // }}} 1034 1035 // {{{ getInfo 1036 1037 function getInfo($path, $rev = 0, $peg = '') { 1038 global $config, $curInfo; 1039 1040 // Since directories returned by svn log don't have trailing slashes (:-(), we need to remove 1041 // the trailing slash from the path for comparison purposes 1042 1043 if ($path[strlen($path) - 1] == '/' && $path != '/') { 1044 $path = substr($path, 0, -1); 1045 } 1046 1047 $curInfo = new SVNInfoEntry; 1048 1049 // Get the svn info 1050 1051 if ($rev == 0) { 1052 $headlog = $this->getLog('/', '', '', true, 1); 1053 if ($headlog && isset($headlog->entries[0])) 1054 $rev = $headlog->entries[0]->rev; 1055 } 1056 1057 $cmd = $this->svnCommandString('info --xml', $path, $rev, $peg); 1058 $this->_xmlParseCmdOutput($cmd, 'infoStartElement', 'infoEndElement', 'infoCharacterData'); 1059 1060 if ($this->repConfig->subpath !== null) { 1061 if (substr($curInfo->path, 0, strlen($this->repConfig->subpath) + 1) === '/'. $this->repConfig->subpath) { 1062 $curInfo->path = substr($curInfo->path, strlen($this->repConfig->subpath) + 1); 1063 } else { 1064 // hide entry when file is outside of subpath 1065 return null; 1066 } 1067 } 1068 1069 return $curInfo; 1070 } 1071 1072 // }}} 1073 1074 // {{{ getList 1075 1076 function getList($path, $rev = 0, $peg = '') { 1077 global $config, $curList; 1078 1079 // Since directories returned by svn log don't have trailing slashes (:-(), we need to remove 1080 // the trailing slash from the path for comparison purposes 1081 1082 if ($path[strlen($path) - 1] == '/' && $path != '/') { 1083 $path = substr($path, 0, -1); 1084 } 1085 1086 $curList = new SVNList; 1087 $curList->entries = array(); 1088 $curList->path = $path; 1089 1090 // Get the list info 1091 1092 if ($rev == 0) { 1093 $headlog = $this->getLog('/', '', '', true, 1); 1094 if ($headlog && isset($headlog->entries[0])) 1095 $rev = $headlog->entries[0]->rev; 1096 } 1097 1098 if ($config->showLoadAllRepos()) { 1099 $cmd = $this->svnCommandString('list -R --xml', $path, $rev, $peg); 1100 $this->_xmlParseCmdOutput($cmd, 'listStartElement', 'listEndElement', 'listCharacterData'); 1101 } 1102 else { 1103 $cmd = $this->svnCommandString('list --xml', $path, $rev, $peg); 1104 $this->_xmlParseCmdOutput($cmd, 'listStartElement', 'listEndElement', 'listCharacterData'); 1105 usort($curList->entries, '_listSort'); 1106 } 1107 1108 return $curList; 1109 } 1110 1111 // }}} 1112 1113 // {{{ getListSearch 1114 1115 function getListSearch($path, $term = '', $rev = 0, $peg = '') { 1116 global $config, $curList; 1117 1118 // Since directories returned by "svn log" don't have trailing slashes (:-(), we need to 1119 // remove the trailing slash from the path for comparison purposes. 1120 if (($path[strlen($path) - 1] == '/') && ($path != '/')) { 1121 $path = substr($path, 0, -1); 1122 } 1123 1124 $curList = new SVNList; 1125 $curList->entries = array(); 1126 $curList->path = $path; 1127 1128 // Get the list info 1129 1130 if ($rev == 0) { 1131 $headlog = $this->getLog('/', '', '', true, 1); 1132 if ($headlog && isset($headlog->entries[0])) 1133 $rev = $headlog->entries[0]->rev; 1134 } 1135 1136 $term = escapeshellarg($term); 1137 $cmd = 'list -R --search ' . $term . ' --xml'; 1138 $cmd = $this->svnCommandString($cmd, $path, $rev, $peg); 1139 $this->_xmlParseCmdOutput($cmd, 'listStartElement', 'listEndElement', 'listCharacterData'); 1140 1141 return $curList; 1142 } 1143 1144 // }}} 1145 1146 1147 // {{{ getLog 1148 1149 function getLog($path, $brev = '', $erev = 1, $quiet = false, $limit = 2, $peg = '', $verbose = false) { 1150 global $config, $curLog; 1151 1152 // Since directories returned by svn log don't have trailing slashes (:-(), 1153 // we must remove the trailing slash from the path for comparison purposes. 1154 if (!empty($path) && $path != '/' && $path[strlen($path) - 1] == '/') { 1155 $path = substr($path, 0, -1); 1156 } 1157 1158 $curLog = new SVNLog; 1159 $curLog->entries = array(); 1160 $curLog->path = $path; 1161 1162 // Get the log info 1163 $effectiveRev = ($brev && $erev ? $brev.':'.$erev : ($brev ? $brev.':1' : '')); 1164 $effectivePeg = ($peg ? $peg : ($brev ? $brev : '')); 1165 $cmd = $this->svnCommandString('log --xml '.($verbose ? '--verbose' : ($quiet ? '--quiet' : '')).($limit != 0 ? ' --limit '.$limit : ''), $path, $effectiveRev, $effectivePeg); 1166 1167 $this->_xmlParseCmdOutput($cmd, 'logStartElement', 'logEndElement', 'logCharacterData'); 1168 1169 foreach ($curLog->entries as $entryKey => $entry) { 1170 $fullModAccess = true; 1171 $anyModAccess = (count($entry->mods) == 0); 1172 $precisePath = null; 1173 foreach ($entry->mods as $modKey => $mod) { 1174 $access = $this->repConfig->hasLogReadAccess($mod->path); 1175 if ($access) { 1176 $anyModAccess = true; 1177 1178 // find path which is parent of all modification but more precise than $curLogEntry->path 1179 $modpath = $mod->path; 1180 if (!$mod->isdir || $mod->action == 'D') { 1181 $pos = strrpos($modpath, '/'); 1182 $modpath = substr($modpath, 0, $pos + 1); 1183 } 1184 if (strlen($modpath) == 0 || substr($modpath, -1) !== '/') { 1185 $modpath .= '/'; 1186 } 1187 //compare with current precise path 1188 if ($precisePath === null) { 1189 $precisePath = $modpath; 1190 } else { 1191 $equalPart = _equalPart($precisePath, $modpath); 1192 if (substr($equalPart, -1) !== '/') { 1193 $pos = strrpos($equalPart, '/'); 1194 $equalPart = substr($equalPart, 0, $pos + 1); 1195 } 1196 $precisePath = $equalPart; 1197 } 1198 1199 // fix paths if command was for a subpath repository 1200 if ($this->repConfig->subpath !== null) { 1201 if (substr($mod->path, 0, strlen($this->repConfig->subpath) + 1) === '/'. $this->repConfig->subpath) { 1202 $curLog->entries[$entryKey]->mods[$modKey]->path = substr($mod->path, strlen($this->repConfig->subpath) + 1); 1203 } else { 1204 // hide modified entry when file is outside of subpath 1205 unset($curLog->entries[$entryKey]->mods[$modKey]); 1206 } 1207 } 1208 } else { 1209 // hide modified entry when access is prohibited 1210 unset($curLog->entries[$entryKey]->mods[$modKey]); 1211 $fullModAccess = false; 1212 } 1213 } 1214 if (!$fullModAccess) { 1215 // hide commit message when access to any of the entries is prohibited 1216 $curLog->entries[$entryKey]->msg = ''; 1217 } 1218 if (!$anyModAccess) { 1219 // hide author and date when access to all of the entries is prohibited 1220 $curLog->entries[$entryKey]->author = ''; 1221 $curLog->entries[$entryKey]->date = ''; 1222 $curLog->entries[$entryKey]->committime = ''; 1223 $curLog->entries[$entryKey]->age = ''; 1224 } 1225 1226 if ($precisePath !== null) { 1227 $curLog->entries[$entryKey]->precisePath = $precisePath; 1228 } else { 1229 $curLog->entries[$entryKey]->precisePath = $curLog->entries[$entryKey]->path; 1230 } 1231 } 1232 return $curLog; 1233 } 1234 1235 // }}} 1236 1237 function isFile($path, $rev = 0, $peg = '') { 1238 $cmd = $this->svnCommandString('info --xml', $path, $rev, $peg); 1239 return strpos(implode(' ', runCommand($cmd, true)), 'kind="file"') !== false; 1240 } 1241 1242 // {{{ getSvnPath 1243 1244 function getSvnPath($path) { 1245 if ($this->repConfig->subpath === null) { 1246 return $this->repConfig->path.$path; 1247 } else { 1248 return $this->repConfig->path.'/'.$this->repConfig->subpath.$path; 1249 } 1250 } 1251 1252 // }}} 1253 1254} 1255 1256// Initialize SVN version information by parsing from command-line output. 1257$cmd = $config->getSvnCommand(); 1258$cmd = str_replace(array('--non-interactive', '--trust-server-cert'), array('', ''), $cmd); 1259$cmd .= ' --version -q'; 1260$ret = runCommand($cmd, false); 1261if (preg_match('~([0-9]+)\.([0-9]+)\.([0-9]+)~', $ret[0], $matches)) { 1262 $config->setSubversionVersion($matches[0]); 1263 $config->setSubversionMajorVersion($matches[1]); 1264 $config->setSubversionMinorVersion($matches[2]); 1265} 1266