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