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('[', '&#91;', str_replace(']', '&#93;', $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