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