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