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// comp.php 22// 23// Compare two paths using `svn diff` 24// 25 26require_once 'include/setup.php'; 27require_once 'include/svnlook.php'; 28require_once 'include/utils.php'; 29require_once 'include/template.php'; 30 31function checkRevision($rev) 32{ 33 if (is_numeric($rev) && ((int)$rev > 0)) 34 { 35 return $rev; 36 } 37 return 'HEAD'; 38} 39 40// Make sure that we have a repository 41if (!$rep) 42{ 43 renderTemplate404('compare','NOREP'); 44} 45 46$svnrep = new SVNRepository($rep); 47 48// Retrieve the request information 49$path1 = @$_REQUEST['compare'][0]; 50$path2 = @$_REQUEST['compare'][1]; 51$rev1 = (int)@$_REQUEST['compare_rev'][0]; 52$rev2 = (int)@$_REQUEST['compare_rev'][1]; 53$manualorder = (@$_REQUEST['manualorder'] == 1); 54$ignoreWhitespace = $config->getIgnoreWhitespacesInDiff(); 55 56if (array_key_exists('ignorews', $_REQUEST)) 57{ 58 $ignoreWhitespace = (bool)$_REQUEST['ignorews']; 59} 60 61// Some page links put the revision with the path... 62if (strpos($path1, '@')) 63{ 64 list($path1, $rev1) = explode('@', $path1); 65} 66else if (strpos($path1, '@') === 0) 67{ 68 // Something went wrong. The path is missing. 69 $rev1 = substr($path1, 1); 70 $path1 = '/'; 71} 72 73if (strpos($path2, '@')) 74{ 75 list($path2, $rev2) = explode('@', $path2); 76} 77else if (strpos($path2, '@') === 0) 78{ 79 $rev2 = substr($path2, 1); 80 $path2 = '/'; 81} 82 83$rev1 = checkRevision($rev1); 84$rev2 = checkRevision($rev2); 85 86// Choose a sensible comparison order unless told not to 87 88if (!$manualorder && is_numeric($rev1) && is_numeric($rev2) && $rev1 > $rev2) 89{ 90 $temppath = $path1; 91 $path1 = $path2; 92 $path2 = $temppath; 93 94 $temprev = $rev1; 95 $rev1 = $rev2; 96 $rev2 = $temprev; 97} 98 99$vars['rev1url'] = $config->getURL($rep, $path1, 'dir').createRevAndPegString($rev1, $rev1); 100$vars['rev2url'] = $config->getURL($rep, $path2, 'dir').createRevAndPegString($rev2, $rev2); 101 102$url = $config->getURL($rep, '', 'comp'); 103$vars['reverselink'] = '<a href="'.$url.'compare%5B%5D='.urlencode($path2).'@'.$rev2.'&compare%5B%5D='.urlencode($path1).'@'.$rev1.'&manualorder=1'.($ignoreWhitespace ? '&ignorews=1' : '').'">'.$lang['REVCOMP'].'</a>'; 104$toggleIgnoreWhitespace = ''; 105 106if ($ignoreWhitespace == $config->getIgnoreWhitespacesInDiff()) 107{ 108 $toggleIgnoreWhitespace = '&ignorews='.($ignoreWhitespace ? '0' : '1'); 109} 110 111if (!$ignoreWhitespace) 112{ 113 $vars['ignorewhitespacelink'] = '<a href="'.$url.'compare%5B%5D='.urlencode($path1).'@'.$rev1.'&compare%5B%5D='.urlencode($path2).'@'.$rev2.($manualorder ? '&manualorder=1' : '').$toggleIgnoreWhitespace.'">'.$lang['IGNOREWHITESPACE'].'</a>'; 114} 115else 116{ 117 $vars['regardwhitespacelink'] = '<a href="'.$url.'compare%5B%5D='.urlencode($path1).'@'.$rev1.'&compare%5B%5D='.urlencode($path2).'@'.$rev2.($manualorder ? '&manualorder=1' : '').$toggleIgnoreWhitespace.'">'.$lang['REGARDWHITESPACE'].'</a>'; 118} 119 120if ($rev1 == 0) $rev1 = 'HEAD'; 121if ($rev2 == 0) $rev2 = 'HEAD'; 122 123$vars['repname'] = escape($rep->getDisplayName()); 124$vars['action'] = $lang['PATHCOMPARISON']; 125 126$hidden = '<input type="hidden" name="manualorder" value="1" />'; 127 128if ($config->multiViews) 129{ 130 $hidden .= '<input type="hidden" name="op" value="comp"/>'; 131} 132else 133{ 134 $hidden .= '<input type="hidden" name="repname" value="'.$repname.'" />'; 135} 136 137$vars['compare_form'] = '<form method="get" action="'.$url.'" id="compare">'.$hidden; 138$vars['compare_path1input'] = '<input type="text" size="40" name="compare[0]" value="'.escape($path1).'" />'; 139$vars['compare_path2input'] = '<input type="text" size="40" name="compare[1]" value="'.escape($path2).'" />'; 140$vars['compare_rev1input'] = '<input type="text" size="5" name="compare_rev[0]" value="'.$rev1.'" />'; 141$vars['compare_rev2input'] = '<input type="text" size="5" name="compare_rev[1]" value="'.$rev2.'" />'; 142$vars['compare_submit'] = '<input name="comparesubmit" type="submit" value="'.$lang['COMPAREPATHS'].'" />'; 143$vars['compare_endform'] = '</form>'; 144 145// safe paths are a hack for fixing XSS exploit 146$vars['path1'] = escape($path1); 147$vars['safepath1'] = escape($path1); 148$vars['path2'] = escape($path2); 149$vars['safepath2'] = escape($path2); 150 151$vars['rev1'] = $rev1; 152$vars['rev2'] = $rev2; 153 154$history1 = $svnrep->getLog($path1, $rev1, 0, false, 1); 155if (!$history1) 156{ 157 renderTemplate404('compare','NOPATH'); 158} 159else 160{ 161 $history2 = $svnrep->getLog($path2, $rev2, 0, false, 1); 162 163 if (!$history2) 164 { 165 renderTemplate404('compare','NOPATH'); 166 } 167} 168 169// Set variables used for the more recent of the two revisions 170$history = ($rev1 >= $rev2 ? $history1 : $history2); 171if ($history && $history->curEntry) 172{ 173 $logEntry = $history->curEntry; 174 $vars['rev'] = $logEntry->rev; 175 $vars['peg'] = $peg; 176 $vars['date'] = $logEntry->date; 177 $vars['age'] = datetimeFormatDuration(time() - strtotime($logEntry->date)); 178 $vars['author'] = $logEntry->author; 179 $vars['log'] = xml_entities($logEntry->msg); 180} 181else 182{ 183 $vars['warning'] = 'Problem with comparison.'; 184} 185 186$noinput = empty($path1) || empty($path2); 187 188// Generate the diff listing 189 190$relativePath1 = $path1; 191$relativePath2 = $path2; 192 193$svnpath1 = encodepath($svnrep->getSvnPath(str_replace(DIRECTORY_SEPARATOR, '/', $path1))); 194$svnpath2 = encodepath($svnrep->getSvnPath(str_replace(DIRECTORY_SEPARATOR, '/', $path2))); 195 196$debug = false; 197 198if (!$noinput) 199{ 200 $cmd = $config->getSvnCommand().$rep->svnCredentials().' diff '.($ignoreWhitespace ? '-x "-w --ignore-eol-style" ' : '').quote($svnpath1.'@'.$rev1).' '.quote($svnpath2.'@'.$rev2); 201} 202 203function clearVars() 204{ 205 global $ignoreWhitespace, $listing, $index; 206 207 if ($ignoreWhitespace && $index > 1) 208 { 209 $endBlock = false; 210 $previous = $index - 1; 211 if ($listing[$previous]['endpath']) $endBlock = 'newpath'; 212 else if ($listing[$previous]['enddifflines']) $endBlock = 'difflines'; 213 214 if ($endBlock !== false) 215 { 216 // check if block ending at previous contains real diff data 217 $i = $previous; 218 $containsOnlyEqualDiff = true; 219 $addedLines = array(); 220 $removedLines = array(); 221 while ($i >= 0 && !$listing[$i - 1][$endBlock]) 222 { 223 $diffclass = $listing[$i - 1]['diffclass']; 224 225 if ($diffclass !== 'diffadded' && $diffclass !== 'diffdeleted') 226 { 227 if ($addedLines !== $removedLines) 228 { 229 $containsOnlyEqualDiff = false; 230 break; 231 } 232 } 233 234 if (count($addedLines) > 0 && $addedLines === $removedLines) 235 { 236 $addedLines = array(); 237 $removedLines = array(); 238 } 239 240 if ($diffclass === 'diff') 241 { 242 $i--; 243 continue; 244 } 245 246 if ($diffclass === null) 247 { 248 $containsOnlyEqualDiff = false; 249 break;; 250 } 251 252 if ($diffclass === 'diffdeleted') 253 { 254 if (count($addedLines) <= count($removedLines)) 255 { 256 $containsOnlyEqualDiff = false; 257 break;; 258 } 259 260 array_unshift($removedLines, $listing[$i - 1]['line']); 261 $i--; 262 continue; 263 } 264 265 if ($diffclass === 'diffadded') 266 { 267 if (count($removedLines) > 0) 268 { 269 $containsOnlyEqualDiff = false; 270 break;; 271 } 272 273 array_unshift($addedLines, $listing[$i - 1]['line']); 274 $i--; 275 continue; 276 } 277 278 assert(false); 279 } 280 281 if ($containsOnlyEqualDiff) 282 { 283 $containsOnlyEqualDiff = $addedLines === $removedLines; 284 } 285 286 // remove blocks which only contain diffclass=diff and equal removes and adds 287 if ($containsOnlyEqualDiff) 288 { 289 for ($j = $i - 1; $j < $index; $j++) 290 { 291 unset($listing[$j]); 292 } 293 294 $index = $i - 1; 295 } 296 } 297 } 298 299 $listvar = &$listing[$index]; 300 $listvar['newpath'] = null; 301 $listvar['endpath'] = null; 302 $listvar['info'] = null; 303 $listvar['diffclass'] = null; 304 $listvar['difflines'] = null; 305 $listvar['enddifflines'] = null; 306 $listvar['properties'] = null; 307} 308 309$vars['success'] = false; 310 311if (!$noinput) 312{ 313 // TODO: Report warning/error if comparison encounters any problems 314 if ($diff = popenCommand($cmd, 'r')) 315 { 316 $listing = array(); 317 $index = 0; 318 $indiff = false; 319 $indiffproper = false; 320 $getLine = true; 321 $node = null; 322 $bufferedLine = false; 323 324 $vars['success'] = true; 325 326 while (!feof($diff)) 327 { 328 if ($getLine) 329 { 330 if ($bufferedLine === false) 331 { 332 $bufferedLine = rtrim(fgets($diff), "\r\n"); 333 } 334 335 $newlineR = strpos($bufferedLine, "\r"); 336 $newlineN = strpos($bufferedLine, "\n"); 337 if ($newlineR === false && $newlineN === false) 338 { 339 $line = $bufferedLine; 340 $bufferedLine = false; 341 } 342 else 343 { 344 $newline = ($newlineR < $newlineN ? $newlineR : $newlineN); 345 $line = substr($bufferedLine, 0, $newline); 346 $bufferedLine = substr($bufferedLine, $newline + 1); 347 } 348 } 349 350 clearVars(); 351 $getLine = true; 352 if ($debug) print 'Line = "'.$line.'"<br />'; 353 if ($indiff) 354 { 355 // If we're in a diff proper, just set up the line 356 if ($indiffproper) 357 { 358 if (strlen($line) > 0 && ($line[0] == ' ' || $line[0] == '+' || $line[0] == '-')) 359 { 360 $subline = escape(toOutputEncoding(substr($line, 1))); 361 $subline = rtrim($subline, "\n\r"); 362 $subline = ($subline) ? expandTabs($subline) : ' '; 363 $listvar = &$listing[$index]; 364 $listvar['line'] = $subline; 365 366 switch ($line[0]) 367 { 368 case ' ': 369 $listvar['diffclass'] = 'diff'; 370 if ($debug) print 'Including as diff: '.$subline.'<br />'; 371 break; 372 373 case '+': 374 $listvar['diffclass'] = 'diffadded'; 375 if ($debug) print 'Including as added: '.$subline.'<br />'; 376 break; 377 378 case '-': 379 $listvar['diffclass'] = 'diffdeleted'; 380 if ($debug) print 'Including as removed: '.$subline.'<br />'; 381 break; 382 } 383 $index++; 384 } 385 else if ($line != '\ No newline at end of file') 386 { 387 $indiffproper = false; 388 $listing[$index++]['enddifflines'] = true; 389 $getLine = false; 390 if ($debug) print 'Ending lines<br />'; 391 } 392 continue; 393 } 394 395 // Check for the start of a new diff area 396 if (!strncmp($line, '@@', 2)) 397 { 398 $pos = strpos($line, '+'); 399 $posline = substr($line, $pos); 400 $sline = 0; 401 $eline = 0; 402 sscanf($posline, '+%d,%d', $sline, $eline); 403 404 if ($debug) print 'sline = "'.$sline.'", eline = "'.$eline.'"<br />'; 405 406 // Check that this isn't a file deletion 407 if ($sline == 0 && $eline == 0) 408 { 409 $line = fgets($diff); 410 if ($debug) print 'Ignoring: "'.$line.'"<br />'; 411 412 while ($line[0] == ' ' || $line[0] == '+' || $line[0] == '-') 413 { 414 $line = fgets($diff); 415 if ($debug) print 'Ignoring: "'.$line.'"<br />'; 416 } 417 418 $getLine = false; 419 if ($debug) print 'Unignoring previous - marking as deleted<br />'; 420 $listing[$index++]['info'] = $lang['FILEDELETED']; 421 422 } 423 else 424 { 425 $listvar = &$listing[$index]; 426 $listvar['difflines'] = $line; 427 $sline = 0; 428 $slen = 0; 429 $eline = 0; 430 $elen = 0; 431 sscanf($line, '@@ -%d,%d +%d,%d @@', $sline, $slen, $eline, $elen); 432 $listvar['rev1line'] = $sline; 433 $listvar['rev1len'] = $slen; 434 $listvar['rev2line'] = $eline; 435 $listvar['rev2len'] = $elen; 436 437 $indiffproper = true; 438 439 $index++; 440 } 441 442 continue; 443 444 } 445 else 446 { 447 $indiff = false; 448 if ($debug) print 'Ending diff'; 449 } 450 } 451 452 // Check for a new node entry 453 if (strncmp(trim($line), 'Index: ', 7) == 0) 454 { 455 // End the current node 456 if ($node) 457 { 458 $listing[$index++]['endpath'] = true; 459 clearVars(); 460 } 461 462 $node = trim(toOutputEncoding($line)); 463 $node = substr($node, 7); 464 if ($node == '' || $node[0] != '/') $node = '/'.$node; 465 466 if (substr($path2, -strlen($node)) === $node) 467 { 468 $absnode = $path2; 469 } 470 else 471 { 472 $absnode = $path2; 473 if (substr($absnode, -1) == '/') $absnode = substr($absnode, 0, -1); 474 $absnode .= $node; 475 } 476 477 $listvar = &$listing[$index]; 478 $listvar['newpath'] = escape($absnode); 479 480 $listvar['fileurl'] = $config->getURL($rep, escape($absnode), 'file').'rev='.$rev2; 481 482 if ($debug) echo 'Creating node '.$node.'<br />'; 483 484 // Skip past the line of ='s 485 $line = fgets($diff); 486 if ($debug) print 'Skipping: '.$line.'<br />'; 487 488 // Check for a file addition 489 $line = fgets($diff); 490 if ($debug) print 'Examining: '.$line.'<br />'; 491 if (strpos($line, '(revision 0)')) 492 { 493 $listvar['info'] = $lang['FILEADDED']; 494 } 495 496 if (strncmp(trim($line), 'Cannot display:', 15) == 0) 497 { 498 $index++; 499 clearVars(); 500 $listing[$index++]['info'] = escape(toOutputEncoding($line)); 501 continue; 502 } 503 504 // Skip second file info 505 $line = fgets($diff); 506 if ($debug) print 'Skipping: '.$line.'<br />'; 507 508 $indiff = true; 509 $index++; 510 511 continue; 512 } 513 514 if (strncmp(trim($line), 'Property changes on: ', 21) == 0) 515 { 516 $propnode = trim($line); 517 $propnode = substr($propnode, 21); 518 if ($propnode == '' || $propnode[0] != '/') $propnode = '/'.$propnode; 519 520 if ($debug) print 'Properties on '.$propnode.' (cur node $ '.$node.')'; 521 if ($propnode != $node) 522 { 523 if ($node) 524 { 525 $listing[$index++]['endpath'] = true; 526 clearVars(); 527 } 528 529 $node = $propnode; 530 531 $listing[$index++]['newpath'] = escape(toOutputEncoding($node)); 532 clearVars(); 533 } 534 535 $listing[$index++]['properties'] = true; 536 clearVars(); 537 if ($debug) echo 'Creating node '.$node.'<br />'; 538 539 // Skip the row of underscores 540 $line = fgets($diff); 541 if ($debug) print 'Skipping: '.$line.'<br />'; 542 543 while ($line = trim(fgets($diff))) 544 { 545 if (!strncmp($line, 'Index: ', 7)) 546 { 547 break; 548 } 549 if (!strncmp($line, '##', 2) || $line == '\ No newline at end of file') 550 { 551 continue; 552 } 553 $listing[$index++]['info'] = escape(toOutputEncoding($line)); 554 clearVars(); 555 } 556 $getLine = false; 557 558 continue; 559 } 560 561 // Check for error messages 562 if (strncmp(trim($line), 'svn: ', 5) == 0) 563 { 564 $listing[$index++]['info'] = urldecode($line); 565 $vars['success'] = false; 566 continue; 567 } 568 569 $listing[$index++]['info'] = escape(toOutputEncoding($line)); 570 571 if (strlen($line) === 0) 572 { 573 if (!isset($vars['warning'])) 574 { 575 $vars['warning'] = "No changes between revisions"; 576 } 577 } 578 579 } 580 581 if ($node) 582 { 583 clearVars(); 584 $listing[$index++]['endpath'] = true; 585 } 586 587 if ($debug) print_r($listing); 588 589 if (!$rep->hasUnrestrictedReadAccess($relativePath1) || !$rep->hasUnrestrictedReadAccess($relativePath2, false)) 590 { 591 // check every item for access and remove it if read access is not allowed 592 $restricted = array(); 593 $inrestricted = false; 594 foreach ($listing as $i => $item) 595 { 596 if ($item['newpath'] !== null) 597 { 598 $newpath = $item['newpath']; 599 $inrestricted = !$rep->hasReadAccess($newpath, false); 600 } 601 602 if ($inrestricted) 603 { 604 $restricted[] = $i; 605 } 606 607 if ($item['endpath'] !== null) 608 { 609 $inrestricted = false; 610 } 611 } 612 613 foreach ($restricted as $i) 614 { 615 unset($listing[$i]); 616 } 617 618 if (count($restricted) && !count($listing)) 619 { 620 $vars['error'] = $lang['NOACCESS']; 621 sendHeaderForbidden(); 622 } 623 } 624 625 pclose($diff); 626 } 627} 628 629renderTemplate('compare'); 630