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 $form->addButton('do[diff]', 'Go')->attr('type','submit'); 164 print $form->toHTML(); 165 166 print '<p>'; 167 // link to exactly this view FS#2835 168 print $this->diffNavigationlink($type, 'difflink', $l_rev, $r_rev ? $r_rev : $INFO['currentrev']); 169 print '</p>'; 170 171 print '</div>'; // .diffoptions 172 } 173 174 /* 175 * Display diff view table 176 */ 177 print '<div class="table">'; 178 print '<table class="diff diff_'. $type .'">'; 179 180 //navigation and header 181 if ($type == 'inline') { 182 if (!$text) { 183 print '<tr>' 184 . '<td class="diff-lineheader">-</td>' 185 . '<td class="diffnav">'. $l_nav .'</td>' 186 . '</tr>'; 187 print '<tr>' 188 . '<th class="diff-lineheader">-</th>' 189 . '<th '. $l_minor .'>'. $l_head .'</th>' 190 .'</tr>'; 191 } 192 print '<tr>' 193 . '<td class="diff-lineheader">+</td>' 194 . '<td class="diffnav">'. $r_nav .'</td>' 195 .'</tr>'; 196 print '<tr>' 197 . '<th class="diff-lineheader">+</th>' 198 . '<th '. $r_minor .'>'. $r_head .'</th>' 199 . '</tr>'; 200 } else { 201 if (!$text) { 202 print '<tr>' 203 . '<td colspan="2" class="diffnav">'. $l_nav .'</td>' 204 . '<td colspan="2" class="diffnav">'. $r_nav .'</td>' 205 . '</tr>'; 206 } 207 print '<tr>' 208 . '<th colspan="2" '. $l_minor .'>'. $l_head .'</th>' 209 . '<th colspan="2" '. $r_minor .'>'. $r_head .'</th>' 210 . '</tr>'; 211 } 212 213 //diff view 214 print html_insert_softbreaks($diffformatter->format($diff)); 215 216 print '</table>'; 217 print '</div>'. DOKU_LF; 218 } 219 220 221 /** 222 * Get header of diff HTML 223 * 224 * @param string $l_rev Left revisions 225 * @param string $r_rev Right revision 226 * @param string $id Page id, if null $ID is used 227 * @param bool $media If it is for media files 228 * @param bool $inline Return the header on a single line 229 * @return string[] HTML snippets for diff header 230 */ 231 protected function diffHead($l_rev, $r_rev, $id = null, $media = false, $inline = false) 232 { 233 global $lang; 234 if ($id === null) { 235 global $ID; 236 $id = $ID; 237 } 238 $head_separator = $inline ? ' ' : '<br />'; 239 $media_or_wikiFN = $media ? 'mediaFN' : 'wikiFN'; 240 $ml_or_wl = $media ? 'ml' : 'wl'; 241 $l_minor = $r_minor = ''; 242 243 if ($media) { 244 $changelog = new MediaChangeLog($id); 245 } else { 246 $changelog = new PageChangeLog($id); 247 } 248 if (!$l_rev) { 249 $l_head = '—'; 250 } else { 251 $l_info = $changelog->getRevisionInfo($l_rev); 252 if ($l_info['user']) { 253 $l_user = '<bdi>'.editorinfo($l_info['user']).'</bdi>'; 254 if (auth_ismanager()) $l_user .= ' <bdo dir="ltr">('.$l_info['ip'].')</bdo>'; 255 } else { 256 $l_user = '<bdo dir="ltr">'.$l_info['ip'].'</bdo>'; 257 } 258 $l_user = '<span class="user">'.$l_user.'</span>'; 259 $l_sum = ($l_info['sum']) ? '<span class="sum"><bdi>'.hsc($l_info['sum']).'</bdi></span>' : ''; 260 if ($l_info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) $l_minor = 'class="minor"'; 261 262 $l_head_title = ($media) ? dformat($l_rev) : $id.' ['.dformat($l_rev).']'; 263 $l_head = '<bdi><a class="wikilink1" href="'.$ml_or_wl($id,"rev=$l_rev").'">' 264 . $l_head_title.'</a></bdi>'.$head_separator.$l_user.' '.$l_sum; 265 } 266 267 if ($r_rev) { 268 $r_info = $changelog->getRevisionInfo($r_rev); 269 if ($r_info['user']) { 270 $r_user = '<bdi>'.editorinfo($r_info['user']).'</bdi>'; 271 if (auth_ismanager()) $r_user .= ' <bdo dir="ltr">('.$r_info['ip'].')</bdo>'; 272 } else { 273 $r_user = '<bdo dir="ltr">'.$r_info['ip'].'</bdo>'; 274 } 275 $r_user = '<span class="user">'.$r_user.'</span>'; 276 $r_sum = ($r_info['sum']) ? '<span class="sum"><bdi>'.hsc($r_info['sum']).'</bdi></span>' : ''; 277 if ($r_info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) $r_minor = 'class="minor"'; 278 279 $r_head_title = ($media) ? dformat($r_rev) : $id.' ['.dformat($r_rev).']'; 280 $r_head = '<bdi><a class="wikilink1" href="'.$ml_or_wl($id,"rev=$r_rev").'">' 281 . $r_head_title.'</a></bdi>'.$head_separator.$r_user.' '.$r_sum; 282 } elseif ($_rev = @filemtime($media_or_wikiFN($id))) { 283 $_info = $changelog->getRevisionInfo($_rev); 284 if ($_info['user']) { 285 $_user = '<bdi>'.editorinfo($_info['user']).'</bdi>'; 286 if (auth_ismanager()) $_user .= ' <bdo dir="ltr">('.$_info['ip'].')</bdo>'; 287 } else { 288 $_user = '<bdo dir="ltr">'.$_info['ip'].'</bdo>'; 289 } 290 $_user = '<span class="user">'.$_user.'</span>'; 291 $_sum = ($_info['sum']) ? '<span class="sum"><bdi>'.hsc($_info['sum']).'</span></bdi>' : ''; 292 if ($_info['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT) $r_minor = 'class="minor"'; 293 294 $r_head_title = ($media) ? dformat($_rev) : $id.' ['.dformat($_rev).']'; 295 $r_head = '<bdi><a class="wikilink1" href="'.$ml_or_wl($id).'">' 296 . $r_head_title.'</a></bdi> '.'('.$lang['current'].')'.$head_separator.$_user.' '.$_sum; 297 }else{ 298 $r_head = '— ('.$lang['current'].')'; 299 } 300 301 return array($l_head, $r_head, $l_minor, $r_minor); 302 } 303 304 /** 305 * Create html for revision navigation 306 * 307 * @param PageChangeLog $pagelog changelog object of current page 308 * @param string $type inline vs sidebyside 309 * @param int $l_rev left revision timestamp 310 * @param int $r_rev right revision timestamp 311 * @return string[] html of left and right navigation elements 312 */ 313 protected function diffNavigation($pagelog, $type, $l_rev, $r_rev) 314 { 315 global $INFO, $ID; 316 317 // last timestamp is not in changelog, retrieve timestamp from metadata 318 // note: when page is removed, the metadata timestamp is zero 319 if (!$r_rev) { 320 if (isset($INFO['meta']['last_change']['date'])) { 321 $r_rev = $INFO['meta']['last_change']['date']; 322 } else { 323 $r_rev = 0; 324 } 325 } 326 327 //retrieve revisions with additional info 328 list($l_revs, $r_revs) = $pagelog->getRevisionsAround($l_rev, $r_rev); 329 $l_revisions = array(); 330 if (!$l_rev) { 331 $l_revisions[0] = array(0, "", false); //no left revision given, add dummy 332 } 333 foreach ($l_revs as $rev) { 334 $info = $pagelog->getRevisionInfo($rev); 335 $l_revisions[$rev] = array( 336 $rev, 337 dformat($info['date']) . ' ' . editorinfo($info['user'], true) . ' ' . $info['sum'], 338 $r_rev ? $rev >= $r_rev : false //disable? 339 ); 340 } 341 $r_revisions = array(); 342 if (!$r_rev) { 343 $r_revisions[0] = array(0, "", false); //no right revision given, add dummy 344 } 345 foreach ($r_revs as $rev) { 346 $info = $pagelog->getRevisionInfo($rev); 347 $r_revisions[$rev] = array( 348 $rev, 349 dformat($info['date']) . ' ' . editorinfo($info['user'], true) . ' ' . $info['sum'], 350 $rev <= $l_rev //disable? 351 ); 352 } 353 354 //determine previous/next revisions 355 $l_index = array_search($l_rev, $l_revs); 356 $l_prev = $l_revs[$l_index + 1]; 357 $l_next = $l_revs[$l_index - 1]; 358 if ($r_rev) { 359 $r_index = array_search($r_rev, $r_revs); 360 $r_prev = $r_revs[$r_index + 1]; 361 $r_next = $r_revs[$r_index - 1]; 362 } else { 363 //removed page 364 if ($l_next) { 365 $r_prev = $r_revs[0]; 366 } else { 367 $r_prev = null; 368 } 369 $r_next = null; 370 } 371 372 /* 373 * Left side: 374 */ 375 $l_nav = ''; 376 //move back 377 if ($l_prev) { 378 $l_nav .= $this->diffNavigationlink($type, 'diffbothprevrev', $l_prev, $r_prev); 379 $l_nav .= $this->diffNavigationlink($type, 'diffprevrev', $l_prev, $r_rev); 380 } 381 //dropdown 382 $form = new \Doku_Form(array('action' => wl())); 383 $form->addHidden('id', $ID); 384 $form->addHidden('difftype', $type); 385 $form->addHidden('rev2[1]', $r_rev); 386 $form->addHidden('do', 'diff'); 387 $form->addElement( 388 form_makeListboxField( 389 'rev2[0]', 390 $l_revisions, 391 $l_rev, 392 '', '', '', 393 array('class' => 'quickselect') 394 ) 395 ); 396 $form->addElement(form_makeButton('submit', 'diff', 'Go')); 397 $l_nav .= $form->getForm(); 398 //move forward 399 if ($l_next && ($l_next < $r_rev || !$r_rev)) { 400 $l_nav .= $this->diffNavigationlink($type, 'diffnextrev', $l_next, $r_rev); 401 } 402 403 /* 404 * Right side: 405 */ 406 $r_nav = ''; 407 //move back 408 if ($l_rev < $r_prev) { 409 $r_nav .= $this->diffNavigationlink($type, 'diffprevrev', $l_rev, $r_prev); 410 } 411 //dropdown 412 $form = new \Doku_Form(array('action' => wl())); 413 $form->addHidden('id', $ID); 414 $form->addHidden('rev2[0]', $l_rev); 415 $form->addHidden('difftype', $type); 416 $form->addHidden('do', 'diff'); 417 $form->addElement( 418 form_makeListboxField( 419 'rev2[1]', 420 $r_revisions, 421 $r_rev, 422 '', '', '', 423 array('class' => 'quickselect') 424 ) 425 ); 426 $form->addElement(form_makeButton('submit', 'diff', 'Go')); 427 $r_nav .= $form->getForm(); 428 //move forward 429 if ($r_next) { 430 if ($pagelog->isCurrentRevision($r_next)) { 431 //last revision is diff with current page 432 $r_nav .= $this->diffNavigationlink($type, 'difflastrev', $l_rev); 433 } else { 434 $r_nav .= $this->diffNavigationlink($type, 'diffnextrev', $l_rev, $r_next); 435 } 436 } else { 437 $r_nav .= $this->diffNavigationlink($type, 'diffbothnextrev', $l_next, $r_next); 438 } 439 return array($l_nav, $r_nav); 440 } 441 442 /** 443 * Create html link to a diff defined by two revisions 444 * 445 * @param string $difftype display type 446 * @param string $linktype 447 * @param int $lrev oldest revision 448 * @param int $rrev newest revision or null for diff with current revision 449 * @return string html of link to a diff 450 */ 451 protected function diffNavigationlink($difftype, $linktype, $lrev, $rrev = null) 452 { 453 global $ID, $lang; 454 if (!$rrev) { 455 $urlparam = array( 456 'do' => 'diff', 457 'rev' => $lrev, 458 'difftype' => $difftype, 459 ); 460 } else { 461 $urlparam = array( 462 'do' => 'diff', 463 'rev2[0]' => $lrev, 464 'rev2[1]' => $rrev, 465 'difftype' => $difftype, 466 ); 467 } 468 return '<a class="'. $linktype .'" href="'. wl($ID, $urlparam) .'" title="'. $lang[$linktype] .'">' 469 . '<span>'. $lang[$linktype] .'</span>' 470 . '</a>'. DOKU_LF; 471 } 472 473} 474