1<?php 2 3namespace dokuwiki\Ui; 4 5use dokuwiki\ChangeLog\PageChangeLog; 6use dokuwiki\ChangeLog\MediaChangeLog; 7use dokuwiki\Extension\Event; 8use dokuwiki\Form\Form; 9 10/** 11 * DokuWiki Diff Insterface 12 * 13 * @package dokuwiki\Ui 14 */ 15class Diff extends Ui 16{ 17 /** 18 * Show diff 19 * between current page version and provided $text 20 * or between the revisions provided via GET or POST 21 * 22 * @author Andreas Gohr <andi@splitbrain.org> 23 * 24 * @param string $text when non-empty: compare with this text with most current version 25 * @param bool $intro display the intro text 26 * @param string $type type of the diff (inline or sidebyside) 27 * @return void 28 */ 29 public function show($text = '', $intro = true, $type = null) 30 { 31 global $ID; 32 global $REV; 33 global $lang; 34 global $INPUT; 35 global $INFO; 36 $pagelog = new PageChangeLog($ID); 37 38 /* 39 * Determine diff type 40 */ 41 if (!$type) { 42 $type = $INPUT->str('difftype'); 43 if (empty($type)) { 44 $type = get_doku_pref('difftype', $type); 45 if (empty($type) && $INFO['ismobile']) { 46 $type = 'inline'; 47 } 48 } 49 } 50 if ($type != 'inline') $type = 'sidebyside'; 51 52 /* 53 * Determine requested revision(s) 54 */ 55 // we're trying to be clever here, revisions to compare can be either 56 // given as rev and rev2 parameters, with rev2 being optional. Or in an 57 // array in rev2. 58 $rev1 = $REV; 59 60 $rev2 = $INPUT->ref('rev2'); 61 if (is_array($rev2)) { 62 $rev1 = (int) $rev2[0]; 63 $rev2 = (int) $rev2[1]; 64 65 if (!$rev1) { 66 $rev1 = $rev2; 67 unset($rev2); 68 } 69 } else { 70 $rev2 = $INPUT->int('rev2'); 71 } 72 73 /* 74 * Determine left and right revision, its texts and the header 75 */ 76 $r_minor = ''; 77 $l_minor = ''; 78 79 if ($text) { // compare text to the most current revision 80 $l_rev = ''; 81 $l_text = rawWiki($ID, ''); 82 $l_head = '<a class="wikilink1" href="'. wl($ID) .'">' 83 . $ID .' '. dformat((int) @filemtime(wikiFN($ID))) .'</a> ' 84 . $lang['current']; 85 86 $r_rev = ''; 87 $r_text = cleanText($text); 88 $r_head = $lang['yours']; 89 } else { 90 if ($rev1 && isset($rev2) && $rev2) { // two specific revisions wanted 91 // make sure order is correct (older on the left) 92 if ($rev1 < $rev2) { 93 $l_rev = $rev1; 94 $r_rev = $rev2; 95 } else { 96 $l_rev = $rev2; 97 $r_rev = $rev1; 98 } 99 } elseif ($rev1) { // single revision given, compare to current 100 $r_rev = ''; 101 $l_rev = $rev1; 102 } else { // no revision was given, compare previous to current 103 $r_rev = ''; 104 $revs = $pagelog->getRevisions(0, 1); 105 $l_rev = $revs[0]; 106 $REV = $l_rev; // store revision back in $REV 107 } 108 109 // when both revisions are empty then the page was created just now 110 if (!$l_rev && !$r_rev) { 111 $l_text = ''; 112 } else { 113 $l_text = rawWiki($ID, $l_rev); 114 } 115 $r_text = rawWiki($ID, $r_rev); 116 117 list($l_head, $r_head, $l_minor, $r_minor) = $this->diffHead( 118 $l_rev, $r_rev, null, false, $type == 'inline' 119 ); 120 } 121 122 /* 123 * Build navigation 124 */ 125 $l_nav = ''; 126 $r_nav = ''; 127 if (!$text) { 128 list($l_nav, $r_nav) = $this->diffNavigation($pagelog, $type, $l_rev, $r_rev); 129 } 130 /* 131 * Create diff object and the formatter 132 */ 133 $diff = new \Diff(explode("\n", $l_text), explode("\n", $r_text)); 134 135 if ($type == 'inline') { 136 $diffformatter = new \InlineDiffFormatter(); 137 } else { 138 $diffformatter = new \TableDiffFormatter(); 139 } 140 /* 141 * Display intro 142 */ 143 if ($intro) print p_locale_xhtml('diff'); 144 145 /* 146 * Display type and exact reference 147 */ 148 if (!$text) { 149 print '<div class="diffoptions group">'; 150 151 // create the form to select diff view type 152 $form = new Form(['action' => wl()]); 153 $form->setHiddenField('id', $ID); 154 $form->setHiddenField('rev2[0]', $l_rev); 155 $form->setHiddenField('rev2[1]', $r_rev); 156 $form->setHiddenField('do', 'diff'); 157 $options = array( 158 'sidebyside' => $lang['diff_side'], 159 'inline' => $lang['diff_inline'] 160 ); 161 $input = $form->addDropdown('difftype', $options, $lang['diff_type']) 162 ->val($type)->addClass('quickselect'); 163 $input->useInput(false); // inhibit prefillInput() during toHTML() process 164 $form->addButton('do[diff]', 'Go')->attr('type','submit'); 165 print $form->toHTML(); 166 167 print '<p>'; 168 // link to exactly this view FS#2835 169 print $this->diffViewlink($type, 'difflink', $l_rev, $r_rev ? $r_rev : $INFO['currentrev']); 170 print '</p>'; 171 172 print '</div>'; // .diffoptions 173 } 174 175 /* 176 * Display diff view table 177 */ 178 print '<div class="table">'; 179 print '<table class="diff diff_'. $type .'">'; 180 181 //navigation and header 182 if ($type == 'inline') { 183 if (!$text) { 184 print '<tr>' 185 . '<td class="diff-lineheader">-</td>' 186 . '<td class="diffnav">'. $l_nav .'</td>' 187 . '</tr>'; 188 print '<tr>' 189 . '<th class="diff-lineheader">-</th>' 190 . '<th '. $l_minor .'>'. $l_head .'</th>' 191 .'</tr>'; 192 } 193 print '<tr>' 194 . '<td class="diff-lineheader">+</td>' 195 . '<td class="diffnav">'. $r_nav .'</td>' 196 .'</tr>'; 197 print '<tr>' 198 . '<th class="diff-lineheader">+</th>' 199 . '<th '. $r_minor .'>'. $r_head .'</th>' 200 . '</tr>'; 201 } else { 202 if (!$text) { 203 print '<tr>' 204 . '<td colspan="2" class="diffnav">'. $l_nav .'</td>' 205 . '<td colspan="2" class="diffnav">'. $r_nav .'</td>' 206 . '</tr>'; 207 } 208 print '<tr>' 209 . '<th colspan="2" '. $l_minor .'>'. $l_head .'</th>' 210 . '<th colspan="2" '. $r_minor .'>'. $r_head .'</th>' 211 . '</tr>'; 212 } 213 214 //diff view 215 print html_insert_softbreaks($diffformatter->format($diff)); 216 217 print '</table>'; 218 print '</div>'. DOKU_LF; 219 } 220 221 222 /** 223 * Get header of diff HTML 224 * 225 * @param string $l_rev Left revisions 226 * @param string $r_rev Right revision 227 * @param string $id Page id, if null $ID is used 228 * @param bool $media If it is for media files 229 * @param bool $inline Return the header on a single line 230 * @return string[] HTML snippets for diff header 231 */ 232 protected function diffHead($l_rev, $r_rev, $id = null, $media = false, $inline = false) 233 { 234 global $lang; 235 if ($id === null) { 236 global $ID; 237 $id = $ID; 238 } 239 $head_separator = $inline ? ' ' : '<br />'; 240 $media_or_wikiFN = $media ? 'mediaFN' : 'wikiFN'; 241 $ml_or_wl = $media ? 'ml' : 'wl'; 242 $l_minor = $r_minor = ''; 243 244 if ($media) { 245 $changelog = new MediaChangeLog($id); 246 } else { 247 $changelog = new PageChangeLog($id); 248 } 249 if (!$l_rev) { 250 $l_head = '—'; 251 } else { 252 $l_info = $changelog->getRevisionInfo($l_rev); 253 if ($l_info['user']) { 254 $l_user = '<bdi>'.editorinfo($l_info['user']).'</bdi>'; 255 if (auth_ismanager()) $l_user .= ' <bdo dir="ltr">('.$l_info['ip'].')</bdo>'; 256 } else { 257 $l_user = '<bdo dir="ltr">'.$l_info['ip'].'</bdo>'; 258 } 259 $l_user = '<span class="user">'.$l_user.'</span>'; 260 $l_sum = ($l_info['sum']) ? '<span class="sum"><bdi>'.hsc($l_info['sum']).'</bdi></span>' : ''; 261 if ($l_info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) $l_minor = 'class="minor"'; 262 263 $l_head_title = ($media) ? dformat($l_rev) : $id.' ['.dformat($l_rev).']'; 264 $l_head = '<bdi><a class="wikilink1" href="'.$ml_or_wl($id,"rev=$l_rev").'">' 265 . $l_head_title.'</a></bdi>'.$head_separator.$l_user.' '.$l_sum; 266 } 267 268 if ($r_rev) { 269 $r_info = $changelog->getRevisionInfo($r_rev); 270 if ($r_info['user']) { 271 $r_user = '<bdi>'.editorinfo($r_info['user']).'</bdi>'; 272 if (auth_ismanager()) $r_user .= ' <bdo dir="ltr">('.$r_info['ip'].')</bdo>'; 273 } else { 274 $r_user = '<bdo dir="ltr">'.$r_info['ip'].'</bdo>'; 275 } 276 $r_user = '<span class="user">'.$r_user.'</span>'; 277 $r_sum = ($r_info['sum']) ? '<span class="sum"><bdi>'.hsc($r_info['sum']).'</bdi></span>' : ''; 278 if ($r_info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) $r_minor = 'class="minor"'; 279 280 $r_head_title = ($media) ? dformat($r_rev) : $id.' ['.dformat($r_rev).']'; 281 $r_head = '<bdi><a class="wikilink1" href="'.$ml_or_wl($id,"rev=$r_rev").'">' 282 . $r_head_title.'</a></bdi>'.$head_separator.$r_user.' '.$r_sum; 283 } elseif ($_rev = @filemtime($media_or_wikiFN($id))) { 284 $_info = $changelog->getRevisionInfo($_rev); 285 if ($_info['user']) { 286 $_user = '<bdi>'.editorinfo($_info['user']).'</bdi>'; 287 if (auth_ismanager()) $_user .= ' <bdo dir="ltr">('.$_info['ip'].')</bdo>'; 288 } else { 289 $_user = '<bdo dir="ltr">'.$_info['ip'].'</bdo>'; 290 } 291 $_user = '<span class="user">'.$_user.'</span>'; 292 $_sum = ($_info['sum']) ? '<span class="sum"><bdi>'.hsc($_info['sum']).'</span></bdi>' : ''; 293 if ($_info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) $r_minor = 'class="minor"'; 294 295 $r_head_title = ($media) ? dformat($_rev) : $id.' ['.dformat($_rev).']'; 296 $r_head = '<bdi><a class="wikilink1" href="'.$ml_or_wl($id).'">' 297 . $r_head_title.'</a></bdi> '.'('.$lang['current'].')'.$head_separator.$_user.' '.$_sum; 298 }else{ 299 $r_head = '— ('.$lang['current'].')'; 300 } 301 302 return array($l_head, $r_head, $l_minor, $r_minor); 303 } 304 305 /** 306 * Create html for revision navigation 307 * 308 * @param PageChangeLog $pagelog changelog object of current page 309 * @param string $type inline vs sidebyside 310 * @param int $l_rev left revision timestamp 311 * @param int $r_rev right revision timestamp 312 * @return string[] html of left and right navigation elements 313 */ 314 protected function diffNavigation($pagelog, $type, $l_rev, $r_rev) 315 { 316 global $INFO, $ID; 317 318 // last timestamp is not in changelog, retrieve timestamp from metadata 319 // note: when page is removed, the metadata timestamp is zero 320 if (!$r_rev) { 321 if (isset($INFO['meta']['last_change']['date'])) { 322 $r_rev = $INFO['meta']['last_change']['date']; 323 } else { 324 $r_rev = 0; 325 } 326 } 327 328 //retrieve revisions with additional info 329 list($l_revs, $r_revs) = $pagelog->getRevisionsAround($l_rev, $r_rev); 330 $l_revisions = array(); 331 if (!$l_rev) { 332 //no left revision given, add dummy 333 $l_revisions[0]= array('label' => '', 'attrs' => []); 334 } 335 foreach ($l_revs as $rev) { 336 $info = $pagelog->getRevisionInfo($rev); 337 $l_revisions[$rev] = array( 338 'label' => dformat($info['date']) .' '. editorinfo($info['user'], true) .' '. $info['sum'], 339 'attrs' => ['title' => $rev], 340 ); 341 if ($r_rev ? $rev >= $r_rev : false) $l_revisions[$rev]['attrs']['disabled'] = 'disabled'; 342 } 343 $r_revisions = array(); 344 if (!$r_rev) { 345 //no right revision given, add dummy 346 $r_revisions[0] = array('label' => '', 'attrs' => []); 347 } 348 foreach ($r_revs as $rev) { 349 $info = $pagelog->getRevisionInfo($rev); 350 $r_revisions[$rev] = array( 351 'label' => dformat($info['date']) .' '. editorinfo($info['user'], true) .' '. $info['sum'], 352 'attrs' => ['title' => $rev], 353 ); 354 if ($rev <= $l_rev) $r_revisions[$rev]['attrs']['disabled'] = 'disabled'; 355 } 356 357 //determine previous/next revisions 358 $l_index = array_search($l_rev, $l_revs); 359 $l_prev = $l_revs[$l_index + 1]; 360 $l_next = $l_revs[$l_index - 1]; 361 if ($r_rev) { 362 $r_index = array_search($r_rev, $r_revs); 363 $r_prev = $r_revs[$r_index + 1]; 364 $r_next = $r_revs[$r_index - 1]; 365 } else { 366 //removed page 367 if ($l_next) { 368 $r_prev = $r_revs[0]; 369 } else { 370 $r_prev = null; 371 } 372 $r_next = null; 373 } 374 375 /* 376 * Left side: 377 */ 378 $l_nav = ''; 379 //move back 380 if ($l_prev) { 381 $l_nav .= $this->diffViewlink($type, 'diffbothprevrev', $l_prev, $r_prev); 382 $l_nav .= $this->diffViewlink($type, 'diffprevrev', $l_prev, $r_rev); 383 } 384 //dropdown 385 $form = new Form(['action' => wl()]); 386 $form->setHiddenField('id', $ID); 387 $form->setHiddenField('difftype', $type); 388 $form->setHiddenField('rev2[1]', $r_rev); 389 $form->setHiddenField('do', 'diff'); 390 $input = $form->addDropdown('rev2[0]', $l_revisions)->val($l_rev)->addClass('quickselect'); 391 $input->useInput(false); // inhibit prefillInput() during toHTML() process 392 $form->addButton('do[diff]', 'Go')->attr('type','submit'); 393 $l_nav .= $form->toHTML(); 394 //move forward 395 if ($l_next && ($l_next < $r_rev || !$r_rev)) { 396 $l_nav .= $this->diffViewlink($type, 'diffnextrev', $l_next, $r_rev); 397 } 398 399 /* 400 * Right side: 401 */ 402 $r_nav = ''; 403 //move back 404 if ($l_rev < $r_prev) { 405 $r_nav .= $this->diffViewlink($type, 'diffprevrev', $l_rev, $r_prev); 406 } 407 //dropdown 408 $form = new Form(['action' => wl()]); 409 $form->setHiddenField('id', $ID); 410 $form->setHiddenField('rev2[0]', $l_rev); 411 $form->setHiddenField('difftype', $type); 412 $form->setHiddenField('do', 'diff'); 413 $input = $form->addDropdown('rev2[1]', $r_revisions)->val($r_rev)->addClass('quickselect'); 414 $input->useInput(false); // inhibit prefillInput() during toHTML() process 415 $form->addButton('do[diff]', 'Go')->attr('type','submit'); 416 $r_nav .= $form->toHTML(); 417 //move forward 418 if ($r_next) { 419 if ($pagelog->isCurrentRevision($r_next)) { 420 //last revision is diff with current page 421 $r_nav .= $this->diffViewlink($type, 'difflastrev', $l_rev); 422 } else { 423 $r_nav .= $this->diffViewlink($type, 'diffnextrev', $l_rev, $r_next); 424 } 425 } else { 426 $r_nav .= $this->diffViewlink($type, 'diffbothnextrev', $l_next, $r_next); 427 } 428 return array($l_nav, $r_nav); 429 } 430 431 /** 432 * Create html link to a diff view defined by two revisions 433 * 434 * @param string $difftype display type 435 * @param string $linktype 436 * @param int $lrev oldest revision 437 * @param int $rrev newest revision or null for diff with current revision 438 * @return string html of link to a diff view 439 */ 440 protected function diffViewlink($difftype, $linktype, $lrev, $rrev = null) 441 { 442 global $ID, $lang; 443 if (!$rrev) { 444 $urlparam = array( 445 'do' => 'diff', 446 'rev' => $lrev, 447 'difftype' => $difftype, 448 ); 449 } else { 450 $urlparam = array( 451 'do' => 'diff', 452 'rev2[0]' => $lrev, 453 'rev2[1]' => $rrev, 454 'difftype' => $difftype, 455 ); 456 } 457 return '<a class="'. $linktype .'" href="'. wl($ID, $urlparam) .'" title="'. $lang[$linktype] .'">' 458 . '<span>'. $lang[$linktype] .'</span>' 459 . '</a>'. DOKU_LF; 460 } 461 462} 463