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