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.'&amp;compare%5B%5D='.urlencode($path1).'@'.$rev1.'&amp;manualorder=1'.($ignoreWhitespace ? '&amp;ignorews=1' : '').'">'.$lang['REVCOMP'].'</a>';
104$toggleIgnoreWhitespace = '';
105
106if ($ignoreWhitespace == $config->getIgnoreWhitespacesInDiff())
107{
108	$toggleIgnoreWhitespace = '&amp;ignorews='.($ignoreWhitespace ? '0' : '1');
109}
110
111if (!$ignoreWhitespace)
112{
113	$vars['ignorewhitespacelink'] = '<a href="'.$url.'compare%5B%5D='.urlencode($path1).'@'.$rev1.'&amp;compare%5B%5D='.urlencode($path2).'@'.$rev2.($manualorder ? '&amp;manualorder=1' : '').$toggleIgnoreWhitespace.'">'.$lang['IGNOREWHITESPACE'].'</a>';
114}
115else
116{
117	$vars['regardwhitespacelink'] = '<a href="'.$url.'compare%5B%5D='.urlencode($path1).'@'.$rev1.'&amp;compare%5B%5D='.urlencode($path2).'@'.$rev2.($manualorder ? '&amp;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) : '&nbsp;';
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