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