[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/Ui/ -> PageDiff.php (source)

   1  <?php
   2  
   3  namespace dokuwiki\Ui;
   4  
   5  use dokuwiki\ChangeLog\PageChangeLog;
   6  use dokuwiki\ChangeLog\RevisionInfo;
   7  use dokuwiki\Form\Form;
   8  use InlineDiffFormatter;
   9  use TableDiffFormatter;
  10  
  11  /**
  12   * DokuWiki PageDiff Interface
  13   *
  14   * @author Andreas Gohr <andi@splitbrain.org>
  15   * @author Satoshi Sahara <sahara.satoshi@gmail.com>
  16   * @package dokuwiki\Ui
  17   */
  18  class PageDiff extends Diff
  19  {
  20      /* @var PageChangeLog */
  21      protected $changelog;
  22  
  23      /* @var RevisionInfo older revision */
  24      protected $RevInfo1;
  25      /* @var RevisionInfo newer revision */
  26      protected $RevInfo2;
  27  
  28      /* @var string */
  29      protected $text;
  30  
  31      /**
  32       * PageDiff Ui constructor
  33       *
  34       * @param string $id  page id
  35       */
  36      public function __construct($id = null)
  37      {
  38          global $INFO;
  39          if (!isset($id)) $id = $INFO['id'];
  40  
  41          // init preference
  42          $this->preference['showIntro'] = true;
  43          $this->preference['difftype'] = 'sidebyside'; // diff view type: inline or sidebyside
  44  
  45          parent::__construct($id);
  46      }
  47  
  48      /** @inheritdoc */
  49      protected function setChangeLog()
  50      {
  51          $this->changelog = new PageChangeLog($this->id);
  52      }
  53  
  54      /**
  55       * Set text to be compared with most current version
  56       * when it has been externally edited
  57       * exclusively use of the compare($old, $new) method
  58       *
  59       * @param string $text
  60       * @return $this
  61       */
  62      public function compareWith($text = null)
  63      {
  64          if (isset($text)) {
  65              $this->text = $text;
  66              $changelog =& $this->changelog;
  67  
  68              // revision info object of older file (left side)
  69              $this->RevInfo1 = new RevisionInfo($changelog->getCurrentRevisionInfo());
  70              $this->RevInfo1->append([
  71                  'current' => true,
  72                  'text' => rawWiki($this->id),
  73              ]);
  74  
  75              // revision info object of newer file (right side)
  76              $this->RevInfo2 = new RevisionInfo();
  77              $this->RevInfo2->append([
  78                  'date' => false,
  79                //'ip'   => '127.0.0.1',
  80                //'type' => DOKU_CHANGE_TYPE_CREATE,
  81                  'id'   => $this->id,
  82                //'user' => '',
  83                //'sum'  => '',
  84                  'extra' => 'compareWith',
  85                //'sizechange' => strlen($this->text) - io_getSizeFile(wikiFN($this->id)),
  86                  'current' => false,
  87                  'text' => cleanText($this->text),
  88              ]);
  89          }
  90          return $this;
  91      }
  92  
  93      /**
  94       * Handle requested revision(s) and diff view preferences
  95       *
  96       * @return void
  97       */
  98      protected function handle()
  99      {
 100          global $INPUT;
 101  
 102          // retrieve requested rev or rev2
 103          if (!isset($this->RevInfo1, $this->RevInfo2)) {
 104              parent::handle();
 105          }
 106  
 107          // requested diff view type
 108          if ($INPUT->has('difftype')) {
 109              $this->preference['difftype'] = $INPUT->str('difftype');
 110          } else {
 111              // read preference from DokuWiki cookie. PageDiff only
 112              $mode = get_doku_pref('difftype', null);
 113              if (isset($mode)) $this->preference['difftype'] = $mode;
 114          }
 115  
 116          if (!$INPUT->has('rev') && !$INPUT->has('rev2')) {
 117              global $INFO, $REV;
 118              if ($this->id == $INFO['id'])
 119                  $REV = $this->rev1; // store revision back in $REV
 120          }
 121      }
 122  
 123      /**
 124       * Prepare revision info of comparison pair
 125       */
 126      protected function preProcess()
 127      {
 128          global $lang;
 129  
 130          $changelog =& $this->changelog;
 131  
 132          // create revision info object for older and newer sides
 133          // RevInfo1 : older, left side
 134          // RevInfo2 : newer, right side
 135          $this->RevInfo1 = new RevisionInfo($changelog->getRevisionInfo($this->rev1));
 136          $this->RevInfo2 = new RevisionInfo($changelog->getRevisionInfo($this->rev2));
 137  
 138          foreach ([$this->RevInfo1, $this->RevInfo2] as $RevInfo) {
 139              $isCurrent = $changelog->isCurrentRevision($RevInfo->val('date'));
 140              $RevInfo->isCurrent($isCurrent);
 141  
 142              if ($RevInfo->val('type') == DOKU_CHANGE_TYPE_DELETE || empty($RevInfo->val('type'))) {
 143                  $text = '';
 144              } else {
 145                  $rev = $isCurrent ? '' : $RevInfo->val('date');
 146                  $text = rawWiki($this->id, $rev);
 147              }
 148              $RevInfo->append(['text' => $text]);
 149          }
 150  
 151          // msg could displayed only when wrong url typed in browser address bar
 152          if ($this->rev2 === false) {
 153              msg(sprintf($lang['page_nonexist_rev'],
 154                  $this->id,
 155                  wl($this->id, ['do'=>'edit']),
 156                  $this->id), -1);
 157          } elseif (!$this->rev1 || $this->rev1 == $this->rev2) {
 158              msg('no way to compare when less than two revisions', -1);
 159          }
 160      }
 161  
 162      /**
 163       * Show diff
 164       * between current page version and provided $text
 165       * or between the revisions provided via GET or POST
 166       *
 167       * @author Andreas Gohr <andi@splitbrain.org>
 168       *
 169       * @return void
 170       */
 171      public function show()
 172      {
 173          global $lang;
 174  
 175          if (!isset($this->RevInfo1, $this->RevInfo2)) {
 176              // retrieve form parameters: rev, rev2, difftype
 177              $this->handle();
 178              // prepare revision info of comparison pair, except PageConfrict or PageDraft
 179              $this->preProcess();
 180          }
 181  
 182          // revision title
 183          $rev1Title = trim($this->RevInfo1->showRevisionTitle() .' '. $this->RevInfo1->showCurrentIndicator());
 184          $rev1Summary = ($this->RevInfo1->val('date'))
 185              ? $this->RevInfo1->showEditSummary() .' '. $this->RevInfo1->showEditor()
 186              : '';
 187  
 188          if ($this->RevInfo2->val('extra') == 'compareWith') {
 189              $rev2Title = $lang['yours'];
 190              $rev2Summary = '';
 191          } else {
 192              $rev2Title = trim($this->RevInfo2->showRevisionTitle() .' '. $this->RevInfo2->showCurrentIndicator());
 193              $rev2Summary = ($this->RevInfo2->val('date'))
 194                  ? $this->RevInfo2->showEditSummary() .' '. $this->RevInfo2->showEditor()
 195                  : '';
 196          }
 197  
 198          // create difference engine object
 199          $Difference = new \Diff(
 200                  explode("\n", $this->RevInfo1->val('text')),
 201                  explode("\n", $this->RevInfo2->val('text'))
 202          );
 203  
 204          // build paired navigation
 205          [$rev1Navi, $rev2Navi] = $this->buildRevisionsNavigation();
 206  
 207          // display intro
 208          if ($this->preference['showIntro']) echo p_locale_xhtml('diff');
 209  
 210          // print form to choose diff view type, and exact url reference to the view
 211          $this->showDiffViewSelector();
 212  
 213          // assign minor edit checker to the variable
 214          $classEditType = function ($changeType) {
 215              return ($changeType === DOKU_CHANGE_TYPE_MINOR_EDIT) ? ' class="minor"' : '';
 216          };
 217  
 218          // display diff view table
 219          echo '<div class="table">';
 220          echo '<table class="diff diff_'.$this->preference['difftype'] .'">';
 221  
 222          //navigation and header
 223          switch ($this->preference['difftype']) {
 224              case 'inline':
 225                  $title1 = $rev1Title . ($rev1Summary ? '<br />'.$rev1Summary : '');
 226                  $title2 = $rev2Title . ($rev2Summary ? '<br />'.$rev2Summary : '');
 227                  // no navigation for PageConflict or PageDraft
 228                  if ($this->RevInfo2->val('extra') !== 'compareWith') {
 229                      echo '<tr>'
 230                          .'<td class="diff-lineheader">-</td>'
 231                          .'<td class="diffnav">'. $rev1Navi .'</td>'
 232                          .'</tr>';
 233                      echo '<tr>'
 234                          .'<th class="diff-lineheader">-</th>'
 235                          .'<th'.$classEditType($this->RevInfo1->val('type')).'>'. $title1 .'</th>'
 236                          .'</tr>';
 237                  }
 238                  echo '<tr>'
 239                      .'<td class="diff-lineheader">+</td>'
 240                      .'<td class="diffnav">'. $rev2Navi .'</td>'
 241                      .'</tr>';
 242                  echo '<tr>'
 243                      .'<th class="diff-lineheader">+</th>'
 244                      .'<th'.$classEditType($this->RevInfo2->val('type')).'>'. $title2 .'</th>'
 245                      .'</tr>';
 246                  // create formatter object
 247                  $DiffFormatter = new InlineDiffFormatter();
 248                  break;
 249  
 250              case 'sidebyside':
 251              default:
 252                  $title1 = $rev1Title . ($rev1Summary ? ' '.$rev1Summary : '');
 253                  $title2 = $rev2Title . ($rev2Summary ? ' '.$rev2Summary : '');
 254                  // no navigation for PageConflict or PageDraft
 255                  if ($this->RevInfo2->val('extra') !== 'compareWith') {
 256                      echo '<tr>'
 257                          .'<td colspan="2" class="diffnav">'. $rev1Navi .'</td>'
 258                          .'<td colspan="2" class="diffnav">'. $rev2Navi .'</td>'
 259                          .'</tr>';
 260                  }
 261                  echo '<tr>'
 262                      .'<th colspan="2"'.$classEditType($this->RevInfo1->val('type')).'>'.$title1.'</th>'
 263                      .'<th colspan="2"'.$classEditType($this->RevInfo2->val('type')).'>'.$title2.'</th>'
 264                      .'</tr>';
 265                  // create formatter object
 266                  $DiffFormatter = new TableDiffFormatter();
 267                  break;
 268          }
 269  
 270          // output formatted difference
 271          echo $this->insertSoftbreaks($DiffFormatter->format($Difference));
 272  
 273          echo '</table>';
 274          echo '</div>';
 275      }
 276  
 277      /**
 278       * Print form to choose diff view type, and exact url reference to the view
 279       */
 280      protected function showDiffViewSelector()
 281      {
 282          global $lang;
 283  
 284          // no revisions selector for PageConflict or PageDraft
 285          if ($this->RevInfo2->val('extra') == 'compareWith') return;
 286  
 287          // use timestamp for current revision, date may be false when revisions < 2
 288          [$rev1, $rev2] = [(int)$this->RevInfo1->val('date'), (int)$this->RevInfo2->val('date')];
 289  
 290          echo '<div class="diffoptions group">';
 291  
 292          // create the form to select difftype
 293          $form = new Form(['action' => wl()]);
 294          $form->setHiddenField('id', $this->id);
 295          $form->setHiddenField('rev2[0]', $rev1);
 296          $form->setHiddenField('rev2[1]', $rev2);
 297          $form->setHiddenField('do', 'diff');
 298          $options = array(
 299                       'sidebyside' => $lang['diff_side'],
 300                       'inline' => $lang['diff_inline'],
 301          );
 302          $input = $form->addDropdown('difftype', $options, $lang['diff_type'])
 303              ->val($this->preference['difftype'])
 304              ->addClass('quickselect');
 305          $input->useInput(false); // inhibit prefillInput() during toHTML() process
 306          $form->addButton('do[diff]', 'Go')->attr('type','submit');
 307          echo $form->toHTML();
 308  
 309          // show exact url reference to the view when it is meaningful
 310          echo '<p>';
 311          if ($rev1 && $rev2) {
 312              // link to exactly this view FS#2835
 313              $viewUrl = $this->diffViewlink('difflink', $rev1, $rev2);
 314          }
 315          echo $viewUrl ?? '<br />';
 316          echo '</p>';
 317  
 318          echo '</div>';
 319      }
 320  
 321      /**
 322       * Create html for revision navigation
 323       *
 324       * The navigation consists of older and newer revisions selectors, each
 325       * state mutually depends on the selected revision of opposite side.
 326       *
 327       * @return string[] html of navigation for both older and newer sides
 328       */
 329      protected function buildRevisionsNavigation()
 330      {
 331          $changelog =& $this->changelog;
 332  
 333          if ($this->RevInfo2->val('extra') == 'compareWith') {
 334              // no revisions selector for PageConflict or PageDraft
 335              return array('', '');
 336          }
 337  
 338          // use timestamp for current revision, date may be false when revisions < 2
 339          [$rev1, $rev2] = [(int)$this->RevInfo1->val('date'), (int)$this->RevInfo2->val('date')];
 340  
 341          // retrieve revisions used in dropdown selectors, even when rev1 or rev2 is false
 342          [$revs1, $revs2] = $changelog->getRevisionsAround(
 343              ($rev1 ?: $changelog->currentRevision()),
 344              ($rev2 ?: $changelog->currentRevision())
 345          );
 346  
 347          // build options for dropdown selector
 348          $rev1Options = $this->buildRevisionOptions('older', $revs1);
 349          $rev2Options = $this->buildRevisionOptions('newer', $revs2);
 350  
 351          // determine previous/next revisions (older/left side)
 352          $rev1Prev = $rev1Next = false;
 353          if (($index = array_search($rev1, $revs1)) !== false) {
 354              $rev1Prev = ($index +1 < count($revs1)) ? $revs1[$index +1] : false;
 355              $rev1Next = ($index > 0)                ? $revs1[$index -1] : false;
 356          }
 357          // determine previous/next revisions (newer/right side)
 358          $rev2Prev = $rev2Next = false;
 359          if (($index = array_search($rev2, $revs2)) !== false) {
 360              $rev2Prev = ($index +1 < count($revs2)) ? $revs2[$index +1] : false;
 361              $rev2Next = ($index > 0)                ? $revs2[$index -1] : false;
 362          }
 363  
 364          /*
 365           * navigation UI for older revisions / Left side:
 366           */
 367          $rev1Navi = '';
 368          // move backward both side: ◀◀
 369          if ($rev1Prev && $rev2Prev)
 370              $rev1Navi .= $this->diffViewlink('diffbothprevrev', $rev1Prev, $rev2Prev);
 371          // move backward left side: ◀
 372          if ($rev1Prev)
 373              $rev1Navi .= $this->diffViewlink('diffprevrev', $rev1Prev, $rev2);
 374          // dropdown
 375          $rev1Navi .= $this->buildDropdownSelector('older', $rev1Options);
 376          // move forward left side: ▶
 377          if ($rev1Next && ($rev1Next < $rev2))
 378              $rev1Navi .= $this->diffViewlink('diffnextrev', $rev1Next, $rev2);
 379  
 380          /*
 381           * navigation UI for newer revisions / Right side:
 382           */
 383          $rev2Navi = '';
 384          // move backward right side: ◀
 385          if ($rev2Prev && ($rev1 < $rev2Prev))
 386              $rev2Navi .= $this->diffViewlink('diffprevrev', $rev1, $rev2Prev);
 387          // dropdown
 388          $rev2Navi .= $this->buildDropdownSelector('newer', $rev2Options);
 389          // move forward right side: ▶
 390          if ($rev2Next) {
 391              if ($changelog->isCurrentRevision($rev2Next)) {
 392                  $rev2Navi .= $this->diffViewlink('difflastrev', $rev1, $rev2Next);
 393              } else {
 394                  $rev2Navi .= $this->diffViewlink('diffnextrev', $rev1, $rev2Next);
 395              }
 396          }
 397          // move forward both side: ▶▶
 398          if ($rev1Next && $rev2Next)
 399              $rev2Navi .= $this->diffViewlink('diffbothnextrev', $rev1Next, $rev2Next);
 400  
 401          return array($rev1Navi, $rev2Navi);
 402      }
 403  
 404      /**
 405       * prepare options for dropdwon selector
 406       *
 407       * @params string $side  "older" or "newer"
 408       * @params array $revs  list of revsion
 409       * @return array
 410       */
 411      protected function buildRevisionOptions($side, $revs)
 412      {
 413          // use timestamp for current revision, date may be false when revisions < 2
 414          [$rev1, $rev2] = [(int)$this->RevInfo1->val('date'), (int)$this->RevInfo2->val('date')];
 415  
 416          $changelog =& $this->changelog;
 417          $options = [];
 418  
 419          foreach ($revs as $rev) {
 420              $info = $changelog->getRevisionInfo($rev);
 421              // revision info may have timestamp key when external edits occurred
 422              $info['timestamp'] = $info['timestamp'] ?? true;
 423              $date = dformat($info['date']);
 424              if ($info['timestamp'] === false) {
 425                  // exteranlly deleted or older file restored
 426                  $date = preg_replace('/[0-9a-zA-Z]/','_', $date);
 427              }
 428              $options[$rev] = array(
 429                  'label' => implode(' ', [
 430                              $date,
 431                              editorinfo($info['user'], true),
 432                              $info['sum'],
 433                             ]),
 434                  'attrs' => ['title' => $rev],
 435              );
 436              if (($side == 'older' && ($rev2 && $rev >= $rev2))
 437                ||($side == 'newer' && ($rev <= $rev1))
 438              ) {
 439                  $options[$rev]['attrs']['disabled'] = 'disabled';
 440              }
 441          }
 442          return $options;
 443      }
 444  
 445      /**
 446       * build Dropdown form for revisions navigation
 447       *
 448       * @params string $side  "older" or "newer"
 449       * @params array $options  dropdown options
 450       * @return string
 451       */
 452      protected function buildDropdownSelector($side, $options)
 453      {
 454          // use timestamp for current revision, date may be false when revisions < 2
 455          [$rev1, $rev2] = [(int)$this->RevInfo1->val('date'), (int)$this->RevInfo2->val('date')];
 456  
 457          $form = new Form(['action' => wl($this->id)]);
 458          $form->setHiddenField('id', $this->id);
 459          $form->setHiddenField('do', 'diff');
 460          $form->setHiddenField('difftype', $this->preference['difftype']);
 461  
 462          switch ($side) {
 463              case 'older': // left side
 464                  $form->setHiddenField('rev2[1]', $rev2);
 465                  $input = $form->addDropdown('rev2[0]', $options)
 466                      ->val($rev1)->addClass('quickselect');
 467                  $input->useInput(false); // inhibit prefillInput() during toHTML() process
 468                  break;
 469              case 'newer': // right side
 470                  $form->setHiddenField('rev2[0]', $rev1);
 471                  $input = $form->addDropdown('rev2[1]', $options)
 472                      ->val($rev2)->addClass('quickselect');
 473                  $input->useInput(false); // inhibit prefillInput() during toHTML() process
 474                  break;
 475          }
 476          $form->addButton('do[diff]', 'Go')->attr('type','submit');
 477          return $form->toHTML();
 478      }
 479  
 480      /**
 481       * Create html link to a diff view defined by two revisions
 482       *
 483       * @param string $linktype
 484       * @param int $rev1 older revision
 485       * @param int $rev2 newer revision or null for diff with current revision
 486       * @return string html of link to a diff view
 487       */
 488      protected function diffViewlink($linktype, $rev1, $rev2 = null)
 489      {
 490          global $lang;
 491          if ($rev1 === false) return '';
 492  
 493          if ($rev2 === null) {
 494              $urlparam = array(
 495                  'do' => 'diff',
 496                  'rev' => $rev1,
 497                  'difftype' => $this->preference['difftype'],
 498              );
 499          } else {
 500              $urlparam = array(
 501                  'do' => 'diff',
 502                  'rev2[0]' => $rev1,
 503                  'rev2[1]' => $rev2,
 504                  'difftype' => $this->preference['difftype'],
 505              );
 506          }
 507          $attr = array(
 508              'class' => $linktype,
 509              'href'  => wl($this->id, $urlparam, true, '&'),
 510              'title' => $lang[$linktype],
 511          );
 512          return '<a '. buildAttributes($attr) .'><span>'. $lang[$linktype] .'</span></a>';
 513      }
 514  
 515  
 516      /**
 517       * Insert soft breaks in diff html
 518       *
 519       * @param string $diffhtml
 520       * @return string
 521       */
 522      public function insertSoftbreaks($diffhtml)
 523      {
 524          // search the diff html string for both:
 525          // - html tags, so these can be ignored
 526          // - long strings of characters without breaking characters
 527          return preg_replace_callback('/<[^>]*>|[^<> ]{12,}/', function ($match) {
 528              // if match is an html tag, return it intact
 529              if ($match[0][0] == '<') return $match[0];
 530              // its a long string without a breaking character,
 531              // make certain characters into breaking characters by inserting a
 532              // word break opportunity (<wbr> tag) in front of them.
 533              $regex = <<< REGEX
 534  (?(?=              # start a conditional expression with a positive look ahead ...
 535  &\#?\\w{1,6};)     # ... for html entities - we don't want to split them (ok to catch some invalid combinations)
 536  &\#?\\w{1,6};      # yes pattern - a quicker match for the html entity, since we know we have one
 537  |
 538  [?/,&\#;:]         # no pattern - any other group of 'special' characters to insert a breaking character after
 539  )+                 # end conditional expression
 540  REGEX;
 541              return preg_replace('<'.$regex.'>xu', '\0<wbr>', $match[0]);
 542          }, $diffhtml);
 543      }
 544  
 545  }