1 | 1 | simandl | <?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 SVNMod { |
35 | | | var $action = ''; |
36 | | | var $copyfrom = ''; |
37 | | | var $copyrev = ''; |
38 | | | var $path = ''; |
39 | | | } |
40 | | | |
41 | | | class SVNListEntry { |
42 | | | var $rev = 1; |
43 | | | var $author = ''; |
44 | | | var $date = ''; |
45 | | | var $committime; |
46 | | | var $age = ''; |
47 | | | var $file = ''; |
48 | | | var $isdir = false; |
49 | | | } |
50 | | | |
51 | | | class SVNList { |
52 | | | var $entries; // Array of entries |
53 | | | var $curEntry; // Current entry |
54 | | | |
55 | | | var $path = ''; // The path of the list |
56 | | | } |
57 | | | |
58 | | | class SVNLogEntry { |
59 | | | var $rev = 1; |
60 | | | var $author = ''; |
61 | | | var $date = ''; |
62 | | | var $committime; |
63 | | | var $age = ''; |
64 | | | var $msg = ''; |
65 | | | var $path = ''; |
66 | | | |
67 | | | var $mods; |
68 | | | var $curMod; |
69 | | | } |
70 | | | |
71 | | | function SVNLogEntry_compare($a, $b) { |
72 | | | return strnatcasecmp($a->path, $b->path); |
73 | | | } |
74 | | | |
75 | | | class SVNLog { |
76 | | | var $entries; // Array of entries |
77 | | | var $curEntry; // Current entry |
78 | | | |
79 | | | var $path = ''; // Temporary variable used to trace path history |
80 | | | |
81 | | | // findEntry |
82 | | | // |
83 | | | // Return the entry for a given revision |
84 | | | |
85 | | | function findEntry($rev) { |
86 | | | foreach ($this->entries as $index => $entry) { |
87 | | | if ($entry->rev == $rev) { |
88 | | | return $index; |
89 | | | } |
90 | | | } |
91 | | | } |
92 | | | } |
93 | | | |
94 | | | // }}} |
95 | | | |
96 | | | // {{{ XML parsing functions--- |
97 | | | |
98 | | | $curTag = ''; |
99 | | | |
100 | | | $curList = 0; |
101 | | | |
102 | | | // {{{ listStartElement |
103 | | | |
104 | | | function listStartElement($parser, $name, $attrs) { |
105 | | | global $curList, $curTag, $debugxml; |
106 | | | |
107 | | | switch ($name) { |
108 | | | case "LIST": |
109 | | | if ($debugxml) print "Starting list\n"; |
110 | | | |
111 | | | if (sizeof($attrs)) { |
112 | | | while (list($k, $v) = each($attrs)) { |
113 | | | switch ($k) { |
114 | | | case "PATH": |
115 | | | if ($debugxml) print "Path $v\n"; |
116 | | | $curList->path = $v; |
117 | | | break; |
118 | | | } |
119 | | | } |
120 | | | } |
121 | | | break; |
122 | | | |
123 | | | case "ENTRY": |
124 | | | if ($debugxml) print "Creating new entry\n"; |
125 | | | $curList->curEntry = new SVNListEntry; |
126 | | | |
127 | | | if (sizeof($attrs)) { |
128 | | | while (list($k, $v) = each($attrs)) { |
129 | | | switch ($k) { |
130 | | | case "KIND": |
131 | | | if ($debugxml) print "Kind $v\n"; |
132 | | | $curList->curEntry->isdir = ($v == 'dir'); |
133 | | | break; |
134 | | | } |
135 | | | } |
136 | | | } |
137 | | | break; |
138 | | | |
139 | | | case "COMMIT": |
140 | | | if ($debugxml) print "Commit\n"; |
141 | | | |
142 | | | if (sizeof($attrs)) { |
143 | | | while (list($k, $v) = each($attrs)) { |
144 | | | switch ($k) { |
145 | | | case "REVISION": |
146 | | | if ($debugxml) print "Revision $v\n"; |
147 | | | $curList->curEntry->rev = $v; |
148 | | | break; |
149 | | | } |
150 | | | } |
151 | | | } |
152 | | | break; |
153 | | | |
154 | | | default: |
155 | | | $curTag = $name; |
156 | | | break; |
157 | | | } |
158 | | | } |
159 | | | |
160 | | | // }}} |
161 | | | |
162 | | | // {{{ listEndElement |
163 | | | |
164 | | | function listEndElement($parser, $name) { |
165 | | | global $curList, $debugxml, $curTag; |
166 | | | |
167 | | | switch ($name) { |
168 | | | case "ENTRY": |
169 | | | if ($debugxml) print "Ending new list entry\n"; |
170 | | | if ($curList->curEntry->isdir) { |
171 | | | $curList->curEntry->file .= '/'; |
172 | | | } |
173 | | | $curList->entries[] = $curList->curEntry; |
174 | | | $curList->curEntry = null; |
175 | | | break; |
176 | | | } |
177 | | | |
178 | | | $curTag = ""; |
179 | | | } |
180 | | | |
181 | | | // }}} |
182 | | | |
183 | | | // {{{ listCharacterData |
184 | | | |
185 | | | function listCharacterData($parser, $data) { |
186 | | | global $curList, $curTag, $lang, $debugxml; |
187 | | | |
188 | | | switch ($curTag) { |
189 | | | case "NAME": |
190 | | | if ($debugxml) print "Name: $data\n"; |
191 | | | if (empty($data)) return; |
192 | | | $curList->curEntry->file .= $data; |
193 | | | break; |
194 | | | |
195 | | | case "AUTHOR": |
196 | | | if ($debugxml) print "Author: $data\n"; |
197 | | | if (empty($data)) return; |
198 | | | $curList->curEntry->author .= htmlentities($data, ENT_COMPAT, "UTF-8"); |
199 | | | break; |
200 | | | |
201 | | | case "DATE": |
202 | | | if ($debugxml) print "Date: $data\n"; |
203 | | | $data = trim($data); |
204 | | | if (empty($data)) return; |
205 | | | |
206 | | | sscanf($data, "%d-%d-%dT%d:%d:%d.", $y, $mo, $d, $h, $m, $s); |
207 | | | |
208 | | | $mo = substr("00".$mo, -2); |
209 | | | $d = substr("00".$d, -2); |
210 | | | $h = substr("00".$h, -2); |
211 | | | $m = substr("00".$m, -2); |
212 | | | $s = substr("00".$s, -2); |
213 | | | |
214 | | | $curList->curEntry->date = "$y-$mo-$d $h:$m:$s GMT"; |
215 | | | |
216 | | | $committime = strtotime($curList->curEntry->date); |
217 | | | $curList->curEntry->committime = $committime; |
218 | | | $curtime = time(); |
219 | | | |
220 | | | // Get the number of seconds since the commit |
221 | | | $agesecs = $curtime - $committime; |
222 | | | if ($agesecs < 0) $agesecs = 0; |
223 | | | |
224 | | | $curList->curEntry->age = datetimeFormatDuration($agesecs, true, true); |
225 | | | |
226 | | | break; |
227 | | | } |
228 | | | } |
229 | | | |
230 | | | // }}} |
231 | | | |
232 | | | $curLog = 0; |
233 | | | |
234 | | | // {{{ logStartElement |
235 | | | |
236 | | | function logStartElement($parser, $name, $attrs) { |
237 | | | global $curLog, $curTag, $debugxml; |
238 | | | |
239 | | | switch ($name) { |
240 | | | case "LOGENTRY": |
241 | | | if ($debugxml) print "Creating new log entry\n"; |
242 | | | $curLog->curEntry = new SVNLogEntry; |
243 | | | $curLog->curEntry->mods = array(); |
244 | | | |
245 | | | $curLog->curEntry->path = $curLog->path; |
246 | | | |
247 | | | if (sizeof($attrs)) { |
248 | | | while (list($k, $v) = each($attrs)) { |
249 | | | switch ($k) { |
250 | | | case "REVISION": |
251 | | | if ($debugxml) print "Revision $v\n"; |
252 | | | $curLog->curEntry->rev = $v; |
253 | | | break; |
254 | | | } |
255 | | | } |
256 | | | } |
257 | | | break; |
258 | | | |
259 | | | case "PATH": |
260 | | | if ($debugxml) print "Creating new path\n"; |
261 | | | $curLog->curEntry->curMod = new SVNMod; |
262 | | | |
263 | | | if (sizeof($attrs)) { |
264 | | | while (list($k, $v) = each($attrs)) { |
265 | | | switch ($k) { |
266 | | | case "ACTION": |
267 | | | if ($debugxml) print "Action $v\n"; |
268 | | | $curLog->curEntry->curMod->action = $v; |
269 | | | break; |
270 | | | |
271 | | | case "COPYFROM-PATH": |
272 | | | if ($debugxml) print "Copy from: $v\n"; |
273 | | | $curLog->curEntry->curMod->copyfrom = $v; |
274 | | | break; |
275 | | | |
276 | | | case "COPYFROM-REV": |
277 | | | $curLog->curEntry->curMod->copyrev = $v; |
278 | | | break; |
279 | | | } |
280 | | | } |
281 | | | } |
282 | | | |
283 | | | $curTag = $name; |
284 | | | break; |
285 | | | |
286 | | | default: |
287 | | | $curTag = $name; |
288 | | | break; |
289 | | | } |
290 | | | } |
291 | | | |
292 | | | // }}} |
293 | | | |
294 | | | // {{{ logEndElement |
295 | | | |
296 | | | function logEndElement($parser, $name) { |
297 | | | global $curLog, $debugxml, $curTag; |
298 | | | |
299 | | | switch ($name) { |
300 | | | case "LOGENTRY": |
301 | | | if ($debugxml) print "Ending new log entry\n"; |
302 | | | $curLog->entries[] = $curLog->curEntry; |
303 | | | break; |
304 | | | |
305 | | | case "PATH": |
306 | | | if ($debugxml) print "Ending path\n"; |
307 | | | $curLog->curEntry->mods[] = $curLog->curEntry->curMod; |
308 | | | break; |
309 | | | |
310 | | | case "MSG": |
311 | | | $curLog->curEntry->msg = trim($curLog->curEntry->msg); |
312 | | | if ($debugxml) print "Completed msg = '".$curLog->curEntry->msg."'\n"; |
313 | | | break; |
314 | | | } |
315 | | | |
316 | | | $curTag = ""; |
317 | | | } |
318 | | | |
319 | | | // }}} |
320 | | | |
321 | | | // {{{ logCharacterData |
322 | | | |
323 | | | function logCharacterData($parser, $data) { |
324 | | | global $curLog, $curTag, $lang, $debugxml; |
325 | | | |
326 | | | switch ($curTag) { |
327 | | | case "AUTHOR": |
328 | | | if ($debugxml) print "Author: $data\n"; |
329 | | | if (empty($data)) return; |
330 | | | $curLog->curEntry->author .= htmlentities($data, ENT_COMPAT, "UTF-8"); |
331 | | | break; |
332 | | | |
333 | | | case "DATE": |
334 | | | if ($debugxml) print "Date: $data\n"; |
335 | | | $data = trim($data); |
336 | | | if (empty($data)) return; |
337 | | | |
338 | | | sscanf($data, "%d-%d-%dT%d:%d:%d.", $y, $mo, $d, $h, $m, $s); |
339 | | | |
340 | | | $mo = substr("00".$mo, -2); |
341 | | | $d = substr("00".$d, -2); |
342 | | | $h = substr("00".$h, -2); |
343 | | | $m = substr("00".$m, -2); |
344 | | | $s = substr("00".$s, -2); |
345 | | | |
346 | | | $curLog->curEntry->date = "$y-$mo-$d $h:$m:$s GMT"; |
347 | | | |
348 | | | $committime = strtotime($curLog->curEntry->date); |
349 | | | $curLog->curEntry->committime = $committime; |
350 | | | $curtime = time(); |
351 | | | |
352 | | | // Get the number of seconds since the commit |
353 | | | $agesecs = $curtime - $committime; |
354 | | | if ($agesecs < 0) $agesecs = 0; |
355 | | | |
356 | | | $curLog->curEntry->age = datetimeFormatDuration($agesecs, true, true); |
357 | | | |
358 | | | break; |
359 | | | |
360 | | | case "MSG": |
361 | | | if ($debugxml) print "Msg: '$data'\n"; |
362 | | | $curLog->curEntry->msg .= htmlentities($data, ENT_COMPAT, "UTF-8"); |
363 | | | break; |
364 | | | |
365 | | | case "PATH": |
366 | | | if ($debugxml) print "Path name: '$data'\n"; |
367 | | | $data = trim($data); |
368 | | | if (empty($data)) return; |
369 | | | |
370 | | | $curLog->curEntry->curMod->path .= $data; |
371 | | | |
372 | | | // The XML returned when a file is renamed/branched in inconsistant. In the case |
373 | | | // of a branch, the path information doesn't include the leafname. In the case of |
374 | | | // a rename, it does. Ludicrous. |
375 | | | |
376 | | | if (!empty($curLog->path)) { |
377 | | | $pos = strrpos($curLog->path, "/"); |
378 | | | $curpath = substr($curLog->path, 0, $pos); |
379 | | | $leafname = substr($curLog->path, $pos + 1); |
380 | | | } else { |
381 | | | $curpath = ""; |
382 | | | $leafname = ""; |
383 | | | } |
384 | | | |
385 | | | if ($curLog->curEntry->curMod->action == "A") { |
386 | | | if ($debugxml) print "Examining added path '".$curLog->curEntry->curMod->copyfrom."' - Current path = '$curpath', leafname = '$leafname'\n"; |
387 | | | if ($data == $curLog->path) { // For directories and renames |
388 | | | if ($debugxml) print "New path for comparison: '".$curLog->curEntry->curMod->copyfrom."'\n"; |
389 | | | $curLog->path = $curLog->curEntry->curMod->copyfrom; |
390 | | | } else if ($data == $curpath || $data == $curpath."/") { // Logs of files that have moved due to branching |
391 | | | if ($debugxml) print "New path for comparison: '".$curLog->curEntry->curMod->copyfrom."/$leafname'\n"; |
392 | | | $curLog->path = $curLog->curEntry->curMod->copyfrom."/$leafname"; |
393 | | | } |
394 | | | } |
395 | | | break; |
396 | | | } |
397 | | | } |
398 | | | |
399 | | | // }}} |
400 | | | |
401 | | | // }}} |
402 | | | |
403 | | | // {{{ internal functions (_topLevel and _listSort) |
404 | | | |
405 | | | // Function returns true if the give entry in a directory tree is at the top level |
406 | | | |
407 | | | function _topLevel($entry) { |
408 | | | // To be at top level, there must be one space before the entry |
409 | | | return (strlen($entry) > 1 && $entry{0} == " " && $entry{1} != " "); |
410 | | | } |
411 | | | |
412 | | | // Function to sort two given directory entries. Directories go at the top |
413 | | | |
414 | | | function _listSort($e1, $e2) { |
415 | | | $isDir1 = $e1->file{strlen($e1->file) - 1} == "/"; |
416 | | | $isDir2 = $e2->file{strlen($e2->file) - 1} == "/"; |
417 | | | |
418 | | | if ($isDir1 && !$isDir2) return -1; |
419 | | | if ($isDir2 && !$isDir1) return 1; |
420 | | | |
421 | | | return strnatcasecmp($e1->file, $e2->file); |
422 | | | } |
423 | | | |
424 | | | // }}} |
425 | | | |
426 | | | // {{{ encodePath |
427 | | | |
428 | | | // Function to encode a URL without encoding the /'s |
429 | | | |
430 | | | function encodePath($uri) { |
431 | | | global $config; |
432 | | | |
433 | | | $uri = str_replace(DIRECTORY_SEPARATOR, "/", $uri); |
434 | | | |
435 | | | $parts = explode('/', $uri); |
436 | | | for ($i = 0; $i < count($parts); $i++) { |
437 | | | if ( function_exists("mb_detect_encoding") && function_exists("mb_convert_encoding")) { |
438 | | | $parts[$i] = mb_convert_encoding($parts[$i], "UTF-8", mb_detect_encoding($parts[$i])); |
439 | | | } |
440 | | | |
441 | | | $parts[$i] = rawurlencode($parts[$i]); |
442 | | | } |
443 | | | |
444 | | | $uri = implode('/', $parts); |
445 | | | |
446 | | | // Quick hack. Subversion seems to have a bug surrounding the use of %3A instead of : |
447 | | | |
448 | | | $uri = str_replace("%3A" ,":", $uri); |
449 | | | |
450 | | | // Correct for Window share names |
451 | | | if ( $config->serverIsWindows==true ) { |
452 | | | if (substr($uri, 0,2) == "//") { |
453 | | | $uri = "\\".substr($uri, 2, strlen($uri)); |
454 | | | } |
455 | | | |
456 | | | if (substr($uri, 0,10)=="file://///" ) { |
457 | | | $uri="file:///\\".substr($uri, 10, strlen($uri)); |
458 | | | } |
459 | | | } |
460 | | | |
461 | | | return $uri; |
462 | | | } |
463 | | | |
464 | | | // }}} |
465 | | | |
466 | | | // The SVNRepository class |
467 | | | |
468 | | | class SVNRepository { |
469 | | | var $repConfig; |
470 | | | |
471 | | | function SVNRepository($repConfig) { |
472 | | | $this->repConfig = $repConfig; |
473 | | | } |
474 | | | |
475 | | | // {{{ highlightLine |
476 | | | // |
477 | | | // Distill line-spanning syntax highlighting so that each line can stand alone |
478 | | | // (when invoking on the first line, $attributes should be an empty array) |
479 | | | // Invoked to make sure all open syntax highlighting tags (<font>, <i>, <b>, etc.) |
480 | | | // are closed at the end of each line and re-opened on the next line |
481 | | | |
482 | | | function highlightLine($line, &$attributes) { |
483 | | | $hline = ""; |
484 | | | |
485 | | | // Apply any highlighting in effect from the previous line |
486 | | | foreach ($attributes as $attr) { |
487 | | | $hline.=$attr['text']; |
488 | | | } |
489 | | | |
490 | | | // append the new line |
491 | | | $hline.=$line; |
492 | | | |
493 | | | // update attributes |
494 | | | for ($line = strstr($line, "<"); $line; $line = strstr(substr($line,1), "<")) { |
495 | | | if (substr($line,1,1) == "/") { // if this closes a tag, remove most recent corresponding opener |
496 | | | $tagNamLen = strcspn($line, "> \t", 2); |
497 | | | $tagNam = substr($line,2,$tagNamLen); |
498 | | | foreach (array_reverse(array_keys($attributes)) as $k) { |
499 | | | if ($attributes[$k]['tag'] == $tagNam) { |
500 | | | unset($attributes[$k]); |
501 | | | break; |
502 | | | } |
503 | | | } |
504 | | | } else { // if this opens a tag, add it to the list |
505 | | | $tagNamLen = strcspn($line, "> \t", 1); |
506 | | | $tagNam = substr($line,1,$tagNamLen); |
507 | | | $tagLen = strcspn($line, ">") + 1; |
508 | | | $attributes[] = array('tag' => $tagNam, 'text' => substr($line,0,$tagLen)); |
509 | | | } |
510 | | | } |
511 | | | |
512 | | | // close any still-open tags |
513 | | | foreach (array_reverse($attributes) as $attr) { |
514 | | | $hline.="</".$attr['tag'].">"; |
515 | | | } |
516 | | | |
517 | | | // XXX: this just simply replaces [ and ] with their entities to prevent |
518 | | | // it from being parsed by the template parser; maybe something more |
519 | | | // elegant is in order? |
520 | | | $hline = str_replace('[', '[', str_replace(']', ']', $hline) ); |
521 | | | return $hline; |
522 | | | } |
523 | | | |
524 | | | // }}} |
525 | | | |
526 | | | // {{{ getFileContents |
527 | | | // |
528 | | | // Dump the content of a file to the given filename |
529 | | | |
530 | | | function getFileContents($path, $filename, $rev = 0, $pipe = "", $perLineHighlighting = false) { |
531 | | | global $config, $extEnscript; |
532 | | | |
533 | | | // If there's no filename, we'll just deliver the contents as it is to the user |
534 | | | if ($filename == "") { |
535 | | | $path = encodepath($this->repConfig->path.$path); |
536 | | | passthru(quoteCommand($config->svn." cat ".$this->repConfig->svnParams().quote($path).' -r '.$rev.' '.$pipe)); |
537 | | | return; |
538 | | | } |
539 | | | |
540 | | | // Get the file contents info |
541 | | | |
542 | | | $ext = strrchr($path, "."); |
543 | | | $l = @$extEnscript[$ext]; |
544 | | | |
545 | | | if ($l == "php") { |
546 | | | // Output the file to the filename |
547 | | | $path = encodepath($this->repConfig->path.$path); |
548 | | | $cmd = quoteCommand($config->svn." cat ".$this->repConfig->svnParams().quote($path).' -r '.$rev.' > '.quote($filename)); |
549 | | | @exec($cmd); |
550 | | | |
551 | | | // Get the file as a string (memory hogging, but we have no other options) |
552 | | | $content = highlight_file($filename, true); |
553 | | | |
554 | | | // Destroy the previous version, and replace it with the highlighted version |
555 | | | $f = fopen($filename, "w"); |
556 | | | if ($f) { |
557 | | | // The highlight file function doesn't deal with line endings very nicely at all. We'll have to do it |
558 | | | // by hand. |
559 | | | |
560 | | | // Remove the first line generated by highlight() |
561 | | | $pos = strpos($content, "\n"); |
562 | | | $content = substr($content, $pos+1); |
563 | | | |
564 | | | $content = explode("<br />", $content); |
565 | | | |
566 | | | if ($perLineHighlighting) { |
567 | | | // If we need each line independently highlighted (e.g. for diff or blame) |
568 | | | // hen we'll need to filter the output of the highlighter |
569 | | | // to make sure tags like <font>, <i> or <b> don't span lines |
570 | | | |
571 | | | // $attributes is used to remember what highlighting attributes |
572 | | | // are in effect from one line to the next |
573 | | | $attributes = array(); // start with no attributes in effect |
574 | | | |
575 | | | foreach ($content as $line) { |
576 | | | fputs($f, $this->highlightLine(rtrim($line),$attributes)."\n"); |
577 | | | } |
578 | | | } else { |
579 | | | foreach ($content as $line) { |
580 | | | fputs($f, rtrim($line)."\n"); |
581 | | | } |
582 | | | } |
583 | | | |
584 | | | fclose($f); |
585 | | | } |
586 | | | |
587 | | | } else { |
588 | | | if ($l !== null && $config->useGeshi) { |
589 | | | $this->applyGeshi($path, $filename, $rev, $l); |
590 | | | |
591 | | | } else if ($config->useEnscript) { |
592 | | | // Get the files, feed it through enscript, then remove the enscript headers using sed |
593 | | | // |
594 | | | // Note that the sec command returns only the part of the file between <PRE> and </PRE>. |
595 | | | // It's complicated because it's designed not to return those lines themselves. |
596 | | | |
597 | | | $path = encodepath($this->repConfig->path.$path); |
598 | | | $cmd = quoteCommand($config->svn." cat ".$this->repConfig->svnParams().quote($path).' -r '.$rev.' | '. |
599 | | | $config->enscript." --language=html ". |
600 | | | ($l ? "--color --pretty-print=$l" : "")." -o - | ". |
601 | | | $config->sed." -n ".$config->quote."1,/^<PRE.$/!{/^<\\/PRE.$/,/^<PRE.$/!p;}".$config->quote." > $filename"); |
602 | | | @exec($cmd); |
603 | | | |
604 | | | } else { |
605 | | | $path = encodepath(str_replace(DIRECTORY_SEPARATOR, "/", $this->repConfig->path.$path)); |
606 | | | $cmd = quoteCommand($config->svn." cat ".$this->repConfig->svnParams().quote($path).' -r '.$rev.' > '.quote($filename)); |
607 | | | @exec($cmd); |
608 | | | } |
609 | | | } |
610 | | | } |
611 | | | |
612 | | | // }}} |
613 | | | |
614 | | | // {{{ applyGeshi |
615 | | | // |
616 | | | // perform syntax highlighting using geshi |
617 | | | |
618 | | | function applyGeshi($path, $filename, $rev = 0, $l, $return = false) { |
619 | | | global $config; |
620 | | | |
621 | | | // Output the file to the filename |
622 | | | $path = encodepath($this->repConfig->path.$path); |
623 | | | $cmd = quoteCommand($config->svn." cat ".$this->repConfig->svnParams().quote($path).' -r '.$rev.' > '.quote($filename)); |
624 | | | @exec($cmd); |
625 | | | |
626 | | | $source = file_get_contents($filename); |
627 | | | require_once 'lib/geshi.php'; |
628 | | | $geshi = new GeSHi($source, $l); |
629 | | | if ($return) { |
630 | | | return $geshi->parse_code(); |
631 | | | } else { |
632 | | | $code = $geshi->parse_code(); |
633 | | | $code = preg_replace("/^<pre.*?>/", '', $code); |
634 | | | $code = preg_replace("/<\/pre>$/", '', $code); |
635 | | | $f = @fopen($filename, 'w'); |
636 | | | fwrite($f, $code); |
637 | | | fclose($f); |
638 | | | } |
639 | | | } |
640 | | | |
641 | | | // }}} |
642 | | | |
643 | | | // {{{ listFileContents |
644 | | | // |
645 | | | // Print the contents of a file without filling up Apache's memory |
646 | | | |
647 | | | function listFileContents($path, $rev = 0) { |
648 | | | global $config, $extEnscript; |
649 | | | |
650 | | | $pre = false; |
651 | | | |
652 | | | // Get the file contents info |
653 | | | |
654 | | | $ext = strrchr($path, "."); |
655 | | | $l = @$extEnscript[$ext]; |
656 | | | |
657 | | | // Deal with php highlighting internally |
658 | | | if ($l == "php") { |
659 | | | $tmp = tempnam("temp", "wsvn"); |
660 | | | |
661 | | | // Output the file to a temporary file |
662 | | | $path = encodepath($this->repConfig->path.$path); |
663 | | | $cmd = quoteCommand($config->svn." cat ".$this->repConfig->svnParams().quote($path).' -r '.$rev.' > '.$tmp); |
664 | | | @exec($cmd); |
665 | | | $tmpStr = file_get_contents($tmp); |
666 | | | $tmpStr = str_replace(array("\r\n"), array("\n"), $tmpStr); |
667 | | | highlight_string($tmpStr); |
668 | | | @unlink($tmp); |
669 | | | } else if ($l !== null && $config->useGeshi) { |
670 | | | $tmp = tempnam("temp", "wsvn"); |
671 | | | print $this->applyGeshi($path, $tmp, $rev, $l, true); |
672 | | | unlink($tmp); |
673 | | | } else { |
674 | | | if ($config->useEnscript) { |
675 | | | $path = encodepath($this->repConfig->path.$path); |
676 | | | $cmd = quoteCommand($config->svn." cat ".$this->repConfig->svnParams().quote($path).' -r '.$rev.' | '. |
677 | | | $config->enscript." --language=html ". |
678 | | | ($l ? "--color --pretty-print=$l" : "")." -o - | ". |
679 | | | $config->sed." -n ".$config->quote."/^<PRE.$/,/^<\\/PRE.$/p".$config->quote |
680 | | | ); |
681 | | | } else { |
682 | | | $path = encodepath($this->repConfig->path.$path); |
683 | | | $cmd = quoteCommand($config->svn." cat ".$this->repConfig->svnParams().quote($path).' -r '.$rev); |
684 | | | $pre = true; |
685 | | | } |
686 | | | |
687 | | | if ($result = popen($cmd, "r")) { |
688 | | | if ($pre) echo "<PRE>"; |
689 | | | |
690 | | | while (!feof($result)) { |
691 | | | $line = fgets($result, 1024); |
692 | | | if ($pre) $line = replaceEntities($line, $this->repConfig); |
693 | | | |
694 | | | print hardspace($line); |
695 | | | } |
696 | | | |
697 | | | if ($pre) echo "</PRE>"; |
698 | | | |
699 | | | pclose($result); |
700 | | | } |
701 | | | } |
702 | | | } |
703 | | | |
704 | | | // }}} |
705 | | | |
706 | | | // {{{ getBlameDetails |
707 | | | // |
708 | | | // Dump the blame content of a file to the given filename |
709 | | | |
710 | | | function getBlameDetails($path, $filename, $rev = 0) { |
711 | | | global $config; |
712 | | | |
713 | | | $path = encodepath($this->repConfig->path.$path); |
714 | | | $cmd = quoteCommand($config->svn." blame ".$this->repConfig->svnParams().quote($path).' -r '.$rev.' > '.quote($filename)); |
715 | | | |
716 | | | @exec($cmd); |
717 | | | } |
718 | | | |
719 | | | // }}} |
720 | | | |
721 | | | // {{{ getProperty |
722 | | | |
723 | | | function getProperty($path, $property, $rev = 0) { |
724 | | | global $config; |
725 | | | |
726 | | | $path = encodepath($this->repConfig->path.$path); |
727 | | | |
728 | | | if ($rev > 0) { |
729 | | | $rev = ' -r '.$rev; |
730 | | | } else { |
731 | | | $rev = ''; |
732 | | | } |
733 | | | |
734 | | | $ret = runCommand($config->svn." propget $property ".$this->repConfig->svnParams().quote($path).$rev, true); |
735 | | | |
736 | | | // Remove the surplus newline |
737 | | | if (count($ret)) { |
738 | | | unset($ret[count($ret) - 1]); |
739 | | | } |
740 | | | |
741 | | | return implode("\n", $ret); |
742 | | | } |
743 | | | |
744 | | | // }}} |
745 | | | |
746 | | | // {{{ exportDirectory |
747 | | | // |
748 | | | // Exports the directory to the given location |
749 | | | |
750 | | | function exportDirectory($path, $filename, $rev = 0) { |
751 | | | global $config; |
752 | | | |
753 | | | $path = encodepath($this->repConfig->path.$path); |
754 | | | $cmd = quoteCommand($config->svn." export ".$this->repConfig->svnParams().quote($path).' -r '.$rev.' '.quote($filename)); |
755 | | | |
756 | | | @exec($cmd); |
757 | | | } |
758 | | | |
759 | | | // }}} |
760 | | | |
761 | | | // {{{ getList |
762 | | | |
763 | | | function getList($path, $rev = 0) { |
764 | | | global $config, $curList, $vars, $lang; |
765 | | | |
766 | | | $xml_parser = xml_parser_create("UTF-8"); |
767 | | | xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, true); |
768 | | | xml_set_element_handler($xml_parser, "listStartElement", "listEndElement"); |
769 | | | xml_set_character_data_handler($xml_parser, "listCharacterData"); |
770 | | | |
771 | | | // Since directories returned by svn log don't have trailing slashes (:-(), we need to remove |
772 | | | // the trailing slash from the path for comparison purposes |
773 | | | |
774 | | | if ($path{strlen($path) - 1} == "/" && $path != "/") { |
775 | | | $path = substr($path, 0, -1); |
776 | | | } |
777 | | | |
778 | | | $curList = new SVNList; |
779 | | | $curList->entries = array(); |
780 | | | $curList->path = $path; |
781 | | | |
782 | | | // Get the list info |
783 | | | $path = encodepath($this->repConfig->path.$path); |
784 | | | |
785 | | | if ($rev == 0) { |
786 | | | $headlog = $this->getLog("/", "", "", true, 1); |
787 | | | if (isset($headlog->entries[0])) $rev = $headlog->entries[0]->rev; |
788 | | | } |
789 | | | $revStr = "-r $rev"; |
790 | | | |
791 | | | $cmd = quoteCommand($config->svn." list --xml $revStr ".$this->repConfig->svnParams().quote($path)); |
792 | | | |
793 | | | $descriptorspec = array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w')); |
794 | | | |
795 | | | $resource = proc_open($cmd, $descriptorspec, $pipes); |
796 | | | $error = ""; |
797 | | | |
798 | | | if (!is_resource($resource)) { |
799 | | | echo "<p>".$lang['BADCMD'].": <code>".$cmd."</code></p>"; |
800 | | | exit; |
801 | | | } |
802 | | | |
803 | | | $handle = $pipes[1]; |
804 | | | $firstline = true; |
805 | | | while (!feof($handle)) { |
806 | | | $line = fgets($handle); |
807 | | | if (!xml_parse($xml_parser, $line, feof($handle))) { |
808 | | | if (xml_get_error_code($xml_parser) != 5) { |
809 | | | // errors can contain sensitive info! don't echo this ~J |
810 | | | error_log(sprintf("XML error: %s (%d) at line %d column %d byte %d\ncmd: %s", |
811 | | | xml_error_string(xml_get_error_code($xml_parser)), |
812 | | | xml_get_error_code($xml_parser), |
813 | | | xml_get_current_line_number($xml_parser), |
814 | | | xml_get_current_column_number($xml_parser), |
815 | | | xml_get_current_byte_index($xml_parser), |
816 | | | $cmd)); |
817 | | | exit; |
818 | | | } else { |
819 | | | $vars["error"] = $lang["UNKNOWNREVISION"]; |
820 | | | return 0; |
821 | | | } |
822 | | | } |
823 | | | } |
824 | | | |
825 | | | while (!feof($pipes[2])) { |
826 | | | $error .= fgets($pipes[2]); |
827 | | | } |
828 | | | |
829 | | | $error = toOutputEncoding(trim($error)); |
830 | | | |
831 | | | fclose($pipes[0]); |
832 | | | fclose($pipes[1]); |
833 | | | fclose($pipes[2]); |
834 | | | |
835 | | | proc_close($resource); |
836 | | | |
837 | | | if (!empty($error)) { |
838 | | | echo "<p>".$lang['BADCMD'].": <code>".$cmd."</code></p><p>".nl2br($error)."</p>"; |
839 | | | exit; |
840 | | | } |
841 | | | |
842 | | | xml_parser_free($xml_parser); |
843 | | | |
844 | | | // Sort the entries into alphabetical order with the directories at the top of the list |
845 | | | usort($curList->entries, "_listSort"); |
846 | | | |
847 | | | return $curList; |
848 | | | } |
849 | | | |
850 | | | // }}} |
851 | | | |
852 | | | // {{{ getLog |
853 | | | |
854 | | | function getLog($path, $brev = "", $erev = 1, $quiet = false, $limit = 2) { |
855 | | | global $config, $curLog, $vars, $lang; |
856 | | | |
857 | | | $xml_parser = xml_parser_create("UTF-8"); |
858 | | | xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, true); |
859 | | | xml_set_element_handler($xml_parser, "logStartElement", "logEndElement"); |
860 | | | xml_set_character_data_handler($xml_parser, "logCharacterData"); |
861 | | | |
862 | | | // Since directories returned by svn log don't have trailing slashes (:-(), we need to remove |
863 | | | // the trailing slash from the path for comparison purposes |
864 | | | |
865 | | | if ($path{strlen($path) - 1} == "/" && $path != "/") { |
866 | | | $path = substr($path, 0, -1); |
867 | | | } |
868 | | | |
869 | | | $curLog = new SVNLog; |
870 | | | $curLog->entries = array(); |
871 | | | $curLog->path = $path; |
872 | | | |
873 | | | $revStr = ""; |
874 | | | |
875 | | | if ($brev && $erev) { |
876 | | | $revStr = "-r$brev:$erev"; |
877 | | | } else if ($brev) { |
878 | | | $revStr = "-r$brev:1"; |
879 | | | } |
880 | | | |
881 | | | if (($config->subversionMajorVersion > 1 || $config->subversionMinorVersion >=2) && $limit != 0) { |
882 | | | $revStr .= " --limit $limit"; |
883 | | | } |
884 | | | |
885 | | | // Get the log info |
886 | | | $path = encodepath($this->repConfig->path.$path); |
887 | | | $info = "--verbose"; |
888 | | | if ($quiet) $info = "--quiet"; |
889 | | | |
890 | | | $cmd = quoteCommand($config->svn." log --xml $info $revStr ".$this->repConfig->svnParams().quote($path)); |
891 | | | |
892 | | | $descriptorspec = array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w')); |
893 | | | |
894 | | | $resource = proc_open($cmd, $descriptorspec, $pipes); |
895 | | | $error = ""; |
896 | | | |
897 | | | if (!is_resource($resource)) { |
898 | | | echo "<p>".$lang['BADCMD'].": <code>".$cmd."</code></p>"; |
899 | | | exit; |
900 | | | } |
901 | | | |
902 | | | $handle = $pipes[1]; |
903 | | | $firstline = true; |
904 | | | while (!feof($handle)) { |
905 | | | $line = fgets($handle); |
906 | | | if (!xml_parse($xml_parser, $line, feof($handle))) { |
907 | | | if (xml_get_error_code($xml_parser) != 5) { |
908 | | | // errors can contain sensitive info! don't echo this ~J |
909 | | | error_log(sprintf("XML error: %s (%d) at line %d column %d byte %d\ncmd: %s", |
910 | | | xml_error_string(xml_get_error_code($xml_parser)), |
911 | | | xml_get_error_code($xml_parser), |
912 | | | xml_get_current_line_number($xml_parser), |
913 | | | xml_get_current_column_number($xml_parser), |
914 | | | xml_get_current_byte_index($xml_parser), |
915 | | | $cmd)); |
916 | | | exit; |
917 | | | } else { |
918 | | | $vars["error"] = $lang["UNKNOWNREVISION"]; |
919 | | | return 0; |
920 | | | } |
921 | | | } |
922 | | | } |
923 | | | |
924 | | | while (!feof($pipes[2])) { |
925 | | | $error .= fgets($pipes[2]); |
926 | | | } |
927 | | | |
928 | | | $error = toOutputEncoding(trim($error)); |
929 | | | |
930 | | | fclose($pipes[0]); |
931 | | | fclose($pipes[1]); |
932 | | | fclose($pipes[2]); |
933 | | | |
934 | | | proc_close($resource); |
935 | | | |
936 | | | if (!empty($error)) { |
937 | | | echo "<p>".$lang['BADCMD'].": <code>".$cmd."</code></p><p>".nl2br($error)."</p>"; |
938 | | | exit; |
939 | | | } |
940 | | | |
941 | | | xml_parser_free($xml_parser); |
942 | | | |
943 | | | foreach ($curLog->entries as $entryKey => $entry) { |
944 | | | $fullModAccess = true; |
945 | | | $anyModAccess = (count($entry->mods) == 0); |
946 | | | foreach ($entry->mods as $modKey => $mod) { |
947 | | | $access = $this->repConfig->hasReadAccess($mod->path); |
948 | | | if ($access) { |
949 | | | $anyModAccess = true; |
950 | | | } else { |
951 | | | // hide modified entry when access is prohibited |
952 | | | unset($curLog->entries[$entryKey]->mods[$modKey]); |
953 | | | $fullModAccess = false; |
954 | | | } |
955 | | | } |
956 | | | if (!$fullModAccess) { |
957 | | | // hide commit message when access to any of the entries is prohibited |
958 | | | $curLog->entries[$entryKey]->msg = ''; |
959 | | | } |
960 | | | if (!$anyModAccess) { |
961 | | | // hide author and date when access to all of the entries is prohibited |
962 | | | $curLog->entries[$entryKey]->author = ''; |
963 | | | $curLog->entries[$entryKey]->date = ''; |
964 | | | $curLog->entries[$entryKey]->committime = ''; |
965 | | | $curLog->entries[$entryKey]->age = ''; |
966 | | | } |
967 | | | } |
968 | | | |
969 | | | return $curLog; |
970 | | | } |
971 | | | |
972 | | | // }}} |
973 | | | |
974 | | | } |
975 | | | |
976 | | | // {{{ initSvnVersion |
977 | | | |
978 | | | function initSvnVersion(&$major, &$minor) { |
979 | | | global $config; |
980 | | | |
981 | | | $ret = runCommand($config->svn_noparams." --version", false); |
982 | | | |
983 | | | if (preg_match("~([0-9]?)\.([0-9]?)\.([0-9]?)~",$ret[0],$matches)) { |
984 | | | $major = $matches[1]; |
985 | | | $minor = $matches[2]; |
986 | | | } |
987 | | | |
988 | | | $config->setSubversionMajorVersion($major); |
989 | | | $config->setSubversionMinorVersion($minor); |
990 | | | } |
991 | | | |
992 | | | // }}} |