ILIAS  release_5-3 Revision v5.3.23-19-g915713cf615
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  while (list($junk, $y) = each($matches)) {
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  while (list( /* $junk */, $y) = each($matches)) {
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 
1798  parent::__construct(
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 }
File written to
getDiffBody()
Get the diff table body, without header Results are cached Returns false on error.
diff($from_lines, $to_lines)
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
$x
Definition: example_009.php:98
$end
Definition: saml1-acs.php:18
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.
$r
Definition: example_031.php:79
wfMsg($x)
Definition: RandomTest.php:63
$y
Definition: example_007.php:83
getMultiNotice()
If there are revisions between the ones being compared, return a note saying so.
const NS_SPECIAL
Definition: Title.php:16
_block($xbeg, $xlen, $ybeg, $ylen, &$edits)
$text
Definition: errorreport.php:18
Add a drawing to the header
Definition: 04printing.php:69
$n
Definition: RandomTest.php:85
$old
__construct($titleObj=null, $old=0, $new=0, $rcid=0)
#-
Create styles array
The data for the language used.
setText($oldText, $newText)
Use specified text instead of loading from the database.
foreach($mandatory_scripts as $file) $timestamp
Definition: buildRTE.php:81
Write to Excel2007 format
loadNewText()
Load the text of the new revision, not the old one.
$i
Definition: disco.tpl.php:19
showFirstRevision()
Show the first revision of an article.
const NBSP
Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3.
$key
Definition: croninfo.php:18
loadRevisionData()
Load revision metadata for the specified articles.
if(function_exists('posix_getuid') &&posix_getuid()===0) if(!array_key_exists('t', $options)) $tag
Definition: cron.php:35
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...