ILIAS  release_7 Revision v7.30-3-g800a261c036
class.WordLevelDiff.php
Go to the documentation of this file.
1 <?php
2 
3 // Copyright holded by MediaWiki contributers, Licensed under GPL version 2 or later
4 
5 
18 {
22  public $mOldid;
23  public $mNewid;
24  public $mTitle;
25  public $mOldtitle;
26  public $mNewtitle;
27  public $mPagetitle;
28  public $mOldtext;
29  public $mNewtext;
30  public $mOldPage;
31  public $mNewPage;
33  public $mOldRev;
34  public $mNewRev;
35  public $mRevisionsLoaded = false; // Have the revisions been loaded
36  public $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2?
46  public function __construct($titleObj = null, $old = 0, $new = 0, $rcid = 0)
47  {
48  $this->mTitle = $titleObj;
49  wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
50 
51  if ('prev' === $new) {
52  # Show diff between revision $old and the previous one.
53  # Get previous one from DB.
54  #
55  $this->mNewid = intval($old);
56 
57  $this->mOldid = $this->mTitle->getPreviousRevisionID($this->mNewid);
58  } elseif ('next' === $new) {
59  # Show diff between revision $old and the previous one.
60  # Get previous one from DB.
61  #
62  $this->mOldid = intval($old);
63  $this->mNewid = $this->mTitle->getNextRevisionID($this->mOldid);
64  if (false === $this->mNewid) {
65  # if no result, NewId points to the newest old revision. The only newer
66  # revision is cur, which is "0".
67  $this->mNewid = 0;
68  }
69  } else {
70  $this->mOldid = intval($old);
71  $this->mNewid = intval($new);
72  }
73  $this->mRcidMarkPatrolled = intval($rcid); # force it to be an integer
74  }
75 
76  public function showDiffPage($diffOnly = false)
77  {
78  global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol;
79  $fname = 'DifferenceEngine::showDiffPage';
80  //wfProfileIn( $fname );
81 
82  # If external diffs are enabled both globally and for the user,
83  # we'll use the application/x-external-editor interface to call
84  # an external diff tool like kompare, kdiff3, etc.
85  if ($wgUseExternalEditor && $wgUser->getOption('externaldiff')) {
86  global $wgInputEncoding,$wgServer,$wgScript,$wgLang;
87  $wgOut->disable();
88  header("Content-type: application/x-external-editor; charset=" . $wgInputEncoding);
89  $url1 = $this->mTitle->getFullURL("action=raw&oldid=" . $this->mOldid);
90  $url2 = $this->mTitle->getFullURL("action=raw&oldid=" . $this->mNewid);
91  $special = $wgLang->getNsText(NS_SPECIAL);
92  $control = <<<CONTROL
93 [Process]
94 Type=Diff text
95 Engine=MediaWiki
96 Script={$wgServer}{$wgScript}
97 Special namespace={$special}
98 
99 [File]
100 Extension=wiki
101 URL=$url1
102 
103 [File 2]
104 Extension=wiki
105 URL=$url2
106 CONTROL;
107  echo($control);
108  return;
109  }
110 
111  $wgOut->setArticleFlag(false);
112  if (!$this->loadRevisionData()) {
113  $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, {$this->mNewid})";
114  $mtext = wfMsg('missingarticle', "<nowiki>$t</nowiki>");
115  $wgOut->setPagetitle(wfMsg('errorpagetitle'));
116  $wgOut->addWikitext($mtext);
117  //wfProfileOut( $fname );
118  return;
119  }
120 
121  wfRunHooks('DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ));
122 
123  if ($this->mNewRev->isCurrent()) {
124  $wgOut->setArticleFlag(true);
125  }
126 
127  # mOldid is false if the difference engine is called with a "vague" query for
128  # a diff between a version V and its previous version V' AND the version V
129  # is the first version of that article. In that case, V' does not exist.
130  if ($this->mOldid === false) {
131  $this->showFirstRevision();
132  $this->renderNewRevision(); // should we respect $diffOnly here or not?
133  //wfProfileOut( $fname );
134  return;
135  }
136 
137  $wgOut->suppressQuickbar();
138 
139  $oldTitle = $this->mOldPage->getPrefixedText();
140  $newTitle = $this->mNewPage->getPrefixedText();
141  if ($oldTitle == $newTitle) {
142  $wgOut->setPageTitle($newTitle);
143  } else {
144  $wgOut->setPageTitle($oldTitle . ', ' . $newTitle);
145  }
146  $wgOut->setSubtitle(wfMsg('difference'));
147  $wgOut->setRobotpolicy('noindex,nofollow');
148 
149  if (!($this->mOldPage->userCanRead() && $this->mNewPage->userCanRead())) {
150  $wgOut->loginToUse();
151  $wgOut->output();
152  //wfProfileOut( $fname );
153  exit;
154  }
155 
156  $sk = $wgUser->getSkin();
157 
158  if ($this->mNewRev->isCurrent() && $wgUser->isAllowed('rollback')) {
159  $rollback = '&nbsp;&nbsp;&nbsp;' . $sk->generateRollback($this->mNewRev);
160  } else {
161  $rollback = '';
162  }
163  if ($wgUseRCPatrol && $this->mRcidMarkPatrolled != 0 && $wgUser->isAllowed('patrol')) {
164  $patrol = ' [' . $sk->makeKnownLinkObj($this->mTitle, wfMsg('markaspatrolleddiff'), "action=markpatrolled&rcid={$this->mRcidMarkPatrolled}") . ']';
165  } else {
166  $patrol = '';
167  }
168 
169  $prevlink = $sk->makeKnownLinkObj(
170  $this->mTitle,
171  wfMsgHtml('previousdiff'),
172  'diff=prev&oldid=' . $this->mOldid,
173  '',
174  '',
175  'id="differences-prevlink"'
176  );
177  if ($this->mNewRev->isCurrent()) {
178  $nextlink = '&nbsp;';
179  } else {
180  $nextlink = $sk->makeKnownLinkObj(
181  $this->mTitle,
182  wfMsgHtml('nextdiff'),
183  'diff=next&oldid=' . $this->mNewid,
184  '',
185  '',
186  'id="differences-nextlink"'
187  );
188  }
189 
190  $oldminor = '';
191  $newminor = '';
192 
193  if ($this->mOldRev->mMinorEdit == 1) {
194  $oldminor = wfElement(
195  'span',
196  array( 'class' => 'minor' ),
197  wfMsg('minoreditletter')
198  ) . ' ';
199  }
200 
201  if ($this->mNewRev->mMinorEdit == 1) {
202  $newminor = wfElement(
203  'span',
204  array( 'class' => 'minor' ),
205  wfMsg('minoreditletter')
206  ) . ' ';
207  }
208 
209  $oldHeader = "<strong>{$this->mOldtitle}</strong><br />" .
210  $sk->revUserTools($this->mOldRev) . "<br />" .
211  $oldminor . $sk->revComment($this->mOldRev, !$diffOnly) . "<br />" .
212  $prevlink;
213  $newHeader = "<strong>{$this->mNewtitle}</strong><br />" .
214  $sk->revUserTools($this->mNewRev) . " $rollback<br />" .
215  $newminor . $sk->revComment($this->mNewRev, !$diffOnly) . "<br />" .
216  $nextlink . $patrol;
217 
218  $this->showDiff($oldHeader, $newHeader);
219 
220  if (!$diffOnly) {
221  $this->renderNewRevision();
222  }
223 
224  //wfProfileOut( $fname );
225  }
226 
230  public function renderNewRevision()
231  {
232  global $wgOut;
233  $fname = 'DifferenceEngine::renderNewRevision';
234  //wfProfileIn( $fname );
235 
236  $wgOut->addHTML("<hr /><h2>{$this->mPagetitle}</h2>\n");
237  #add deleted rev tag if needed
238  if (!$this->mNewRev->userCan(Revision::DELETED_TEXT)) {
239  $wgOut->addWikiText(wfMsg('rev-deleted-text-permission'));
240  }
241 
242  if (!$this->mNewRev->isCurrent()) {
243  $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection(false);
244  }
245 
246  $this->loadNewText();
247  if (is_object($this->mNewRev)) {
248  $wgOut->setRevisionId($this->mNewRev->getId());
249  }
250 
251  $wgOut->addWikiTextTidy($this->mNewtext);
252 
253  if (!$this->mNewRev->isCurrent()) {
254  $wgOut->parserOptions()->setEditSection($oldEditSectionSetting);
255  }
256 
257  //wfProfileOut( $fname );
258  }
259 
264  public function showFirstRevision()
265  {
266  global $wgOut, $wgUser;
267 
268  $fname = 'DifferenceEngine::showFirstRevision';
269  //wfProfileIn( $fname );
270 
271  # Get article text from the DB
272  #
273  if (!$this->loadNewText()) {
274  $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " .
275  "{$this->mNewid})";
276  $mtext = wfMsg('missingarticle', "<nowiki>$t</nowiki>");
277  $wgOut->setPagetitle(wfMsg('errorpagetitle'));
278  $wgOut->addWikitext($mtext);
279  //wfProfileOut( $fname );
280  return;
281  }
282  if ($this->mNewRev->isCurrent()) {
283  $wgOut->setArticleFlag(true);
284  }
285 
286  # Check if user is allowed to look at this page. If not, bail out.
287  #
288  if (!($this->mTitle->userCanRead())) {
289  $wgOut->loginToUse();
290  $wgOut->output();
291  //wfProfileOut( $fname );
292  exit;
293  }
294 
295  # Prepare the header box
296  #
297  $sk = $wgUser->getSkin();
298 
299  $nextlink = $sk->makeKnownLinkObj($this->mTitle, wfMsgHtml('nextdiff'), 'diff=next&oldid=' . $this->mNewid, '', '', 'id="differences-nextlink"');
300  $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\"><strong>{$this->mOldtitle}</strong><br />" .
301  $sk->revUserTools($this->mNewRev) . "<br />" .
302  $sk->revComment($this->mNewRev) . "<br />" .
303  $nextlink . "</div>\n";
304 
305  $wgOut->addHTML($header);
306 
307  $wgOut->setSubtitle(wfMsg('difference'));
308  $wgOut->setRobotpolicy('noindex,nofollow');
309 
310  //wfProfileOut( $fname );
311  }
312 
317  public function showDiff($otitle, $ntitle)
318  {
319  global $wgOut;
320  $diff = $this->getDiff($otitle, $ntitle);
321  if ($diff === false) {
322  $wgOut->addWikitext(wfMsg('missingarticle', "<nowiki>(fixme, bug)</nowiki>"));
323  return false;
324  } else {
325  $wgOut->addHTML($diff);
326  return true;
327  }
328  }
329 
335  public function getDiff($otitle, $ntitle)
336  {
337  $body = $this->getDiffBody();
338  if ($body === false) {
339  return false;
340  } else {
341  $multi = $this->getMultiNotice();
342  return $this->addHeader($body, $otitle, $ntitle, $multi);
343  }
344  }
345 
351  public function getDiffBody()
352  {
353  global $wgMemc;
354  $fname = 'DifferenceEngine::getDiffBody';
355  //wfProfileIn( $fname );
356 
357  // Cacheable?
358  $key = false;
359  if ($this->mOldid && $this->mNewid) {
360  // Try cache
361  $key = wfMemcKey('diff', 'oldid', $this->mOldid, 'newid', $this->mNewid);
362  $difftext = $wgMemc->get($key);
363  if ($difftext) {
364  wfIncrStats('diff_cache_hit');
365  $difftext = $this->localiseLineNumbers($difftext);
366  $difftext .= "\n<!-- diff cache key $key -->\n";
367  //wfProfileOut( $fname );
368  return $difftext;
369  }
370  }
371 
372  #loadtext is permission safe, this just clears out the diff
373  if (!$this->loadText()) {
374  //wfProfileOut( $fname );
375  return false;
376  } elseif ($this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT)) {
377  return '';
378  } elseif ($this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT)) {
379  return '';
380  }
381 
382  $difftext = $this->generateDiffBody($this->mOldtext, $this->mNewtext);
383 
384  // Save to cache for 7 days
385  if ($key !== false && $difftext !== false) {
386  wfIncrStats('diff_cache_miss');
387  $wgMemc->set($key, $difftext, 7 * 86400);
388  } else {
389  wfIncrStats('diff_uncacheable');
390  }
391  // Replace line numbers with the text in the user's language
392  if ($difftext !== false) {
393  $difftext = $this->localiseLineNumbers($difftext);
394  }
395  //wfProfileOut( $fname );
396  return $difftext;
397  }
398 
403  public function generateDiffBody($otext, $ntext)
404  {
405  global $wgExternalDiffEngine, $wgContLang;
406  $fname = 'DifferenceEngine::generateDiffBody';
407 
408  $otext = str_replace("\r\n", "\n", $otext);
409  $ntext = str_replace("\r\n", "\n", $ntext);
410 
411  if ($wgExternalDiffEngine == 'wikidiff') {
412  # For historical reasons, external diff engine expects
413  # input text to be HTML-escaped already
414  $otext = htmlspecialchars($wgContLang->segmentForDiff($otext));
415  $ntext = htmlspecialchars($wgContLang->segmentForDiff($ntext));
416  if (!function_exists('wikidiff_do_diff')) {
417  dl('php_wikidiff.so');
418  }
419  return $wgContLang->unsegementForDiff(wikidiff_do_diff($otext, $ntext, 2));
420  }
421 
422  if ($wgExternalDiffEngine == 'wikidiff2') {
423  # Better external diff engine, the 2 may some day be dropped
424  # This one does the escaping and segmenting itself
425  if (!function_exists('wikidiff2_do_diff')) {
426  //wfProfileIn( "$fname-dl" );
427  @dl('php_wikidiff2.so');
428  //wfProfileOut( "$fname-dl" );
429  }
430  if (function_exists('wikidiff2_do_diff')) {
431  //wfProfileIn( 'wikidiff2_do_diff' );
432  $text = wikidiff2_do_diff($otext, $ntext, 2);
433  //wfProfileOut( 'wikidiff2_do_diff' );
434  return $text;
435  }
436  }
437  if ($wgExternalDiffEngine !== false) {
438  # Diff via the shell
439  global $wgTmpDirectory;
440  $tempName1 = tempnam($wgTmpDirectory, 'diff_');
441  $tempName2 = tempnam($wgTmpDirectory, 'diff_');
442 
443  $tempFile1 = fopen($tempName1, "w");
444  if (!$tempFile1) {
445  //wfProfileOut( $fname );
446  return false;
447  }
448  $tempFile2 = fopen($tempName2, "w");
449  if (!$tempFile2) {
450  //wfProfileOut( $fname );
451  return false;
452  }
453  fwrite($tempFile1, $otext);
454  fwrite($tempFile2, $ntext);
455  fclose($tempFile1);
456  fclose($tempFile2);
457  $cmd = wfEscapeShellArg($wgExternalDiffEngine, $tempName1, $tempName2);
458  //wfProfileIn( "$fname-shellexec" );
459  $difftext = wfShellExec($cmd);
460  //wfProfileOut( "$fname-shellexec" );
461  unlink($tempName1);
462  unlink($tempName2);
463  return $difftext;
464  }
465 
466  # Native PHP diff
467  $ota = explode("\n", $wgContLang->segmentForDiff($otext));
468  $nta = explode("\n", $wgContLang->segmentForDiff($ntext));
469  $diffs = new Diff($ota, $nta);
470  $formatter = new TableDiffFormatter();
471  return $wgContLang->unsegmentForDiff($formatter->format($diffs));
472  }
473 
474 
478  public function localiseLineNumbers($text)
479  {
480  return preg_replace_callback(
481  '/<!--LINE (\d+)-->/',
482  array( &$this, 'localiseLineNumbersCb' ),
483  $text
484  );
485  }
486 
487  public function localiseLineNumbersCb($matches)
488  {
489  global $wgLang;
490  return wfMsgExt('lineno', array('parseinline'), $wgLang->formatNum($matches[1]));
491  }
492 
493 
497  public function getMultiNotice()
498  {
499  if (!is_object($this->mOldRev) || !is_object($this->mNewRev)) {
500  return '';
501  }
502 
503  if (!$this->mOldPage->equals($this->mNewPage)) {
504  // Comparing two different pages? Count would be meaningless.
505  return '';
506  }
507 
508  $oldid = $this->mOldRev->getId();
509  $newid = $this->mNewRev->getId();
510  if ($oldid > $newid) {
511  $tmp = $oldid;
512  $oldid = $newid;
513  $newid = $tmp;
514  }
515 
516  $n = $this->mTitle->countRevisionsBetween($oldid, $newid);
517  if (!$n) {
518  return '';
519  }
520 
521  return wfMsgExt('diff-multi', array( 'parseinline' ), $n);
522  }
523 
524 
528  public function addHeader($diff, $otitle, $ntitle, $multi = '')
529  {
530  global $wgOut;
531 
532  if ($this->mOldRev && $this->mOldRev->isDeleted(Revision::DELETED_TEXT)) {
533  $otitle = '<span class="history-deleted">' . $otitle . '</span>';
534  }
535  if ($this->mNewRev && $this->mNewRev->isDeleted(Revision::DELETED_TEXT)) {
536  $ntitle = '<span class="history-deleted">' . $ntitle . '</span>';
537  }
538  $header = "
539  <table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>
540  <tr>
541  <td colspan='2' width='50%' align='center' class='diff-otitle'>{$otitle}</td>
542  <td colspan='2' width='50%' align='center' class='diff-ntitle'>{$ntitle}</td>
543  </tr>
544  ";
545 
546  if ($multi != '') {
547  $header .= "<tr><td colspan='4' align='center' class='diff-multi'>{$multi}</td></tr>";
548  }
549 
550  return $header . $diff . "</table>";
551  }
552 
556  public function setText($oldText, $newText)
557  {
558  $this->mOldtext = $oldText;
559  $this->mNewtext = $newText;
560  $this->mTextLoaded = 2;
561  }
562 
573  public function loadRevisionData()
574  {
575  global $wgLang;
576  if ($this->mRevisionsLoaded) {
577  return true;
578  } else {
579  // Whether it succeeds or fails, we don't want to try again
580  $this->mRevisionsLoaded = true;
581  }
582 
583  // Load the new revision object
584  if ($this->mNewid) {
585  $this->mNewRev = Revision::newFromId($this->mNewid);
586  } else {
587  $this->mNewRev = Revision::newFromTitle($this->mTitle);
588  }
589 
590  if (is_null($this->mNewRev)) {
591  return false;
592  }
593 
594  // Set assorted variables
595  $timestamp = $wgLang->timeanddate($this->mNewRev->getTimestamp(), true);
596  $this->mNewPage = $this->mNewRev->getTitle();
597  if ($this->mNewRev->isCurrent()) {
598  $newLink = $this->mNewPage->escapeLocalUrl();
599  $this->mPagetitle = htmlspecialchars(wfMsg('currentrev'));
600  $newEdit = $this->mNewPage->escapeLocalUrl('action=edit');
601 
602  $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)"
603  . " (<a href='$newEdit'>" . htmlspecialchars(wfMsg('editold')) . "</a>)";
604  } else {
605  $newLink = $this->mNewPage->escapeLocalUrl('oldid=' . $this->mNewid);
606  $newEdit = $this->mNewPage->escapeLocalUrl('action=edit&oldid=' . $this->mNewid);
607  $this->mPagetitle = htmlspecialchars(wfMsg('revisionasof', $timestamp));
608 
609  $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>"
610  . " (<a href='$newEdit'>" . htmlspecialchars(wfMsg('editold')) . "</a>)";
611  }
612 
613  // Load the old revision object
614  $this->mOldRev = false;
615  if ($this->mOldid) {
616  $this->mOldRev = Revision::newFromId($this->mOldid);
617  } elseif ($this->mOldid === 0) {
618  $rev = $this->mNewRev->getPrevious();
619  if ($rev) {
620  $this->mOldid = $rev->getId();
621  $this->mOldRev = $rev;
622  } else {
623  // No previous revision; mark to show as first-version only.
624  $this->mOldid = false;
625  $this->mOldRev = false;
626  }
627  }/* elseif ( $this->mOldid === false ) leave mOldRev false; */
628 
629  if (is_null($this->mOldRev)) {
630  return false;
631  }
632 
633  if ($this->mOldRev) {
634  $this->mOldPage = $this->mOldRev->getTitle();
635 
636  $t = $wgLang->timeanddate($this->mOldRev->getTimestamp(), true);
637  $oldLink = $this->mOldPage->escapeLocalUrl('oldid=' . $this->mOldid);
638  $oldEdit = $this->mOldPage->escapeLocalUrl('action=edit&oldid=' . $this->mOldid);
639  $this->mOldtitle = "<a href='$oldLink'>" . htmlspecialchars(wfMsg('revisionasof', $t))
640  . "</a> (<a href='$oldEdit'>" . htmlspecialchars(wfMsg('editold')) . "</a>)";
641  //now that we considered old rev, we can make undo link (bug 8133, multi-edit undo)
642  $newUndo = $this->mNewPage->escapeLocalUrl('action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid);
643  $this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars(wfMsg('editundo')) . "</a>)";
644  }
645 
646  return true;
647  }
648 
652  public function loadText()
653  {
654  if ($this->mTextLoaded == 2) {
655  return true;
656  } else {
657  // Whether it succeeds or fails, we don't want to try again
658  $this->mTextLoaded = 2;
659  }
660 
661  if (!$this->loadRevisionData()) {
662  return false;
663  }
664  if ($this->mOldRev) {
665  // FIXME: permission tests
666  $this->mOldtext = $this->mOldRev->revText();
667  if ($this->mOldtext === false) {
668  return false;
669  }
670  }
671  if ($this->mNewRev) {
672  $this->mNewtext = $this->mNewRev->revText();
673  if ($this->mNewtext === false) {
674  return false;
675  }
676  }
677  return true;
678  }
679 
683  public function loadNewText()
684  {
685  if ($this->mTextLoaded >= 1) {
686  return true;
687  } else {
688  $this->mTextLoaded = 1;
689  }
690  if (!$this->loadRevisionData()) {
691  return false;
692  }
693  $this->mNewtext = $this->mNewRev->getText();
694  return true;
695  }
696 }
698 // A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
699 //
700 // Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
701 // You may copy this code freely under the conditions of the GPL.
702 //
704 define('USE_ASSERTS', function_exists('assert'));
705 
711 class _DiffOp
712 {
713  public $type;
714  public $orig;
715  public $closing;
716 
717  public function reverse()
718  {
719  trigger_error('pure virtual', E_USER_ERROR);
720  }
721 
722  public function norig()
723  {
724  return $this->orig ? sizeof($this->orig) : 0;
725  }
727  public function nclosing()
728  {
729  return $this->closing ? sizeof($this->closing) : 0;
730  }
731 }
732 
738 class _DiffOp_Copy extends _DiffOp
739 {
740  public $type = 'copy';
741 
742  public function __construct($orig, $closing = false)
743  {
744  if (!is_array($closing)) {
745  $closing = $orig;
746  }
747  $this->orig = $orig;
748  $this->closing = $closing;
749  }
751  public function reverse()
752  {
753  return new _DiffOp_Copy($this->closing, $this->orig);
754  }
755 }
756 
762 class _DiffOp_Delete extends _DiffOp
763 {
764  public $type = 'delete';
765 
766  public function __construct($lines)
767  {
768  $this->orig = $lines;
769  $this->closing = false;
770  }
772  public function reverse()
773  {
774  return new _DiffOp_Add($this->orig);
775  }
776 }
777 
783 class _DiffOp_Add extends _DiffOp
784 {
785  public $type = 'add';
786 
787  public function __construct($lines)
788  {
789  $this->closing = $lines;
790  $this->orig = false;
791  }
793  public function reverse()
794  {
795  return new _DiffOp_Delete($this->closing);
796  }
797 }
798 
804 class _DiffOp_Change extends _DiffOp
805 {
806  public $type = 'change';
807 
808  public function __construct($orig, $closing)
809  {
810  $this->orig = $orig;
811  $this->closing = $closing;
812  }
813 
814  public function reverse()
815  {
816  return new _DiffOp_Change($this->closing, $this->orig);
817  }
818 }
819 
820 
844 class _DiffEngine
845 {
846  const MAX_XREF_LENGTH = 10000;
847 
848  public function diff($from_lines, $to_lines)
849  {
850  $fname = '_DiffEngine::diff';
851  //wfProfileIn( $fname );
852 
853  $n_from = sizeof($from_lines);
854  $n_to = sizeof($to_lines);
855 
856  $this->xchanged = $this->ychanged = array();
857  $this->xv = $this->yv = array();
858  $this->xind = $this->yind = array();
859  unset($this->seq);
860  unset($this->in_seq);
861  unset($this->lcs);
862 
863  // Skip leading common lines.
864  for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
865  if ($from_lines[$skip] !== $to_lines[$skip]) {
866  break;
867  }
868  $this->xchanged[$skip] = $this->ychanged[$skip] = false;
869  }
870  // Skip trailing common lines.
871  $xi = $n_from;
872  $yi = $n_to;
873  for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
874  if ($from_lines[$xi] !== $to_lines[$yi]) {
875  break;
876  }
877  $this->xchanged[$xi] = $this->ychanged[$yi] = false;
878  }
879 
880  // Ignore lines which do not exist in both files.
881  for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
882  $xhash[$this->_line_hash($from_lines[$xi])] = 1;
883  }
884 
885  for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
886  $line = $to_lines[$yi];
887  if (($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)]))) {
888  continue;
889  }
890  $yhash[$this->_line_hash($line)] = 1;
891  $this->yv[] = $line;
892  $this->yind[] = $yi;
893  }
894  for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
895  $line = $from_lines[$xi];
896  if (($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)]))) {
897  continue;
898  }
899  $this->xv[] = $line;
900  $this->xind[] = $xi;
901  }
902 
903  // Find the LCS.
904  $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
905 
906  // Merge edits when possible
907  $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
908  $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
909 
910  // Compute the edit operations.
911  $edits = array();
912  $xi = $yi = 0;
913  while ($xi < $n_from || $yi < $n_to) {
914  USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
915  USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
916 
917  // Skip matching "snake".
918  $copy = array();
919  while ($xi < $n_from && $yi < $n_to
920  && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
921  $copy[] = $from_lines[$xi++];
922  ++$yi;
923  }
924  if ($copy) {
925  $edits[] = new _DiffOp_Copy($copy);
926  }
927 
928  // Find deletes & adds.
929  $delete = array();
930  while ($xi < $n_from && $this->xchanged[$xi]) {
931  $delete[] = $from_lines[$xi++];
932  }
933 
934  $add = array();
935  while ($yi < $n_to && $this->ychanged[$yi]) {
936  $add[] = $to_lines[$yi++];
937  }
938 
939  if ($delete && $add) {
940  $edits[] = new _DiffOp_Change($delete, $add);
941  } elseif ($delete) {
942  $edits[] = new _DiffOp_Delete($delete);
943  } elseif ($add) {
944  $edits[] = new _DiffOp_Add($add);
945  }
946  }
947  //wfProfileOut( $fname );
948  return $edits;
949  }
950 
954  public function _line_hash($line)
955  {
956  if (strlen($line) > self::MAX_XREF_LENGTH) {
957  return md5($line);
958  } else {
959  return $line;
960  }
961  }
962 
963 
964  /* Divide the Largest Common Subsequence (LCS) of the sequences
965  * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
966  * sized segments.
967  *
968  * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an
969  * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
970  * sub sequences. The first sub-sequence is contained in [X0, X1),
971  * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on. Note
972  * that (X0, Y0) == (XOFF, YOFF) and
973  * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
974  *
975  * This function assumes that the first lines of the specified portions
976  * of the two files do not match, and likewise that the last lines do not
977  * match. The caller must trim matching lines from the beginning and end
978  * of the portions it is going to specify.
979  */
980  public function _diag($xoff, $xlim, $yoff, $ylim, $nchunks)
981  {
982  $fname = '_DiffEngine::_diag';
983  //wfProfileIn( $fname );
984  $flip = false;
985 
986  if ($xlim - $xoff > $ylim - $yoff) {
987  // Things seems faster (I'm not sure I understand why)
988  // when the shortest sequence in X.
989  $flip = true;
990  list($xoff, $xlim, $yoff, $ylim)
991  = array( $yoff, $ylim, $xoff, $xlim);
992  }
993 
994  if ($flip) {
995  for ($i = $ylim - 1; $i >= $yoff; $i--) {
996  $ymatches[$this->xv[$i]][] = $i;
997  }
998  } else {
999  for ($i = $ylim - 1; $i >= $yoff; $i--) {
1000  $ymatches[$this->yv[$i]][] = $i;
1001  }
1002  }
1003 
1004  $this->lcs = 0;
1005  $this->seq[0] = $yoff - 1;
1006  $this->in_seq = array();
1007  $ymids[0] = array();
1008 
1009  $numer = $xlim - $xoff + $nchunks - 1;
1010  $x = $xoff;
1011  for ($chunk = 0; $chunk < $nchunks; $chunk++) {
1012  //wfProfileIn( "$fname-chunk" );
1013  if ($chunk > 0) {
1014  for ($i = 0; $i <= $this->lcs; $i++) {
1015  $ymids[$i][$chunk - 1] = $this->seq[$i];
1016  }
1017  }
1018 
1019  $x1 = $xoff + (int) (($numer + ($xlim - $xoff) * $chunk) / $nchunks);
1020  for (; $x < $x1; $x++) {
1021  $line = $flip ? $this->yv[$x] : $this->xv[$x];
1022  if (empty($ymatches[$line])) {
1023  continue;
1024  }
1025  $matches = $ymatches[$line];
1026  reset($matches);
1027  foreach ($matches as $junk => $y) {
1028  if (empty($this->in_seq[$y])) {
1029  $k = $this->_lcs_pos($y);
1030  USE_ASSERTS && assert($k > 0);
1031  $ymids[$k] = $ymids[$k - 1];
1032  break;
1033  }
1034  }
1035  foreach ($matches as $y) {
1036  if ($y > $this->seq[$k - 1]) {
1037  USE_ASSERTS && assert($y < $this->seq[$k]);
1038  // Optimization: this is a common case:
1039  // next match is just replacing previous match.
1040  $this->in_seq[$this->seq[$k]] = false;
1041  $this->seq[$k] = $y;
1042  $this->in_seq[$y] = 1;
1043  } elseif (empty($this->in_seq[$y])) {
1044  $k = $this->_lcs_pos($y);
1045  USE_ASSERTS && assert($k > 0);
1046  $ymids[$k] = $ymids[$k - 1];
1047  }
1048  }
1049  }
1050  //wfProfileOut( "$fname-chunk" );
1051  }
1053  $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
1054  $ymid = $ymids[$this->lcs];
1055  for ($n = 0; $n < $nchunks - 1; $n++) {
1056  $x1 = $xoff + (int) (($numer + ($xlim - $xoff) * $n) / $nchunks);
1057  $y1 = $ymid[$n] + 1;
1058  $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
1059  }
1060  $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
1061 
1062  //wfProfileOut( $fname );
1063  return array($this->lcs, $seps);
1064  }
1065 
1066  public function _lcs_pos($ypos)
1067  {
1068  $fname = '_DiffEngine::_lcs_pos';
1069  //wfProfileIn( $fname );
1070 
1071  $end = $this->lcs;
1072  if ($end == 0 || $ypos > $this->seq[$end]) {
1073  $this->seq[++$this->lcs] = $ypos;
1074  $this->in_seq[$ypos] = 1;
1075  //wfProfileOut( $fname );
1076  return $this->lcs;
1077  }
1078 
1079  $beg = 1;
1080  while ($beg < $end) {
1081  $mid = (int) (($beg + $end) / 2);
1082  if ($ypos > $this->seq[$mid]) {
1083  $beg = $mid + 1;
1084  } else {
1085  $end = $mid;
1086  }
1087  }
1088 
1089  USE_ASSERTS && assert($ypos != $this->seq[$end]);
1090 
1091  $this->in_seq[$this->seq[$end]] = false;
1092  $this->seq[$end] = $ypos;
1093  $this->in_seq[$ypos] = 1;
1094  //wfProfileOut( $fname );
1095  return $end;
1096  }
1097 
1098  /* Find LCS of two sequences.
1099  *
1100  * The results are recorded in the vectors $this->{x,y}changed[], by
1101  * storing a 1 in the element for each line that is an insertion
1102  * or deletion (ie. is not in the LCS).
1103  *
1104  * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
1105  *
1106  * Note that XLIM, YLIM are exclusive bounds.
1107  * All line numbers are origin-0 and discarded lines are not counted.
1108  */
1109  public function _compareseq($xoff, $xlim, $yoff, $ylim)
1110  {
1111  $fname = '_DiffEngine::_compareseq';
1112  //wfProfileIn( $fname );
1113 
1114  // Slide down the bottom initial diagonal.
1115  while ($xoff < $xlim && $yoff < $ylim
1116  && $this->xv[$xoff] == $this->yv[$yoff]) {
1117  ++$xoff;
1118  ++$yoff;
1119  }
1120 
1121  // Slide up the top initial diagonal.
1122  while ($xlim > $xoff && $ylim > $yoff
1123  && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
1124  --$xlim;
1125  --$ylim;
1126  }
1127 
1128  if ($xoff == $xlim || $yoff == $ylim) {
1129  $lcs = 0;
1130  } else {
1131  // This is ad hoc but seems to work well.
1132  //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
1133  //$nchunks = max(2,min(8,(int)$nchunks));
1134  $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
1135  list($lcs, $seps)
1136  = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
1137  }
1138 
1139  if ($lcs == 0) {
1140  // X and Y sequences have no common subsequence:
1141  // mark all changed.
1142  while ($yoff < $ylim) {
1143  $this->ychanged[$this->yind[$yoff++]] = 1;
1144  }
1145  while ($xoff < $xlim) {
1146  $this->xchanged[$this->xind[$xoff++]] = 1;
1147  }
1148  } else {
1149  // Use the partitions to split this problem into subproblems.
1150  reset($seps);
1151  $pt1 = $seps[0];
1152  while ($pt2 = next($seps)) {
1153  $this->_compareseq($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
1154  $pt1 = $pt2;
1155  }
1156  }
1157  //wfProfileOut( $fname );
1158  }
1159 
1160  /* Adjust inserts/deletes of identical lines to join changes
1161  * as much as possible.
1162  *
1163  * We do something when a run of changed lines include a
1164  * line at one end and has an excluded, identical line at the other.
1165  * We are free to choose which identical line is included.
1166  * `compareseq' usually chooses the one at the beginning,
1167  * but usually it is cleaner to consider the following identical line
1168  * to be the "change".
1169  *
1170  * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
1171  */
1172  public function _shift_boundaries($lines, &$changed, $other_changed)
1173  {
1174  $fname = '_DiffEngine::_shift_boundaries';
1175  //wfProfileIn( $fname );
1176  $i = 0;
1177  $j = 0;
1178 
1179  USE_ASSERTS && assert(sizeof($lines) == sizeof($changed));
1180  $len = sizeof($lines);
1181  $other_len = sizeof($other_changed);
1182 
1183  while (1) {
1184  /*
1185  * Scan forwards to find beginning of another run of changes.
1186  * Also keep track of the corresponding point in the other file.
1187  *
1188  * Throughout this code, $i and $j are adjusted together so that
1189  * the first $i elements of $changed and the first $j elements
1190  * of $other_changed both contain the same number of zeros
1191  * (unchanged lines).
1192  * Furthermore, $j is always kept so that $j == $other_len or
1193  * $other_changed[$j] == false.
1194  */
1195  while ($j < $other_len && $other_changed[$j]) {
1196  $j++;
1197  }
1198 
1199  while ($i < $len && !$changed[$i]) {
1200  USE_ASSERTS && assert($j < $other_len && !$other_changed[$j]);
1201  $i++;
1202  $j++;
1203  while ($j < $other_len && $other_changed[$j]) {
1204  $j++;
1205  }
1206  }
1207 
1208  if ($i == $len) {
1209  break;
1210  }
1211 
1212  $start = $i;
1213 
1214  // Find the end of this run of changes.
1215  while (++$i < $len && $changed[$i]) {
1216  continue;
1217  }
1218 
1219  do {
1220  /*
1221  * Record the length of this run of changes, so that
1222  * we can later determine whether the run has grown.
1223  */
1224  $runlength = $i - $start;
1225 
1226  /*
1227  * Move the changed region back, so long as the
1228  * previous unchanged line matches the last changed one.
1229  * This merges with previous changed regions.
1230  */
1231  while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
1232  $changed[--$start] = 1;
1233  $changed[--$i] = false;
1234  while ($start > 0 && $changed[$start - 1]) {
1235  $start--;
1236  }
1237  USE_ASSERTS && assert($j > 0);
1238  while ($other_changed[--$j]) {
1239  continue;
1240  }
1241  USE_ASSERTS && assert($j >= 0 && !$other_changed[$j]);
1242  }
1243 
1244  /*
1245  * Set CORRESPONDING to the end of the changed run, at the last
1246  * point where it corresponds to a changed run in the other file.
1247  * CORRESPONDING == LEN means no such point has been found.
1248  */
1249  $corresponding = $j < $other_len ? $i : $len;
1250 
1251  /*
1252  * Move the changed region forward, so long as the
1253  * first changed line matches the following unchanged one.
1254  * This merges with following changed regions.
1255  * Do this second, so that if there are no merges,
1256  * the changed region is moved forward as far as possible.
1257  */
1258  while ($i < $len && $lines[$start] == $lines[$i]) {
1259  $changed[$start++] = false;
1260  $changed[$i++] = 1;
1261  while ($i < $len && $changed[$i]) {
1262  $i++;
1263  }
1264 
1265  USE_ASSERTS && assert($j < $other_len && !$other_changed[$j]);
1266  $j++;
1267  if ($j < $other_len && $other_changed[$j]) {
1268  $corresponding = $i;
1269  while ($j < $other_len && $other_changed[$j]) {
1270  $j++;
1271  }
1272  }
1273  }
1274  } while ($runlength != $i - $start);
1275 
1276  /*
1277  * If possible, move the fully-merged run of changes
1278  * back to a corresponding run in the other file.
1279  */
1280  while ($corresponding < $i) {
1281  $changed[--$start] = 1;
1282  $changed[--$i] = 0;
1283  USE_ASSERTS && assert($j > 0);
1284  while ($other_changed[--$j]) {
1285  continue;
1286  }
1287  USE_ASSERTS && assert($j >= 0 && !$other_changed[$j]);
1288  }
1289  }
1290  //wfProfileOut( $fname );
1291  }
1292 }
1293 
1300 class Diff
1301 {
1302  public $edits;
1303 
1312  public function __construct($from_lines, $to_lines)
1313  {
1314  $eng = new _DiffEngine;
1315  $this->edits = $eng->diff($from_lines, $to_lines);
1316  //$this->_check($from_lines, $to_lines);
1317  }
1318 
1329  public function reverse()
1330  {
1331  $rev = $this;
1332  $rev->edits = array();
1333  foreach ($this->edits as $edit) {
1334  $rev->edits[] = $edit->reverse();
1335  }
1336  return $rev;
1337  }
1338 
1344  public function isEmpty()
1345  {
1346  foreach ($this->edits as $edit) {
1347  if ($edit->type != 'copy') {
1348  return false;
1349  }
1350  }
1351  return true;
1352  }
1353 
1361  public function lcs()
1362  {
1363  $lcs = 0;
1364  foreach ($this->edits as $edit) {
1365  if ($edit->type == 'copy') {
1366  $lcs += sizeof($edit->orig);
1367  }
1368  }
1369  return $lcs;
1370  }
1371 
1380  public function orig()
1381  {
1382  $lines = array();
1383 
1384  foreach ($this->edits as $edit) {
1385  if ($edit->orig) {
1386  array_splice($lines, sizeof($lines), 0, $edit->orig);
1387  }
1388  }
1389  return $lines;
1390  }
1391 
1400  public function closing()
1401  {
1402  $lines = array();
1404  foreach ($this->edits as $edit) {
1405  if ($edit->closing) {
1406  array_splice($lines, sizeof($lines), 0, $edit->closing);
1407  }
1408  }
1409  return $lines;
1410  }
1411 
1417  public function _check($from_lines, $to_lines)
1418  {
1419  $fname = 'Diff::_check';
1420  //wfProfileIn( $fname );
1421  if (serialize($from_lines) != serialize($this->orig())) {
1422  trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
1423  }
1424  if (serialize($to_lines) != serialize($this->closing())) {
1425  trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
1426  }
1427 
1428  $rev = $this->reverse();
1429  if (serialize($to_lines) != serialize($rev->orig())) {
1430  trigger_error("Reversed original doesn't match", E_USER_ERROR);
1431  }
1432  if (serialize($from_lines) != serialize($rev->closing())) {
1433  trigger_error("Reversed closing doesn't match", E_USER_ERROR);
1434  }
1435 
1436 
1437  $prevtype = 'none';
1438  foreach ($this->edits as $edit) {
1439  if ($prevtype == $edit->type) {
1440  trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
1441  }
1442  $prevtype = $edit->type;
1443  }
1444 
1445  $lcs = $this->lcs();
1446  trigger_error('Diff okay: LCS = ' . $lcs, E_USER_NOTICE);
1447  //wfProfileOut( $fname );
1448  }
1449 }
1450 
1456 class MappedDiff extends Diff
1457 {
1481  public function __construct(
1482  $from_lines,
1483  $to_lines,
1484  $mapped_from_lines,
1485  $mapped_to_lines
1486  ) {
1487  $fname = 'MappedDiff::MappedDiff';
1488  //wfProfileIn( $fname );
1489 
1490  assert(sizeof($from_lines) == sizeof($mapped_from_lines));
1491  assert(sizeof($to_lines) == sizeof($mapped_to_lines));
1492 
1493  parent::__construct($mapped_from_lines, $mapped_to_lines);
1494 
1495  $xi = $yi = 0;
1496  for ($i = 0; $i < sizeof($this->edits); $i++) {
1497  $orig = &$this->edits[$i]->orig;
1498  if (is_array($orig)) {
1499  $orig = array_slice($from_lines, $xi, sizeof($orig));
1500  $xi += sizeof($orig);
1501  }
1502 
1503  $closing = &$this->edits[$i]->closing;
1504  if (is_array($closing)) {
1505  $closing = array_slice($to_lines, $yi, sizeof($closing));
1506  $yi += sizeof($closing);
1507  }
1508  }
1509  //wfProfileOut( $fname );
1510  }
1511 }
1512 
1523 class DiffFormatter
1524 {
1531  public $leading_context_lines = 0;
1532 
1539  public $trailing_context_lines = 0;
1540 
1547  public function format($diff)
1548  {
1549  $fname = 'DiffFormatter::format';
1550  //wfProfileIn( $fname );
1551 
1552  $xi = $yi = 1;
1553  $block = false;
1554  $context = array();
1555 
1556  $nlead = $this->leading_context_lines;
1557  $ntrail = $this->trailing_context_lines;
1558 
1559  $this->_start_diff();
1560 
1561  foreach ($diff->edits as $edit) {
1562  if ($edit->type == 'copy') {
1563  if (is_array($block)) {
1564  if (sizeof($edit->orig) <= $nlead + $ntrail) {
1565  $block[] = $edit;
1566  } else {
1567  if ($ntrail) {
1568  $context = array_slice($edit->orig, 0, $ntrail);
1569  $block[] = new _DiffOp_Copy($context);
1570  }
1571  $this->_block(
1572  $x0,
1573  $ntrail + $xi - $x0,
1574  $y0,
1575  $ntrail + $yi - $y0,
1576  $block
1577  );
1578  $block = false;
1579  }
1580  }
1581  $context = $edit->orig;
1582  } else {
1583  if (!is_array($block)) {
1584  $context = array_slice($context, sizeof($context) - $nlead);
1585  $x0 = $xi - sizeof($context);
1586  $y0 = $yi - sizeof($context);
1587  $block = array();
1588  if ($context) {
1589  $block[] = new _DiffOp_Copy($context);
1590  }
1591  }
1592  $block[] = $edit;
1593  }
1594 
1595  if ($edit->orig) {
1596  $xi += sizeof($edit->orig);
1597  }
1598  if ($edit->closing) {
1599  $yi += sizeof($edit->closing);
1600  }
1601  }
1602 
1603  if (is_array($block)) {
1604  $this->_block(
1605  $x0,
1606  $xi - $x0,
1607  $y0,
1608  $yi - $y0,
1609  $block
1610  );
1611  }
1612 
1613  $end = $this->_end_diff();
1614  //wfProfileOut( $fname );
1615  return $end;
1616  }
1617 
1618  public function _block($xbeg, $xlen, $ybeg, $ylen, &$edits)
1619  {
1620  $fname = 'DiffFormatter::_block';
1621  //wfProfileIn( $fname );
1622  $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
1623  foreach ($edits as $edit) {
1624  if ($edit->type == 'copy') {
1625  $this->_context($edit->orig);
1626  } elseif ($edit->type == 'add') {
1627  $this->_added($edit->closing);
1628  } elseif ($edit->type == 'delete') {
1629  $this->_deleted($edit->orig);
1630  } elseif ($edit->type == 'change') {
1631  $this->_changed($edit->orig, $edit->closing);
1632  } else {
1633  trigger_error('Unknown edit type', E_USER_ERROR);
1634  }
1635  }
1636  $this->_end_block();
1637  //wfProfileOut( $fname );
1638  }
1639 
1640  public function _start_diff()
1641  {
1642  ob_start();
1643  }
1644 
1645  public function _end_diff()
1646  {
1647  $val = ob_get_contents();
1648  ob_end_clean();
1649  return $val;
1650  }
1651 
1652  public function _block_header($xbeg, $xlen, $ybeg, $ylen)
1653  {
1654  if ($xlen > 1) {
1655  $xbeg .= "," . ($xbeg + $xlen - 1);
1656  }
1657  if ($ylen > 1) {
1658  $ybeg .= "," . ($ybeg + $ylen - 1);
1659  }
1660 
1661  return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
1662  }
1663 
1664  public function _start_block($header)
1665  {
1666  echo $header;
1667  }
1668 
1669  public function _end_block()
1670  {
1671  }
1672 
1673  public function _lines($lines, $prefix = ' ')
1674  {
1675  foreach ($lines as $line) {
1676  echo "$prefix $line\n";
1677  }
1678  }
1679 
1680  public function _context($lines)
1681  {
1682  $this->_lines($lines);
1683  }
1684 
1685  public function _added($lines)
1686  {
1687  $this->_lines($lines, '>');
1688  }
1689  public function _deleted($lines)
1690  {
1691  $this->_lines($lines, '<');
1692  }
1693 
1694  public function _changed($orig, $closing)
1695  {
1696  $this->_deleted($orig);
1697  echo "---\n";
1698  $this->_added($closing);
1699  }
1700 }
1702 
1708 define('NBSP', '&#160;'); // iso-8859-x non-breaking space.
1709 
1716 {
1717  public function __construct()
1718  {
1719  $this->_lines = array();
1720  $this->_line = '';
1721  $this->_group = '';
1722  $this->_tag = '';
1723  }
1724 
1725  public function _flushGroup($new_tag)
1726  {
1727  if ($this->_group !== '') {
1728  if ($this->_tag == 'ins') {
1729  $this->_line .= '[ilDiffInsStart]' .
1730  htmlspecialchars($this->_group) . '[ilDiffInsEnd]';
1731  } elseif ($this->_tag == 'del') {
1732  $this->_line .= '[ilDiffDelStart]' .
1733  htmlspecialchars($this->_group) . '[ilDiffDelEnd]';
1734  } else {
1735  $this->_line .= htmlspecialchars($this->_group);
1736  }
1737  }
1738  $this->_group = '';
1739  $this->_tag = $new_tag;
1740  }
1741 
1742  public function _flushLine($new_tag)
1743  {
1744  $this->_flushGroup($new_tag);
1745  if ($this->_line != '') {
1746  array_push($this->_lines, $this->_line);
1747  } else {
1748  # make empty lines visible by inserting an NBSP
1749  array_push($this->_lines, NBSP);
1750  }
1751  $this->_line = '';
1752  }
1753 
1754  public function addWords($words, $tag = '')
1755  {
1756  if ($tag != $this->_tag) {
1757  $this->_flushGroup($tag);
1758  }
1759 
1760  foreach ($words as $word) {
1761  // new-line should only come as first char of word.
1762  if ($word == '') {
1763  continue;
1764  }
1765  if ($word[0] == "\n") {
1766  $this->_flushLine($tag);
1767  $word = substr($word, 1);
1768  }
1769  assert(!strstr($word, "\n"));
1770  $this->_group .= $word;
1771  }
1772  }
1773 
1774  public function getLines()
1775  {
1776  $this->_flushLine('~done');
1777  return $this->_lines;
1778  }
1779 }
1780 
1786 class WordLevelDiff extends MappedDiff
1787 {
1788  const MAX_LINE_LENGTH = 10000;
1789 
1790  public function __construct($orig_lines, $closing_lines)
1791  {
1792  $fname = 'WordLevelDiff::WordLevelDiff';
1793  //wfProfileIn( $fname );
1794 
1795  list($orig_words, $orig_stripped) = $this->_split($orig_lines);
1796  list($closing_words, $closing_stripped) = $this->_split($closing_lines);
1797 
1799  $orig_words,
1800  $closing_words,
1801  $orig_stripped,
1802  $closing_stripped
1803  );
1804  //wfProfileOut( $fname );
1805  }
1806 
1807  public function _split($lines)
1808  {
1809  $fname = 'WordLevelDiff::_split';
1810  //wfProfileIn( $fname );
1811 
1812  $words = array();
1813  $stripped = array();
1814  $first = true;
1815  foreach ($lines as $line) {
1816  # If the line is too long, just pretend the entire line is one big word
1817  # This prevents resource exhaustion problems
1818  if ($first) {
1819  $first = false;
1820  } else {
1821  $words[] = "\n";
1822  $stripped[] = "\n";
1823  }
1824  if (strlen($line) > self::MAX_LINE_LENGTH) {
1825  $words[] = $line;
1826  $stripped[] = $line;
1827  } else {
1828  $m = array();
1829  if (preg_match_all(
1830  '/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
1831  $line,
1832  $m
1833  )) {
1834  $words = array_merge($words, $m[0]);
1835  $stripped = array_merge($stripped, $m[1]);
1836  }
1837  }
1838  }
1839  //wfProfileOut( $fname );
1840  return array($words, $stripped);
1841  }
1842 
1843  public function orig()
1844  {
1845  $fname = 'WordLevelDiff::orig';
1846  //wfProfileIn( $fname );
1848 
1849  foreach ($this->edits as $edit) {
1850  if ($edit->type == 'copy') {
1851  $orig->addWords($edit->orig);
1852  } elseif ($edit->orig) {
1853  $orig->addWords($edit->orig, 'del');
1854  }
1855  }
1856  $lines = $orig->getLines();
1857  //wfProfileOut( $fname );
1858  return $lines;
1859  }
1860 
1861  public function closing()
1862  {
1863  $fname = 'WordLevelDiff::closing';
1864  //wfProfileIn( $fname );
1865  $closing = new _HWLDF_WordAccumulator;
1866 
1867  foreach ($this->edits as $edit) {
1868  if ($edit->type == 'copy') {
1869  $closing->addWords($edit->closing);
1870  } elseif ($edit->closing) {
1871  $closing->addWords($edit->closing, 'ins');
1872  }
1873  }
1874  $lines = $closing->getLines();
1875  //wfProfileOut( $fname );
1876  return $lines;
1877  }
1878 }
1879 
1886 class TableDiffFormatter extends DiffFormatter
1888  public function __construct()
1889  {
1890  $this->leading_context_lines = 2;
1891  $this->trailing_context_lines = 2;
1892  }
1893 
1894  public function _block_header($xbeg, $xlen, $ybeg, $ylen)
1895  {
1896  $r = '<tr><td colspan="2" align="left"><strong><!--LINE ' . $xbeg . "--></strong></td>\n" .
1897  '<td colspan="2" align="left"><strong><!--LINE ' . $ybeg . "--></strong></td></tr>\n";
1898  return $r;
1899  }
1900 
1901  public function _start_block($header)
1902  {
1903  echo $header;
1904  }
1905 
1906  public function _end_block()
1907  {
1908  }
1909 
1910  public function _lines($lines, $prefix = ' ', $color = 'white')
1911  {
1912  }
1914  # HTML-escape parameter before calling this
1915  public function addedLine($line)
1916  {
1917  return "<td>+</td><td class='diff-addedline'>{$line}</td>";
1918  }
1919 
1920  # HTML-escape parameter before calling this
1921  public function deletedLine($line)
1922  {
1923  return "<td>-</td><td class='diff-deletedline'>{$line}</td>";
1924  }
1925 
1926  # HTML-escape parameter before calling this
1927  public function contextLine($line)
1928  {
1929  return "<td> </td><td class='diff-context'>{$line}</td>";
1930  }
1932  public function emptyLine()
1933  {
1934  return '<td colspan="2">&nbsp;</td>';
1935  }
1936 
1937  public function _added($lines)
1938  {
1939  foreach ($lines as $line) {
1940  echo '<tr>' . $this->emptyLine() .
1941  $this->addedLine(htmlspecialchars($line)) . "</tr>\n";
1942  }
1943  }
1944 
1945  public function _deleted($lines)
1946  {
1947  foreach ($lines as $line) {
1948  echo '<tr>' . $this->deletedLine(htmlspecialchars($line)) .
1949  $this->emptyLine() . "</tr>\n";
1950  }
1951  }
1952 
1953  public function _context($lines)
1954  {
1955  foreach ($lines as $line) {
1956  echo '<tr>' .
1957  $this->contextLine(htmlspecialchars($line)) .
1958  $this->contextLine(htmlspecialchars($line)) . "</tr>\n";
1959  }
1960  }
1961 
1962  public function _changed($orig, $closing)
1963  {
1964  $fname = 'TableDiffFormatter::_changed';
1965  //wfProfileIn( $fname );
1966 
1967  $diff = new WordLevelDiff($orig, $closing);
1968  $del = $diff->orig();
1969  $add = $diff->closing();
1970 
1971  # Notice that WordLevelDiff returns HTML-escaped output.
1972  # Hence, we will be calling addedLine/deletedLine without HTML-escaping.
1973 
1974  while ($line = array_shift($del)) {
1975  $aline = array_shift($add);
1976  echo '<tr>' . $this->deletedLine($line) .
1977  $this->addedLine($aline) . "</tr>\n";
1978  }
1979  foreach ($add as $line) { # If any leftovers
1980  echo '<tr>' . $this->emptyLine() .
1981  $this->addedLine($line) . "</tr>\n";
1982  }
1983  //wfProfileOut( $fname );
1984  }
1985 }
getDiffBody()
Get the diff table body, without header Results are cached Returns false on error.
exit
Definition: login.php:29
diff($from_lines, $to_lines)
$context
Definition: webdav.php:26
showDiff($otitle, $ntitle)
Get the diff text, send it to $wgOut Returns false if the diff could not be generated, otherwise returns true.
localiseLineNumbers($text)
Replace line numbers with the text in the user&#39;s language.
$type
const NS_SPECIAL
Definition: Title.php:16
generateDiffBody($otext, $ntext)
Generate a diff, no caching $otext and $ntext must be already segmented.
addHeader($diff, $otitle, $ntitle, $multi='')
Add the header to a diff body.
showDiffPage($diffOnly=false)
const USE_ASSERTS
renderNewRevision()
Show the new revision of the page.
wfMsg($x)
Definition: RandomTest.php:63
getMultiNotice()
If there are revisions between the ones being compared, return a note saying so.
_block($xbeg, $xlen, $ybeg, $ylen, &$edits)
$n
Definition: RandomTest.php:85
__construct($titleObj=null, $old=0, $new=0, $rcid=0)
#-
setText($oldText, $newText)
Use specified text instead of loading from the database.
foreach($mandatory_scripts as $file) $timestamp
Definition: buildRTE.php:81
__construct(Container $dic, ilPlugin $plugin)
loadNewText()
Load the text of the new revision, not the old one.
showFirstRevision()
Show the first revision of an article.
const NBSP
Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3.
loadRevisionData()
Load revision metadata for the specified articles.
$i
Definition: metadata.php:24
loadText()
Load the text of the revisions, as well as revision data.
getDiff($otitle, $ntitle)
Get diff table, including header Note that the interface has changed, it&#39;s no longer static...