Rev Author Line No. Line
185 miho 1 <?php
2 // WebSVN - Subversion repository viewing via the web using PHP
4988 kaklik 3 // Copyright (C) 2004-2006 Tim Armes
185 miho 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
4988 kaklik 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
185 miho 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
4988 kaklik 17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
185 miho 18 //
19 // --
20 //
21 // log.php
22 //
23 // Show the logs for the given path
24  
4988 kaklik 25 require_once 'include/setup.php';
26 require_once 'include/svnlook.php';
27 require_once 'include/utils.php';
28 require_once 'include/template.php';
29 require_once 'include/bugtraq.php';
185 miho 30  
4988 kaklik 31 $page = (int)@$_REQUEST['page'];
32 $all = @$_REQUEST['all'] == 1;
33 $isDir = @$_REQUEST['isdir'] == 1 || $path == '' || $path == '/';
34  
35 // Make sure that we have a repository
36 if (!$rep)
37 {
38 renderTemplate404('log','NOREP');
39 }
40  
41 if (isset($_REQUEST['showchanges']))
42 {
43 $showchanges = @$_REQUEST['showchanges'] == 1;
44 }
45 else
46 {
47 $showchanges = $rep->logsShowChanges();
48 }
49  
50 $search = trim((string) @$_REQUEST['search']);
51 $dosearch = strlen($search) > 0;
52  
185 miho 53 $words = preg_split('#\s+#', $search);
4988 kaklik 54 $fromRev = (int)@$_REQUEST['fr'];
55 $startrev = strtoupper(trim((string) @$_REQUEST['sr']));
56 $endrev = strtoupper(trim((string) @$_REQUEST['er']));
57 $max = isset($_REQUEST['max']) ? (int)$_REQUEST['max'] : false;
185 miho 58  
59 // Max number of results to find at a time
4988 kaklik 60 $numSearchResults = 20;
185 miho 61  
4988 kaklik 62 if ($search == '')
63 {
64 $dosearch = false;
65 }
185 miho 66  
67 // removeAccents
68 //
4988 kaklik 69 // Remove all the accents from a string. This function doesn't seem
185 miho 70 // ideal, but expecting everyone to install 'unac' seems a little
71 // excessive as well...
72  
4988 kaklik 73 function removeAccents($string)
74 {
75 $string = htmlentities($string, ENT_QUOTES, 'ISO-8859-1');
76 $string = preg_replace('/&([A-Za-z])(acute|uml|circ|grave|ring|cedil|slash|tilde|caron);/', '$1', $string);
77 return $string;
78 }
185 miho 79  
80 // Normalise the search words
4988 kaklik 81 foreach ($words as $index => $word)
185 miho 82 {
4988 kaklik 83 $words[$index] = strtolower(removeAccents($word));
84  
85 // Remove empty string introduced by multiple spaces
86 if (empty($words[$index]))
87 unset($words[$index]);
185 miho 88 }
89  
4988 kaklik 90 if (empty($page))
91 {
92 $page = 1;
93 }
185 miho 94  
95 // If searching, display all the results
4988 kaklik 96 if ($dosearch)
97 {
98 $all = true;
99 }
185 miho 100  
101 $maxperpage = 20;
102  
4988 kaklik 103 $svnrep = new SVNRepository($rep);
104  
105 $history = $svnrep->getLog($path, 'HEAD', '', false, 1, ($path == '/') ? '' : $peg);
106  
107 if (!$history)
185 miho 108 {
4988 kaklik 109 unset($vars['error']);
110 $history = $svnrep->getLog($path, '', '', false, 1, ($path == '/') ? '' : $peg);
111  
112 if (!$history)
113 {
114 renderTemplate404('log','NOPATH');
115 }
185 miho 116 }
117  
4988 kaklik 118 $youngest = ($history && isset($history->entries[0])) ? $history->entries[0]->rev : 0;
185 miho 119  
4988 kaklik 120 if (empty($startrev))
121 {
122 //$startrev = ($rev) ? $rev : 'HEAD';
123 $startrev = $rev;
124 }
125 else if ($startrev != 'HEAD' && $startrev != 'BASE' && $startrev != 'PREV' && $startrev != 'COMMITTED')
126 {
127 $startrev = (int)$startrev;
128 }
185 miho 129  
4988 kaklik 130 if (empty($endrev))
131 {
132 $endrev = 1;
133 }
134 else if ($endrev != 'HEAD' && $endrev != 'BASE' && $endrev != 'PREV' && $endrev != 'COMMITTED')
135 {
136 $endrev = (int)$endrev;
137 }
185 miho 138  
4988 kaklik 139 if (empty($rev))
140 {
141 $rev = $youngest;
142 }
185 miho 143  
4988 kaklik 144 if (empty($startrev))
145 {
146 $startrev = $rev;
147 }
148  
185 miho 149 // make sure path is prefixed by a /
150 $ppath = $path;
151  
4988 kaklik 152 if ($path == '' || $path[0] != '/')
153 {
154 $ppath = '/'.$path;
155 }
185 miho 156  
4988 kaklik 157 $vars['action'] = $lang['LOG'];
158 $vars['rev'] = $rev;
159 $vars['peg'] = $peg;
160 $vars['path'] = str_replace('%2F', '/', rawurlencode($ppath));
161 $vars['safepath'] = escape($ppath);
185 miho 162  
4988 kaklik 163 if ($history && isset($history->entries[0]))
164 {
165 $vars['log'] = xml_entities($history->entries[0]->msg);
166 $vars['date'] = $history->entries[0]->date;
167 $vars['age'] = datetimeFormatDuration(time() - strtotime($history->entries[0]->date));
168 $vars['author'] = $history->entries[0]->author;
169 }
185 miho 170  
4988 kaklik 171 if ($max === false)
172 {
173 $max = ($dosearch) ? 0 : 40;
174 }
175 else if ($max < 0)
176 {
177 $max = 40;
178 }
179  
180 // TODO: If the rev is less than the head, get the path (may have been renamed!)
181 // Will probably need to call `svn info`, parse XML output, and substring a path
182  
183 createPathLinks($rep, $ppath, $passrev, $peg);
184 $passRevString = createRevAndPegString($rev, $peg);
185 $isDirString = ($isDir) ? 'isdir=1&amp;' : '';
186  
187 unset($queryParams['repname']);
188 unset($queryParams['path']);
189  
190 // Toggle 'showchanges' param for link to switch from the current behavior
191 if ($showchanges == $rep->logsShowChanges())
192 {
193 $queryParams['showchanges'] = (int)!$showchanges;
194 }
185 miho 195 else
4988 kaklik 196 {
197 unset($queryParams['showchanges']);
198 }
185 miho 199  
4988 kaklik 200 $vars['changesurl'] = $config->getURL($rep, $path, 'log').buildQuery($queryParams);
201 $vars['changeslink'] = '<a href="'.$vars['changesurl'].'">'.$lang[($showchanges ? 'HIDECHANGED' : 'SHOWCHANGED')].'</a>';
202 $vars['showchanges'] = $showchanges;
203  
204 // Revert 'showchanges' param to propagate the current behavior
205 if ($showchanges == $rep->logsShowChanges())
206 {
207 unset($queryParams['showchanges']);
208 }
209 else
210 {
211 $queryParams['showchanges'] = (int)$showchanges;
212 }
213  
214 $vars['revurl'] = $config->getURL($rep, $path, 'revision').$isDirString.$passRevString;
215  
216 if ($isDir)
217 {
218 $vars['directoryurl'] = $config->getURL($rep, $path, 'dir').$passRevString.'#'.anchorForPath($path);
219 $vars['directorylink'] = '<a href="'.$vars['directoryurl'].'">'.$lang['LISTING'].'</a>';
220 }
221 else
222 {
223 $vars['filedetailurl'] = $config->getURL($rep, $path, 'file').$passRevString;
224 $vars['filedetaillink'] = '<a href="'.$vars['filedetailurl'].'">'.$lang['FILEDETAIL'].'</a>';
225  
226 $vars['blameurl'] = $config->getURL($rep, $path, 'blame').$passRevString;
227 $vars['blamelink'] = '<a href="'.$vars['blameurl'].'">'.$lang['BLAME'].'</a>';
228  
229 $vars['diffurl'] = $config->getURL($rep, $path, 'diff').$passRevString;
230 $vars['difflink'] = '<a href="'.$vars['diffurl'].'">'.$lang['DIFFPREV'].'</a>';
231 }
232  
233 if ($rep->isRssEnabled())
234 {
235 $vars['rssurl'] = $config->getURL($rep, $path, 'rss').$isDirString.createRevAndPegString('', $peg);
236 $vars['rsslink'] = '<a href="'.$vars['rssurl'].'">'.$lang['RSSFEED'].'</a>';
237 }
238  
239 if ($rev != $youngest)
240 {
241 if ($path == '/')
242 {
243 $vars['goyoungesturl'] = $config->getURL($rep, '', 'log').$isDirString;
244 }
245 else
246 {
247 $vars['goyoungesturl'] = $config->getURL($rep, $path, 'log').$isDirString.'peg='.($peg ? $peg : $rev);
248 }
249  
250 $vars['goyoungestlink'] = '<a href="'.$vars['goyoungesturl'].'"'.($youngest ? ' title="'.$lang['REV'].' '.$youngest.'"' : '').'>'.$lang['GOYOUNGEST'].'</a>';
251 }
252  
185 miho 253 // We get the bugtraq variable just once based on the HEAD
254 $bugtraq = new Bugtraq($rep, $svnrep, $ppath);
255  
4988 kaklik 256 $vars['logsearch_moreresultslink'] = '';
257 $vars['pagelinks'] = '';
258 $vars['showalllink'] = '';
185 miho 259  
4988 kaklik 260 if ($history)
185 miho 261 {
4988 kaklik 262 $history = $svnrep->getLog($path, $startrev, $endrev, true, $max, $peg);
263  
264 if (empty($history))
265 {
266 unset($vars['error']);
267 $vars['warning'] = 'Revision '.$startrev.' of this resource does not exist.';
268 }
185 miho 269 }
4988 kaklik 270  
271 if (!empty($history))
185 miho 272 {
4988 kaklik 273 // Get the number of separate revisions
274 $revisions = count($history->entries);
275  
276 if ($all)
277 {
278 $firstrevindex = 0;
279 $lastrevindex = $revisions - 1;
280 $pages = 1;
281 }
282 else
283 {
284 // Calculate the number of pages
285 $pages = floor($revisions / $maxperpage);
286 if (($revisions % $maxperpage) > 0) $pages++;
287  
288 if ($page > $pages) $page = $pages;
289  
290 // Work out where to start and stop
291 $firstrevindex = ($page - 1) * $maxperpage;
292 $lastrevindex = min($firstrevindex + $maxperpage - 1, $revisions - 1);
293 }
294  
295 $frev = isset($history->entries[0]) ? $history->entries[0]->rev : false;
296 $brev = isset($history->entries[$firstrevindex]) ? $history->entries[$firstrevindex]->rev : false;
297 $erev = isset($history->entries[$lastrevindex]) ? $history->entries[$lastrevindex]->rev : false;
298  
299 $entries = array();
300  
301 if ($brev && $erev)
302 {
303 $history = $svnrep->getLog($path, $brev, $erev, false, 0, $peg, true);
304 if ($history)
305 {
306 $entries = $history->entries;
307 }
308 }
309  
310 $row = 0;
311 $index = 0;
312 $found = false;
313  
314 foreach ($entries as $revision)
315 {
316 // Assume a good match
317 $match = true;
318 $thisrev = $revision->rev;
319  
320 // Check the log for the search words, if searching
321 if ($dosearch)
322 {
323 if ((empty($fromRev) || $fromRev > $thisrev))
324 {
325 // Turn all the HTML entities into real characters.
326  
327 // Make sure that each word in the search in also in the log
328 foreach ($words as $word)
329 {
330 if (strpos(strtolower(removeAccents($revision->msg)), $word) === false && strpos(strtolower(removeAccents($revision->author)), $word) === false)
331 {
332 $match = false;
333 break;
334 }
335 }
336  
337 if ($match)
338 {
339 $numSearchResults--;
340 $found = true;
341 }
342 }
343 else
344 {
345 $match = false;
346 }
347 }
348  
349 $thisRevString = createRevAndPegString($thisrev, ($peg ? $peg : $thisrev));
350  
351 if ($match)
352 {
353 // Add the trailing slash if we need to (svnlook history doesn't return trailing slashes!)
354 $rpath = $revision->path;
355  
356 if (empty($rpath))
357 {
358 $rpath = '/';
359 }
360 else if ($isDir && $rpath[strlen($rpath) - 1] != '/')
361 {
362 $rpath .= '/';
363 }
364  
365 $precisePath = $revision->precisePath;
366 if (empty($precisePath))
367 {
368 $precisePath = '/';
369 }
370 else if ($isDir && $precisePath[strlen($precisePath) - 1] != '/')
371 {
372 $precisePath .= '/';
373 }
374  
375 // Find the parent path (or the whole path if it's already a directory)
376 $pos = strrpos($rpath, '/');
377 $parent = substr($rpath, 0, $pos + 1);
378  
379 $compareValue = (($isDir) ? $parent : $rpath).'@'.$thisrev;
380  
381 $listvar = &$listing[$index];
382 $listvar['compare_box'] = '<input type="checkbox" name="compare[]" value="'.$compareValue.'" onclick="enforceOnlyTwoChecked(this)" />';
383 $url = $config->getURL($rep, $rpath, 'revision').$thisRevString;
384 $listvar['revlink'] = '<a href="'.$url.'">'.$thisrev.'</a>';
385  
386 $url = $config->getURL($rep, $precisePath, ($isDir ? 'dir' : 'file')).$thisRevString;
387  
388 $listvar['revpathlink'] = '<a href="'.$url.'">'.escape($precisePath).'</a>';
389 $listvar['revpath'] = escape($precisePath);
390 $listvar['revauthor'] = $revision->author;
391 $listvar['revdate'] = $revision->date;
392 $listvar['revage'] = $revision->age;
393 $listvar['revlog'] = nl2br($bugtraq->replaceIDs(create_anchors(xml_entities($revision->msg))));
394 $listvar['rowparity'] = $row;
395 $listvar['compareurl'] = $config->getURL($rep, '', 'comp').'compare[]='.urlencode($rpath).'@'.($thisrev - 1).'&amp;compare[]='.urlencode($rpath).'@'.$thisrev;
396  
397 if ($showchanges)
398 {
399 // Aggregate added/deleted/modified paths for display in table
400 $modpaths = array();
401  
402 foreach ($revision->mods as $mod)
403 {
404 $modpaths[$mod->action][] = $mod->path;
405 }
406  
407 ksort($modpaths);
408  
409 foreach ($modpaths as $action => $paths)
410 {
411 sort($paths);
412 $modpaths[$action] = $paths;
413 }
414  
415 $listvar['revadded'] = (isset($modpaths['A'])) ? implode('<br/>', escape($modpaths['A'])) : '';
416 $listvar['revdeleted'] = (isset($modpaths['D'])) ? implode('<br/>', escape($modpaths['D'])) : '';
417 $listvar['revmodified'] = (isset($modpaths['M'])) ? implode('<br/>', escape($modpaths['M'])) : '';
418 }
419  
420 $row = 1 - $row;
421 $index++;
422 }
423  
424 // If we've reached the search limit, stop here...
425 if (!$numSearchResults)
426 {
427 $url = $config->getURL($rep, $path, 'log').$isDirString.$thisRevString;
428 $vars['logsearch_moreresultslink'] = '<a href="'.$url.'&amp;search='.$search.'&amp;fr='.$thisrev.'">'.$lang['MORERESULTS'].'</a>';
429 break;
430 }
431 }
432  
433 $vars['logsearch_resultsfound'] = true;
434  
435 if ($dosearch && !$found)
436 {
437 if ($fromRev == 0)
438 {
439 $vars['logsearch_nomatches'] = true;
440 $vars['logsearch_resultsfound'] = false;
441 }
442 else
443 {
444 $vars['logsearch_nomorematches'] = true;
445 }
446 }
447 else if ($dosearch && $numSearchResults > 0)
448 {
449 $vars['logsearch_nomorematches'] = true;
450 }
451  
452 // Work out the paging options, create links to pages of results
453 if ($pages > 1)
454 {
455 $prev = $page - 1;
456 $next = $page + 1;
457  
458 unset($queryParams['page']);
459 $logurl = $config->getURL($rep, $path, 'log').buildQuery($queryParams);
460  
461 if ($page > 1)
462 {
463 $vars['pagelinks'] .= '<a href="'.$logurl.(!$peg && $frev && $prev != 1 ? '&amp;peg='.$frev : '').'&amp;page='.$prev.'">&larr;'.$lang['PREV'].'</a>';
464 }
465 else
466 {
467 $vars['pagelinks'] .= '<span>&larr;'.$lang['PREV'].'</span>';
468 }
469  
470 for ($p = 1; $p <= $pages; $p++)
471 {
472 if ($p != $page)
473 {
474 $vars['pagelinks'] .= '<a href="'.$logurl.(!$peg && $frev && $p != 1 ? '&amp;peg='.$frev : '').'&amp;page='.$p.'">'.$p.'</a>';
475 }
476 else
477 {
478 $vars['pagelinks'] .= '<span id="curpage">'.$p.'</span>';
479 }
480 }
481  
482 if ($page < $pages)
483 {
484 $vars['pagelinks'] .= '<a href="'.$logurl.(!$peg && $frev ? '&amp;peg='.$frev : '').'&amp;page='.$next.'">'.$lang['NEXT'].'&rarr;</a>';
485 }
486 else
487 {
488 $vars['pagelinks'] .= '<span>'.$lang['NEXT'].'&rarr;</span>';
489 }
490  
491 $vars['showalllink'] = '<a href="'.$logurl.'&amp;all=1">'.$lang['SHOWALL'].'</a>';
492 }
185 miho 493 }
494  
4988 kaklik 495 // Create form elements for filtering and searching log messages
496 if ($config->multiViews)
497 {
498 $hidden = '<input type="hidden" name="op" value="log" />';
499 }
500 else
501 {
502 $hidden = '<input type="hidden" name="repname" value="'.$repname.'" />';
503 $hidden .= '<input type="hidden" name="path" value="'.$path.'" />';
504 }
185 miho 505  
4988 kaklik 506 if ($isDir)
185 miho 507 {
4988 kaklik 508 $hidden .= '<input type="hidden" name="isdir" value="'.$isDir.'" />';
185 miho 509 }
510  
4988 kaklik 511 if ($peg)
512 {
513 $hidden .= '<input type="hidden" name="peg" value="'.$peg.'" />';
514 }
185 miho 515  
4988 kaklik 516 if ($showchanges != $rep->logsShowChanges())
517 {
518 $hidden .= '<input type="hidden" name="showchanges" value="'.$showchanges.'" />';
519 }
185 miho 520  
4988 kaklik 521 $vars['logsearch_form'] = '<form method="get" action="'.$config->getURL($rep, $path, 'log').'" id="search">'.$hidden;
522 $vars['logsearch_startbox'] = '<input name="sr" size="5" value="'.$startrev.'" />';
523 $vars['logsearch_endbox'] = '<input name="er" size="5" value="'.$endrev.'" />';
524 $vars['logsearch_maxbox'] = '<input name="max" size="5" value="'.($max == 0 ? 40 : $max).'" />';
525 $vars['logsearch_inputbox'] = '<input name="search" value="'.escape($search).'" />';
526 $vars['logsearch_showall'] = '<input type="checkbox" name="all" value="1"'.($all ? ' checked="checked"' : '').' />';
527 $vars['logsearch_submit'] = '<input type="submit" value="'.$lang['GO'].'" />';
528 $vars['logsearch_endform'] = '</form>';
185 miho 529  
4988 kaklik 530 // If a filter is in place, produce a link to clear all filter parameters
531 if ($page !== 1 || $all || $dosearch || $fromRev || $startrev !== $rev || $endrev !== 1 || $max !== 40)
532 {
533 $url = $config->getURL($rep, $path, 'log').$isDirString.$passRevString;
534 $vars['logsearch_clearloglink'] = '<a href="'.$url.'">'.$lang['CLEARLOG'].'</a>';
535 }
185 miho 536  
4988 kaklik 537 // Create form elements for comparing selected revisions
538 $vars['compare_form'] = '<form method="get" action="'.$config->getURL($rep, '', 'comp').'" id="compare">';
185 miho 539  
4988 kaklik 540 if ($config->multiViews)
541 {
542 $vars['compare_form'] .= '<input type="hidden" name="op" value="comp" />';
543 }
544 else
545 {
546 $vars['compare_form'] .= '<input type="hidden" name="repname" value="'.$repname.'" />';
547 }
185 miho 548  
4988 kaklik 549 $vars['compare_submit'] = '<input type="submit" value="'.$lang['COMPAREREVS'].'" />';
550 $vars['compare_endform'] = '</form>';
185 miho 551  
4988 kaklik 552 if (!$rep->hasReadAccess($path, false))
553 {
554 $vars['error'] = $lang['NOACCESS'];
555 sendHeaderForbidden();
556 }
185 miho 557  
4988 kaklik 558 renderTemplate('log');