PageRenderTime 55ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/websvn-2.3.3/include/svnlook.php

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