path, $b->path); } class SVNLog { var $entries; // Array of entries var $curEntry; // Current entry var $path = ''; // Temporary variable used to trace path history // findEntry // // Return the entry for a given revision function findEntry($rev) { foreach ($this->entries as $index => $entry) { if ($entry->rev == $rev) { return $index; } } } } // }}} // {{{ XML parsing functions--- $curTag = ''; $curInfo = 0; // {{{ infoStartElement function infoStartElement($parser, $name, $attrs) { global $curInfo, $curTag, $debugxml; switch ($name) { case 'INFO': if ($debugxml) print 'Starting info'."\n"; break; case 'ENTRY': if ($debugxml) print 'Creating info entry'."\n"; if (count($attrs)) { while (list($k, $v) = each($attrs)) { switch ($k) { case 'KIND': if ($debugxml) print 'Kind '.$v."\n"; $curInfo->isdir = ($v == 'dir'); break; case 'REVISION': if ($debugxml) print 'Revision '.$v."\n"; $curInfo->rev = $v; break; } } } break; default: $curTag = $name; break; } } // }}} // {{{ infoEndElement function infoEndElement($parser, $name) { global $curInfo, $debugxml, $curTag; switch ($name) { case 'ENTRY': if ($debugxml) print 'Ending info entry'."\n"; if ($curInfo->isdir) { $curInfo->path .= '/'; } break; } $curTag = ''; } // }}} // {{{ infoCharacterData function infoCharacterData($parser, $data) { global $curInfo, $curTag, $debugxml; switch ($curTag) { case 'URL': if ($debugxml) print 'Url: '.$data."\n"; $curInfo->path = $data; break; case 'ROOT': if ($debugxml) print 'Root: '.$data."\n"; $curInfo->path = urldecode(substr($curInfo->path, strlen($data))); break; } } // }}} $curList = 0; // {{{ listStartElement function listStartElement($parser, $name, $attrs) { global $curList, $curTag, $debugxml; switch ($name) { case 'LIST': if ($debugxml) print 'Starting list'."\n"; if (count($attrs)) { while (list($k, $v) = each($attrs)) { switch ($k) { case 'PATH': if ($debugxml) print 'Path '.$v."\n"; $curList->path = $v; break; } } } break; case 'ENTRY': if ($debugxml) print 'Creating new entry'."\n"; $curList->curEntry = new SVNListEntry; if (count($attrs)) { while (list($k, $v) = each($attrs)) { switch ($k) { case 'KIND': if ($debugxml) print 'Kind '.$v."\n"; $curList->curEntry->isdir = ($v == 'dir'); break; } } } break; case 'COMMIT': if ($debugxml) print 'Commit'."\n"; if (count($attrs)) { while (list($k, $v) = each($attrs)) { switch ($k) { case 'REVISION': if ($debugxml) print 'Revision '.$v."\n"; $curList->curEntry->rev = $v; break; } } } break; default: $curTag = $name; break; } } // }}} // {{{ listEndElement function listEndElement($parser, $name) { global $curList, $debugxml, $curTag; switch ($name) { case 'ENTRY': if ($debugxml) print 'Ending new list entry'."\n"; if ($curList->curEntry->isdir) { $curList->curEntry->file .= '/'; } $curList->entries[] = $curList->curEntry; $curList->curEntry = null; break; } $curTag = ''; } // }}} // {{{ listCharacterData function listCharacterData($parser, $data) { global $curList, $curTag, $debugxml; switch ($curTag) { case 'NAME': if ($debugxml) print 'Name: '.$data."\n"; if ($data === false || $data === '') return; $curList->curEntry->file .= $data; break; case 'AUTHOR': if ($debugxml) print 'Author: '.$data."\n"; if ($data === false || $data === '') return; if (function_exists('mb_detect_encoding') && function_exists('mb_convert_encoding')) $data = mb_convert_encoding($data, 'UTF-8', mb_detect_encoding($data)); $curList->curEntry->author .= $data; break; case 'DATE': if ($debugxml) print 'Date: '.$data."\n"; $data = trim($data); if ($data === false || $data === '') return; $committime = parseSvnTimestamp($data); $curList->curEntry->committime = $committime; $curList->curEntry->date = strftime('%Y-%m-%d %H:%M:%S', $committime); $curList->curEntry->age = datetimeFormatDuration(max(time() - $committime, 0), true, true); break; } } // }}} $curLog = 0; // {{{ logStartElement function logStartElement($parser, $name, $attrs) { global $curLog, $curTag, $debugxml; switch ($name) { case 'LOGENTRY': if ($debugxml) print 'Creating new log entry'."\n"; $curLog->curEntry = new SVNLogEntry; $curLog->curEntry->mods = array(); $curLog->curEntry->path = $curLog->path; if (count($attrs)) { while (list($k, $v) = each($attrs)) { switch ($k) { case 'REVISION': if ($debugxml) print 'Revision '.$v."\n"; $curLog->curEntry->rev = $v; break; } } } break; case 'PATH': if ($debugxml) print 'Creating new path'."\n"; $curLog->curEntry->curMod = new SVNMod; if (count($attrs)) { while (list($k, $v) = each($attrs)) { switch ($k) { case 'ACTION': if ($debugxml) print 'Action '.$v."\n"; $curLog->curEntry->curMod->action = $v; break; case 'COPYFROM-PATH': if ($debugxml) print 'Copy from: '.$v."\n"; $curLog->curEntry->curMod->copyfrom = $v; break; case 'COPYFROM-REV': $curLog->curEntry->curMod->copyrev = $v; break; case 'KIND': if ($debugxml) print 'Kind '.$v."\n"; $curLog->curEntry->curMod->isdir = ($v == 'dir'); break; } } } $curTag = $name; break; default: $curTag = $name; break; } } // }}} // {{{ logEndElement function logEndElement($parser, $name) { global $curLog, $debugxml, $curTag; switch ($name) { case 'LOGENTRY': if ($debugxml) print 'Ending new log entry'."\n"; $curLog->entries[] = $curLog->curEntry; break; case 'PATH': if ($debugxml) print 'Ending path'."\n"; $curLog->curEntry->mods[] = $curLog->curEntry->curMod; break; case 'MSG': $curLog->curEntry->msg = trim($curLog->curEntry->msg); if ($debugxml) print 'Completed msg = "'.$curLog->curEntry->msg.'"'."\n"; break; } $curTag = ''; } // }}} // {{{ logCharacterData function logCharacterData($parser, $data) { global $curLog, $curTag, $debugxml; switch ($curTag) { case 'AUTHOR': if ($debugxml) print 'Author: '.$data."\n"; if ($data === false || $data === '') return; if (function_exists('mb_detect_encoding') && function_exists('mb_convert_encoding')) $data = mb_convert_encoding($data, 'UTF-8', mb_detect_encoding($data)); $curLog->curEntry->author .= $data; break; case 'DATE': if ($debugxml) print 'Date: '.$data."\n"; $data = trim($data); if ($data === false || $data === '') return; $committime = parseSvnTimestamp($data); $curLog->curEntry->committime = $committime; $curLog->curEntry->date = strftime('%Y-%m-%d %H:%M:%S', $committime); $curLog->curEntry->age = datetimeFormatDuration(max(time() - $committime, 0), true, true); break; case 'MSG': if ($debugxml) print 'Msg: '.$data."\n"; if (function_exists('mb_detect_encoding') && function_exists('mb_convert_encoding')) $data = mb_convert_encoding($data, 'UTF-8', mb_detect_encoding($data)); $curLog->curEntry->msg .= $data; break; case 'PATH': if ($debugxml) print 'Path name: '.$data."\n"; $data = trim($data); if ($data === false || $data === '') return; $curLog->curEntry->curMod->path .= $data; // The XML returned when a file is renamed/branched in inconsistent. // In the case of a branch, the path doesn't include the leafname. // In the case of a rename, it does. Ludicrous. if (!empty($curLog->path)) { $pos = strrpos($curLog->path, '/'); $curpath = substr($curLog->path, 0, $pos); $leafname = substr($curLog->path, $pos + 1); } else { $curpath = ''; $leafname = ''; } $curMod = $curLog->curEntry->curMod; if ($curMod->action == 'A') { if ($debugxml) print 'Examining added path "'.$curMod->copyfrom.'" - Current path = "'.$curpath.'", leafname = "'.$leafname.'"'."\n"; if ($data == $curLog->path) { // For directories and renames $curLog->path = $curMod->copyfrom; } else if ($data == $curpath || $data == $curpath.'/') { // Logs of files that have moved due to branching $curLog->path = $curMod->copyfrom.'/'.$leafname; } else { $curLog->path = str_replace($curMod->path, $curMod->copyfrom, $curLog->path); } if ($debugxml) print 'New path for comparison: "'.$curLog->path.'"'."\n"; } break; } } // }}} // }}} // {{{ internal functions (_topLevel and _listSort) // Function returns true if the give entry in a directory tree is at the top level function _topLevel($entry) { // To be at top level, there must be one space before the entry return (strlen($entry) > 1 && $entry{0} == ' ' && $entry{1} != ' '); } // Function to sort two given directory entries. // Directories go at the top if config option alphabetic is not set function _listSort($e1, $e2) { global $config; $file1 = $e1->file; $file2 = $e2->file; $isDir1 = ($file1{strlen($file1) - 1} == '/'); $isDir2 = ($file2{strlen($file2) - 1} == '/'); if (!$config->isAlphabeticOrder()) { if ($isDir1 && !$isDir2) return -1; if ($isDir2 && !$isDir1) return 1; } if ($isDir1) $file1 = substr($file1, 0, -1); if ($isDir2) $file2 = substr($file2, 0, -1); return strnatcasecmp($file1, $file2); } // }}} // {{{ encodePath // Function to encode a URL without encoding the /'s function encodePath($uri) { global $config; $uri = str_replace(DIRECTORY_SEPARATOR, '/', $uri); if (function_exists('mb_detect_encoding') && function_exists('mb_convert_encoding')) { $uri = mb_convert_encoding($uri, 'UTF-8', mb_detect_encoding($uri)); } $parts = explode('/', $uri); $partscount = count($parts); for ($i = 0; $i < $partscount; $i++) { // do not urlencode the 'svn+ssh://' part! if ($i != 0 || $parts[$i] != 'svn+ssh:') { $parts[$i] = rawurlencode($parts[$i]); } } $uri = implode('/', $parts); // Quick hack. Subversion seems to have a bug surrounding the use of %3A instead of : $uri = str_replace('%3A', ':', $uri); // Correct for Window share names if ($config->serverIsWindows) { if (substr($uri, 0, 2) == '//') { $uri = '\\'.substr($uri, 2, strlen($uri)); } if (substr($uri, 0, 10) == 'file://///' ) { $uri = 'file:///\\'.substr($uri, 10, strlen($uri)); } } return $uri; } // }}} function _equalPart($str1, $str2) { $len1 = strlen($str1); $len2 = strlen($str2); $i = 0; while ($i < $len1 && $i < $len2) { if (strcmp($str1{$i}, $str2{$i}) != 0) { break; } $i++; } if ($i == 0) { return ''; } return substr($str1, 0, $i); } // The SVNRepository class class SVNRepository { var $repConfig; var $geshi = null; function SVNRepository($repConfig) { $this->repConfig = $repConfig; } // {{{ highlightLine // // Distill line-spanning syntax highlighting so that each line can stand alone // (when invoking on the first line, $attributes should be an empty array) // Invoked to make sure all open syntax highlighting tags (, , , etc.) // are closed at the end of each line and re-opened on the next line function highlightLine($line, &$attributes) { $hline = ''; // Apply any highlighting in effect from the previous line foreach ($attributes as $attr) { $hline .= $attr['text']; } // append the new line $hline .= $line; // update attributes for ($line = strstr($line, '<'); $line; $line = strstr(substr($line, 1), '<')) { if (substr($line, 1, 1) == '/') { // if this closes a tag, remove most recent corresponding opener $tagNamLen = strcspn($line, '> '."\t", 2); $tagNam = substr($line, 2, $tagNamLen); foreach (array_reverse(array_keys($attributes)) as $k) { if ($attributes[$k]['tag'] == $tagNam) { unset($attributes[$k]); break; } } } else { // if this opens a tag, add it to the list $tagNamLen = strcspn($line, '> '."\t", 1); $tagNam = substr($line, 1, $tagNamLen); $tagLen = strcspn($line, '>') + 1; $attributes[] = array('tag' => $tagNam, 'text' => substr($line, 0, $tagLen)); } } // close any still-open tags foreach (array_reverse($attributes) as $attr) { $hline .= ''; } // XXX: this just simply replaces [ and ] with their entities to prevent // it from being parsed by the template parser; maybe something more // elegant is in order? $hline = str_replace('[', '[', str_replace(']', ']', $hline) ); return $hline; } // }}} // Private function to simplify creation of common SVN command string text. function svnCommandString($command, $path, $rev, $peg) { global $config; return $config->getSvnCommand().$this->repConfig->svnCredentials().' '.$command.' '.($rev ? '-r '.$rev.' ' : '').quote(encodePath($this->getSvnPath($path)).'@'.($peg ? $peg : '')); } // Private function to simplify creation of enscript command string text. function enscriptCommandString($path) { global $config, $extEnscript; $filename = basename($path); $ext = strrchr($path, '.'); $lang = false; if (array_key_exists($filename, $extEnscript)) { $lang = $extEnscript[$filename]; } else if (array_key_exists($ext, $extEnscript)) { $lang = $extEnscript[$ext]; } $cmd = $config->enscript.' --language=html'; if ($lang !== false) { $cmd .= ' --color --'.(!$config->getUseEnscriptBefore_1_6_3() ? 'highlight' : 'pretty-print').'='.$lang; } $cmd .= ' -o -'; return $cmd; } // {{{ getFileContents // // Dump the content of a file to the given filename function getFileContents($path, $filename, $rev = 0, $peg = '', $pipe = '', $highlight = 'file') { global $config; assert ($highlight == 'file' || $highlight == 'no' || $highlight == 'line'); $highlighted = false; // If there's no filename, just deliver the contents as-is to the user if ($filename == '') { $cmd = $this->svnCommandString('cat', $path, $rev, $peg); passthruCommand($cmd.' '.$pipe); return $highlighted; } // Get the file contents info $tempname = $filename; if ($highlight == 'line') { $tempname = tempnamWithCheck($config->getTempDir(), ''); } $highlighted = true; if ($highlight != 'no' && $config->useGeshi && $geshiLang = $this->highlightLanguageUsingGeshi($path)) { $this->applyGeshi($path, $tempname, $geshiLang, $rev, $peg); } else if ($highlight != 'no' && $config->useEnscript) { // Get the files, feed it through enscript, then remove the enscript headers using sed // Note that the sed command returns only the part of the file between
 and 
. // It's complicated because it's designed not to return those lines themselves. $cmd = $this->svnCommandString('cat', $path, $rev, $peg); $cmd = quoteCommand($cmd.' | '.$this->enscriptCommandString($path).' | '. $config->sed.' -n '.$config->quote.'1,/^quote.' > '.$tempname); } else { $highlighted = false; $cmd = $this->svnCommandString('cat', $path, $rev, $peg); $cmd = quoteCommand($cmd.' > '.quote($filename)); } if (isset($cmd)) { $descriptorspec = array(2 => array('pipe', 'w')); // stderr $resource = proc_open($cmd, $descriptorspec, $pipes); $error = ''; while (!feof($pipes[2])) { $error .= fgets($pipes[2]); } $error = trim($error); fclose($pipes[2]); proc_close($resource); if (!empty($error)) { global $lang; error_log($lang['BADCMD'].': '.$cmd); error_log($error); global $vars; $vars['warning'] = nl2br(escape(toOutputEncoding($error))); } } if ($highlighted && $highlight == 'line') { // If we need each line independently highlighted (e.g. for diff or blame) // then we'll need to filter the output of the highlighter // to make sure tags like , or don't span lines $dst = fopen($filename, 'w'); if ($dst) { $content = file_get_contents($tempname); $content = explode('
', $content); // $attributes is used to remember what highlighting attributes // are in effect from one line to the next $attributes = array(); // start with no attributes in effect foreach ($content as $line) { fputs($dst, $this->highlightLine(trim($line), $attributes)."\n"); } fclose($dst); } } if ($tempname != $filename) { @unlink($tempname); } return $highlighted; } // }}} // {{{ highlightLanguageUsingGeshi // // check if geshi can highlight the given extension and return the language function highlightLanguageUsingGeshi($path) { global $extGeshi; $filename = basename($path); $ext = strrchr($path, '.'); if (substr($ext, 0, 1) == '.') $ext = substr($ext, 1); foreach ($extGeshi as $language => $extensions) { if (in_array($filename, $extensions) || in_array($ext, $extensions)) { if ($this->geshi === null) { require_once 'lib/geshi.php'; $this->geshi = new GeSHi(); } else { $this->geshi->error = false; } $this->geshi->set_language($language); if ($this->geshi->error() === false) { return $language; } } } return ''; } // }}} // {{{ applyGeshi // // perform syntax highlighting using geshi function applyGeshi($path, $filename, $language, $rev, $peg = '', $return = false) { // Output the file to the filename $cmd = quoteCommand($this->svnCommandString('cat', $path, $rev, $peg).' > '.quote($filename)); $descriptorspec = array(2 => array('pipe', 'w')); // stderr $resource = proc_open($cmd, $descriptorspec, $pipes); $error = ''; while (!feof($pipes[2])) { $error .= fgets($pipes[2]); } $error = trim($error); fclose($pipes[2]); proc_close($resource); if (!empty($error)) { global $lang; error_log($lang['BADCMD'].': '.$cmd); error_log($error); global $vars; $vars['warning'] = 'Unable to cat file: '.nl2br(escape(toOutputEncoding($error))); return; } $source = file_get_contents($filename); if ($this->geshi === null) { require_once 'lib/geshi.php'; $this->geshi = new GeSHi(); } $this->geshi->set_source($source); $this->geshi->set_language($language); $this->geshi->set_header_type(GESHI_HEADER_NONE); $this->geshi->set_overall_class('geshi'); $this->geshi->set_tab_width($this->repConfig->getExpandTabsBy()); if ($return) { return $this->geshi->parse_code(); } else { $f = @fopen($filename, 'w'); fwrite($f, $this->geshi->parse_code()); fclose($f); } } // }}} // {{{ listFileContents // // Print the contents of a file without filling up Apache's memory function listFileContents($path, $rev = 0, $peg = '') { global $config; if ($config->useGeshi && $geshiLang = $this->highlightLanguageUsingGeshi($path)) { $tempname = tempnamWithCheck($config->getTempDir(), 'wsvn'); if ($tempname !== false) { print toOutputEncoding($this->applyGeshi($path, $tempname, $geshiLang, $rev, $peg, true)); @unlink($tempname); } } else { $pre = false; $cmd = $this->svnCommandString('cat', $path, $rev, $peg); if ($config->useEnscript) { $cmd .= ' | '.$this->enscriptCommandString($path).' | '. $config->sed.' -n '.$config->quote.'/^quote; } else { $pre = true; } if ($result = popenCommand($cmd, 'r')) { if ($pre) echo '
';
				while (!feof($result)) {
					$line = fgets($result, 1024);
					$line = toOutputEncoding($line);
					if ($pre) {
						$line = escape($line);
					}
					print hardspace($line);
				}
				if ($pre)
					echo '
'; pclose($result); } } } // }}} // {{{ getBlameDetails // // Dump the blame content of a file to the given filename function getBlameDetails($path, $filename, $rev = 0, $peg = '') { $cmd = quoteCommand($this->svnCommandString('blame', $path, $rev, $peg).' > '.quote($filename)); $descriptorspec = array(2 => array('pipe', 'w')); // stderr $resource = proc_open($cmd, $descriptorspec, $pipes); $error = ''; while (!feof($pipes[2])) { $error .= fgets($pipes[2]); } $error = trim($error); fclose($pipes[2]); proc_close($resource); if (!empty($error)) { global $lang; error_log($lang['BADCMD'].': '.$cmd); error_log($error); global $vars; $vars['warning'] = 'No blame info: '.nl2br(escape(toOutputEncoding($error))); } } // }}} function getProperties($path, $rev = 0, $peg = '') { $cmd = $this->svnCommandString('proplist', $path, $rev, $peg); $ret = runCommand($cmd, true); $properties = array(); if (is_array($ret)) { foreach ($ret as $line) { if (substr($line, 0, 1) == ' ') { $properties[] = ltrim($line); } } } return $properties; } // {{{ getProperty function getProperty($path, $property, $rev = 0, $peg = '') { $cmd = $this->svnCommandString('propget '.$property, $path, $rev, $peg); $ret = runCommand($cmd, true); // Remove the surplus newline if (count($ret)) { unset($ret[count($ret) - 1]); } return implode("\n", $ret); } // }}} // {{{ exportDirectory // // Exports the directory to the given location function exportRepositoryPath($path, $filename, $rev = 0, $peg = '') { $cmd = $this->svnCommandString('export', $path, $rev, $peg).' '.quote($filename); $retcode = 0; execCommand($cmd, $retcode); if ($retcode != 0) { global $lang; error_log($lang['BADCMD'].': '.$cmd); } return $retcode; } // }}} // {{{ getInfo function getInfo($path, $rev = 0, $peg = '') { global $config, $curInfo; $xml_parser = xml_parser_create('UTF-8'); xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, true); xml_set_element_handler($xml_parser, 'infoStartElement', 'infoEndElement'); xml_set_character_data_handler($xml_parser, 'infoCharacterData'); // Since directories returned by svn log don't have trailing slashes (:-(), we need to remove // the trailing slash from the path for comparison purposes if ($path{strlen($path) - 1} == '/' && $path != '/') { $path = substr($path, 0, -1); } $curInfo = new SVNInfoEntry; // Get the svn info if ($rev == 0) { $headlog = $this->getLog('/', '', '', true, 1); if ($headlog && isset($headlog->entries[0])) $rev = $headlog->entries[0]->rev; } $cmd = quoteCommand($this->svnCommandString('info --xml', $path, $rev, $peg)); $descriptorspec = array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w')); $resource = proc_open($cmd, $descriptorspec, $pipes); if (!is_resource($resource)) { global $lang; echo $lang['BADCMD'].': '.escape(stripCredentialsFromCommand($cmd)).''; exit; } $handle = $pipes[1]; $firstline = true; while (!feof($handle)) { $line = fgets($handle); if (!xml_parse($xml_parser, $line, feof($handle))) { $errorMsg = sprintf('XML error: %s (%d) at line %d column %d byte %d'."\n".'cmd: %s', xml_error_string(xml_get_error_code($xml_parser)), xml_get_error_code($xml_parser), xml_get_current_line_number($xml_parser), xml_get_current_column_number($xml_parser), xml_get_current_byte_index($xml_parser), $cmd); if (xml_get_error_code($xml_parser) != 5) { // errors can contain sensitive info! don't echo this ~J error_log($errorMsg); exit; } else { break; } } } $error = ''; while (!feof($pipes[2])) { $error .= fgets($pipes[2]); } $error = toOutputEncoding(trim($error)); fclose($pipes[0]); fclose($pipes[1]); fclose($pipes[2]); proc_close($resource); xml_parser_free($xml_parser); if (!empty($error)) { $error = toOutputEncoding(nl2br(str_replace('svn: ', '', $error))); global $lang; error_log($lang['BADCMD'].': '.$cmd); error_log($error); global $vars; if (strstr($error, 'found format')) { $vars['error'] = 'Repository uses a newer format than Subversion '.$config->getSubversionVersion().' can read. ("'.nl2br(escape(toOutputEncoding(substr($error, strrpos($error, 'Expected'))))).'.")'; } else if (strstr($error, 'No such revision')) { $vars['warning'] = 'Revision '.escape($rev).' of this resource does not exist.'; } else { $vars['error'] = $lang['BADCMD'].': '.escape(stripCredentialsFromCommand($cmd)).'
'.nl2br(escape(toOutputEncoding($error))); } return null; } if ($this->repConfig->subpath !== null) { if (substr($curInfo->path, 0, strlen($this->repConfig->subpath) + 1) === '/'. $this->repConfig->subpath) { $curInfo->path = substr($curInfo->path, strlen($this->repConfig->subpath) + 1); } else { global $vars; $vars['error'] = 'Info entry does not start with subpath for repository with subpath'; return null; } } return $curInfo; } // }}} // {{{ getList function getList($path, $rev = 0, $peg = '') { global $config, $curList; $xml_parser = xml_parser_create('UTF-8'); xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, true); xml_set_element_handler($xml_parser, 'listStartElement', 'listEndElement'); xml_set_character_data_handler($xml_parser, 'listCharacterData'); // Since directories returned by svn log don't have trailing slashes (:-(), we need to remove // the trailing slash from the path for comparison purposes if ($path{strlen($path) - 1} == '/' && $path != '/') { $path = substr($path, 0, -1); } $curList = new SVNList; $curList->entries = array(); $curList->path = $path; // Get the list info if ($rev == 0) { $headlog = $this->getLog('/', '', '', true, 1); if ($headlog && isset($headlog->entries[0])) $rev = $headlog->entries[0]->rev; } $cmd = quoteCommand($this->svnCommandString('list --xml', $path, $rev, $peg)); $descriptorspec = array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w')); $resource = proc_open($cmd, $descriptorspec, $pipes); if (!is_resource($resource)) { global $lang; echo $lang['BADCMD'].': '.escape(stripCredentialsFromCommand($cmd)).''; exit; } $handle = $pipes[1]; $firstline = true; while (!feof($handle)) { $line = fgets($handle); if (!xml_parse($xml_parser, $line, feof($handle))) { $errorMsg = sprintf('XML error: %s (%d) at line %d column %d byte %d'."\n".'cmd: %s', xml_error_string(xml_get_error_code($xml_parser)), xml_get_error_code($xml_parser), xml_get_current_line_number($xml_parser), xml_get_current_column_number($xml_parser), xml_get_current_byte_index($xml_parser), $cmd); if (xml_get_error_code($xml_parser) != 5) { // errors can contain sensitive info! don't echo this ~J error_log($errorMsg); exit; } else { break; } } } $error = ''; while (!feof($pipes[2])) { $error .= fgets($pipes[2]); } $error = toOutputEncoding(trim($error)); fclose($pipes[0]); fclose($pipes[1]); fclose($pipes[2]); proc_close($resource); xml_parser_free($xml_parser); if (!empty($error)) { $error = toOutputEncoding(nl2br(str_replace('svn: ', '', $error))); global $lang; error_log($lang['BADCMD'].': '.$cmd); error_log($error); global $vars; if (strstr($error, 'found format')) { $vars['error'] = 'Repository uses a newer format than Subversion '.$config->getSubversionVersion().' can read. ("'.nl2br(escape(toOutputEncoding(substr($error, strrpos($error, 'Expected'))))).'.")'; } else if (strstr($error, 'No such revision')) { $vars['warning'] = 'Revision '.escape($rev).' of this resource does not exist.'; } else { $vars['error'] = $lang['BADCMD'].': '.escape(stripCredentialsFromCommand($cmd)).'
'.nl2br(escape(toOutputEncoding($error))); } return null; } // Sort the entries into alphabetical order usort($curList->entries, '_listSort'); return $curList; } // }}} // {{{ getLog function getLog($path, $brev = '', $erev = 1, $quiet = false, $limit = 2, $peg = '') { global $config, $curLog; $xml_parser = xml_parser_create('UTF-8'); xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, true); xml_set_element_handler($xml_parser, 'logStartElement', 'logEndElement'); xml_set_character_data_handler($xml_parser, 'logCharacterData'); // Since directories returned by svn log don't have trailing slashes (:-(), // we must remove the trailing slash from the path for comparison purposes. if ($path != '/' && $path{strlen($path) - 1} == '/') { $path = substr($path, 0, -1); } $curLog = new SVNLog; $curLog->entries = array(); $curLog->path = $path; // Get the log info $effectiveRev = ($brev && $erev ? $brev.':'.$erev : ($brev ? $brev.':1' : '')); $effectivePeg = ($peg ? $peg : ($brev ? $brev : '')); $cmd = quoteCommand($this->svnCommandString('log --xml '.($quiet ? '--quiet' : '--verbose'), $path, $effectiveRev, $effectivePeg)); if (($config->subversionMajorVersion > 1 || $config->subversionMinorVersion >= 2) && $limit != 0) { $cmd .= ' --limit '.$limit; } $descriptorspec = array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w')); $resource = proc_open($cmd, $descriptorspec, $pipes); if (!is_resource($resource)) { global $lang; echo $lang['BADCMD'].': '.escape(stripCredentialsFromCommand($cmd)).''; exit; } $handle = $pipes[1]; $firstline = true; while (!feof($handle)) { $line = fgets($handle); if (!xml_parse($xml_parser, $line, feof($handle))) { $errorMsg = sprintf('XML error: %s (%d) at line %d column %d byte %d'."\n".'cmd: %s', xml_error_string(xml_get_error_code($xml_parser)), xml_get_error_code($xml_parser), xml_get_current_line_number($xml_parser), xml_get_current_column_number($xml_parser), xml_get_current_byte_index($xml_parser), $cmd); if (xml_get_error_code($xml_parser) != 5) { // errors can contain sensitive info! don't echo this ~J error_log($errorMsg); exit; } else { break; } } } $error = ''; while (!feof($pipes[2])) { $error .= fgets($pipes[2]); } $error = trim($error); fclose($pipes[0]); fclose($pipes[1]); fclose($pipes[2]); proc_close($resource); if (!empty($error)) { global $lang; error_log($lang['BADCMD'].': '.$cmd); error_log($error); global $vars; if (strstr($error, 'found format')) { $vars['error'] = 'Repository uses a newer format than Subversion '.$config->getSubversionVersion().' can read. ("'.nl2br(escape(toOutputEncoding(substr($error, strrpos($error, 'Expected'))))).'.")'; } else if (strstr($error, 'No such revision')) { $vars['warning'] = 'Revision '.escape($brev).' of this resource does not exist.'; } else { $vars['error'] = $lang['BADCMD'].': '.escape(stripCredentialsFromCommand($cmd)).'
'.nl2br(escape(toOutputEncoding($error))); } return null; } xml_parser_free($xml_parser); foreach ($curLog->entries as $entryKey => $entry) { $fullModAccess = true; $anyModAccess = (count($entry->mods) == 0); $precisePath = null; foreach ($entry->mods as $modKey => $mod) { $access = $this->repConfig->hasReadAccess($mod->path); if ($access) { $anyModAccess = true; // find path which is parent of all modification but more precise than $curLogEntry->path $modpath = $mod->path; if (!$mod->isdir || $mod->action == 'D') { $pos = strrpos($modpath, '/'); $modpath = substr($modpath, 0, $pos + 1); } if (strlen($modpath) == 0 || substr($modpath, -1) !== '/') { $modpath .= '/'; } //compare with current precise path if ($precisePath === null) { $precisePath = $modpath; } else { $equalPart = _equalPart($precisePath, $modpath); if (substr($equalPart, -1) !== '/') { $pos = strrpos($equalPart, '/'); $equalPart = substr($equalPart, 0, $pos + 1); } $precisePath = $equalPart; } } else { // hide modified entry when access is prohibited unset($curLog->entries[$entryKey]->mods[$modKey]); $fullModAccess = false; } // fix paths if command was for a subpath repository if ($this->repConfig->subpath !== null) { if (substr($mod->path, 0, strlen($this->repConfig->subpath) + 1) === '/'. $this->repConfig->subpath) { $curLog->entries[$entryKey]->mods[$modKey]->path = substr($mod->path, strlen($this->repConfig->subpath) + 1); } else { $vars['error'] = 'Log entries do not start with subpath for repository with subpath'; return null; } } } if (!$fullModAccess) { // hide commit message when access to any of the entries is prohibited $curLog->entries[$entryKey]->msg = ''; } if (!$anyModAccess) { // hide author and date when access to all of the entries is prohibited $curLog->entries[$entryKey]->author = ''; $curLog->entries[$entryKey]->date = ''; $curLog->entries[$entryKey]->committime = ''; $curLog->entries[$entryKey]->age = ''; } if ($precisePath !== null) { $curLog->entries[$entryKey]->precisePath = $precisePath; } else { $curLog->entries[$entryKey]->precisePath = $curLog->entries[$entryKey]->path; } } return $curLog; } // }}} function isFile($path, $rev = 0, $peg = '') { $cmd = $this->svnCommandString('info --xml', $path, $rev, $peg); return strpos(implode(' ', runCommand($cmd, true)), 'kind="file"') !== false; } // {{{ getSvnPath function getSvnPath($path) { if ($this->repConfig->subpath === null) { return $this->repConfig->path.$path; } else { return $this->repConfig->path.'/'.$this->repConfig->subpath.$path; } } // }}} } // Initialize SVN version information by parsing from command-line output. $cmd = $config->getSvnCommand(); $cmd = str_replace(array('--non-interactive', '--trust-server-cert'), array('', ''), $cmd); $cmd .= ' --version'; $ret = runCommand($cmd, false); if (preg_match('~([0-9]+)\.([0-9]+)\.([0-9]+)~', $ret[0], $matches)) { $config->setSubversionVersion($matches[0]); $config->setSubversionMajorVersion($matches[1]); $config->setSubversionMinorVersion($matches[2]); }