ILIAS  release_4-4 Revision
All Data Structures Namespaces Files Functions Variables Modules Pages
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 
27  var $mRevisionsLoaded = false; // Have the revisions been loaded
28  var $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2?
38  function DifferenceEngine( $titleObj = null, $old = 0, $new = 0, $rcid = 0 ) {
39  $this->mTitle = $titleObj;
40  wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
41 
42  if ( 'prev' === $new ) {
43  # Show diff between revision $old and the previous one.
44  # Get previous one from DB.
45  #
46  $this->mNewid = intval($old);
47 
48  $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid );
49 
50  } elseif ( 'next' === $new ) {
51  # Show diff between revision $old and the previous one.
52  # Get previous one from DB.
53  #
54  $this->mOldid = intval($old);
55  $this->mNewid = $this->mTitle->getNextRevisionID( $this->mOldid );
56  if ( false === $this->mNewid ) {
57  # if no result, NewId points to the newest old revision. The only newer
58  # revision is cur, which is "0".
59  $this->mNewid = 0;
60  }
61 
62  } else {
63  $this->mOldid = intval($old);
64  $this->mNewid = intval($new);
65  }
66  $this->mRcidMarkPatrolled = intval($rcid); # force it to be an integer
67  }
68 
69  function showDiffPage( $diffOnly = false ) {
70  global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol;
71  $fname = 'DifferenceEngine::showDiffPage';
72  //wfProfileIn( $fname );
73 
74  # If external diffs are enabled both globally and for the user,
75  # we'll use the application/x-external-editor interface to call
76  # an external diff tool like kompare, kdiff3, etc.
77  if($wgUseExternalEditor && $wgUser->getOption('externaldiff')) {
78  global $wgInputEncoding,$wgServer,$wgScript,$wgLang;
79  $wgOut->disable();
80  header ( "Content-type: application/x-external-editor; charset=".$wgInputEncoding );
81  $url1=$this->mTitle->getFullURL("action=raw&oldid=".$this->mOldid);
82  $url2=$this->mTitle->getFullURL("action=raw&oldid=".$this->mNewid);
83  $special=$wgLang->getNsText(NS_SPECIAL);
84  $control=<<<CONTROL
85 [Process]
86 Type=Diff text
87 Engine=MediaWiki
88 Script={$wgServer}{$wgScript}
89 Special namespace={$special}
90 
91 [File]
92 Extension=wiki
93 URL=$url1
94 
95 [File 2]
96 Extension=wiki
97 URL=$url2
98 CONTROL;
99  echo($control);
100  return;
101  }
102 
103  $wgOut->setArticleFlag( false );
104  if ( ! $this->loadRevisionData() ) {
105  $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, {$this->mNewid})";
106  $mtext = wfMsg( 'missingarticle', "<nowiki>$t</nowiki>" );
107  $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
108  $wgOut->addWikitext( $mtext );
109  //wfProfileOut( $fname );
110  return;
111  }
112 
113  wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
114 
115  if ( $this->mNewRev->isCurrent() ) {
116  $wgOut->setArticleFlag( true );
117  }
118 
119  # mOldid is false if the difference engine is called with a "vague" query for
120  # a diff between a version V and its previous version V' AND the version V
121  # is the first version of that article. In that case, V' does not exist.
122  if ( $this->mOldid === false ) {
123  $this->showFirstRevision();
124  $this->renderNewRevision(); // should we respect $diffOnly here or not?
125  //wfProfileOut( $fname );
126  return;
127  }
128 
129  $wgOut->suppressQuickbar();
130 
131  $oldTitle = $this->mOldPage->getPrefixedText();
132  $newTitle = $this->mNewPage->getPrefixedText();
133  if( $oldTitle == $newTitle ) {
134  $wgOut->setPageTitle( $newTitle );
135  } else {
136  $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle );
137  }
138  $wgOut->setSubtitle( wfMsg( 'difference' ) );
139  $wgOut->setRobotpolicy( 'noindex,nofollow' );
140 
141  if ( !( $this->mOldPage->userCanRead() && $this->mNewPage->userCanRead() ) ) {
142  $wgOut->loginToUse();
143  $wgOut->output();
144  //wfProfileOut( $fname );
145  exit;
146  }
147 
148  $sk = $wgUser->getSkin();
149 
150  if ( $this->mNewRev->isCurrent() && $wgUser->isAllowed('rollback') ) {
151  $rollback = '&nbsp;&nbsp;&nbsp;' . $sk->generateRollback( $this->mNewRev );
152  } else {
153  $rollback = '';
154  }
155  if( $wgUseRCPatrol && $this->mRcidMarkPatrolled != 0 && $wgUser->isAllowed( 'patrol' ) ) {
156  $patrol = ' [' . $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'markaspatrolleddiff' ), "action=markpatrolled&rcid={$this->mRcidMarkPatrolled}" ) . ']';
157  } else {
158  $patrol = '';
159  }
160 
161  $prevlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousdiff' ),
162  'diff=prev&oldid='.$this->mOldid, '', '', 'id="differences-prevlink"' );
163  if ( $this->mNewRev->isCurrent() ) {
164  $nextlink = '&nbsp;';
165  } else {
166  $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ),
167  'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' );
168  }
169 
170  $oldminor = '';
171  $newminor = '';
172 
173  if ($this->mOldRev->mMinorEdit == 1) {
174  $oldminor = wfElement( 'span', array( 'class' => 'minor' ),
175  wfMsg( 'minoreditletter') ) . ' ';
176  }
177 
178  if ($this->mNewRev->mMinorEdit == 1) {
179  $newminor = wfElement( 'span', array( 'class' => 'minor' ),
180  wfMsg( 'minoreditletter') ) . ' ';
181  }
182 
183  $oldHeader = "<strong>{$this->mOldtitle}</strong><br />" .
184  $sk->revUserTools( $this->mOldRev ) . "<br />" .
185  $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly ) . "<br />" .
186  $prevlink;
187  $newHeader = "<strong>{$this->mNewtitle}</strong><br />" .
188  $sk->revUserTools( $this->mNewRev ) . " $rollback<br />" .
189  $newminor . $sk->revComment( $this->mNewRev, !$diffOnly ) . "<br />" .
190  $nextlink . $patrol;
191 
192  $this->showDiff( $oldHeader, $newHeader );
193 
194  if ( !$diffOnly )
195  $this->renderNewRevision();
196 
197  //wfProfileOut( $fname );
198  }
199 
203  function renderNewRevision() {
204  global $wgOut;
205  $fname = 'DifferenceEngine::renderNewRevision';
206  //wfProfileIn( $fname );
207 
208  $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
209  #add deleted rev tag if needed
210  if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
211  $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) );
212  }
213 
214  if( !$this->mNewRev->isCurrent() ) {
215  $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
216  }
217 
218  $this->loadNewText();
219  if( is_object( $this->mNewRev ) ) {
220  $wgOut->setRevisionId( $this->mNewRev->getId() );
221  }
222 
223  $wgOut->addWikiTextTidy( $this->mNewtext );
224 
225  if( !$this->mNewRev->isCurrent() ) {
226  $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
227  }
228 
229  //wfProfileOut( $fname );
230  }
231 
236  function showFirstRevision() {
237  global $wgOut, $wgUser;
238 
239  $fname = 'DifferenceEngine::showFirstRevision';
240  //wfProfileIn( $fname );
241 
242  # Get article text from the DB
243  #
244  if ( ! $this->loadNewText() ) {
245  $t = $this->mTitle->getPrefixedText() . " (Diff: {$this->mOldid}, " .
246  "{$this->mNewid})";
247  $mtext = wfMsg( 'missingarticle', "<nowiki>$t</nowiki>" );
248  $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
249  $wgOut->addWikitext( $mtext );
250  //wfProfileOut( $fname );
251  return;
252  }
253  if ( $this->mNewRev->isCurrent() ) {
254  $wgOut->setArticleFlag( true );
255  }
256 
257  # Check if user is allowed to look at this page. If not, bail out.
258  #
259  if ( !( $this->mTitle->userCanRead() ) ) {
260  $wgOut->loginToUse();
261  $wgOut->output();
262  //wfProfileOut( $fname );
263  exit;
264  }
265 
266  # Prepare the header box
267  #
268  $sk = $wgUser->getSkin();
269 
270  $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' );
271  $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\"><strong>{$this->mOldtitle}</strong><br />" .
272  $sk->revUserTools( $this->mNewRev ) . "<br />" .
273  $sk->revComment( $this->mNewRev ) . "<br />" .
274  $nextlink . "</div>\n";
275 
276  $wgOut->addHTML( $header );
277 
278  $wgOut->setSubtitle( wfMsg( 'difference' ) );
279  $wgOut->setRobotpolicy( 'noindex,nofollow' );
280 
281  //wfProfileOut( $fname );
282  }
283 
288  function showDiff( $otitle, $ntitle ) {
289  global $wgOut;
290  $diff = $this->getDiff( $otitle, $ntitle );
291  if ( $diff === false ) {
292  $wgOut->addWikitext( wfMsg( 'missingarticle', "<nowiki>(fixme, bug)</nowiki>" ) );
293  return false;
294  } else {
295  $wgOut->addHTML( $diff );
296  return true;
297  }
298  }
299 
305  function getDiff( $otitle, $ntitle ) {
306  $body = $this->getDiffBody();
307  if ( $body === false ) {
308  return false;
309  } else {
310  $multi = $this->getMultiNotice();
311  return $this->addHeader( $body, $otitle, $ntitle, $multi );
312  }
313  }
314 
320  function getDiffBody() {
321  global $wgMemc;
322  $fname = 'DifferenceEngine::getDiffBody';
323  //wfProfileIn( $fname );
324 
325  // Cacheable?
326  $key = false;
327  if ( $this->mOldid && $this->mNewid ) {
328  // Try cache
329  $key = wfMemcKey( 'diff', 'oldid', $this->mOldid, 'newid', $this->mNewid );
330  $difftext = $wgMemc->get( $key );
331  if ( $difftext ) {
332  wfIncrStats( 'diff_cache_hit' );
333  $difftext = $this->localiseLineNumbers( $difftext );
334  $difftext .= "\n<!-- diff cache key $key -->\n";
335  //wfProfileOut( $fname );
336  return $difftext;
337  }
338  }
339 
340  #loadtext is permission safe, this just clears out the diff
341  if ( !$this->loadText() ) {
342  //wfProfileOut( $fname );
343  return false;
344  } else if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
345  return '';
346  } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
347  return '';
348  }
349 
350  $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
351 
352  // Save to cache for 7 days
353  if ( $key !== false && $difftext !== false ) {
354  wfIncrStats( 'diff_cache_miss' );
355  $wgMemc->set( $key, $difftext, 7*86400 );
356  } else {
357  wfIncrStats( 'diff_uncacheable' );
358  }
359  // Replace line numbers with the text in the user's language
360  if ( $difftext !== false ) {
361  $difftext = $this->localiseLineNumbers( $difftext );
362  }
363  //wfProfileOut( $fname );
364  return $difftext;
365  }
366 
371  function generateDiffBody( $otext, $ntext ) {
372  global $wgExternalDiffEngine, $wgContLang;
373  $fname = 'DifferenceEngine::generateDiffBody';
374 
375  $otext = str_replace( "\r\n", "\n", $otext );
376  $ntext = str_replace( "\r\n", "\n", $ntext );
377 
378  if ( $wgExternalDiffEngine == 'wikidiff' ) {
379  # For historical reasons, external diff engine expects
380  # input text to be HTML-escaped already
381  $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) );
382  $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) );
383  if( !function_exists( 'wikidiff_do_diff' ) ) {
384  dl('php_wikidiff.so');
385  }
386  return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) );
387  }
388 
389  if ( $wgExternalDiffEngine == 'wikidiff2' ) {
390  # Better external diff engine, the 2 may some day be dropped
391  # This one does the escaping and segmenting itself
392  if ( !function_exists( 'wikidiff2_do_diff' ) ) {
393  //wfProfileIn( "$fname-dl" );
394  @dl('php_wikidiff2.so');
395  //wfProfileOut( "$fname-dl" );
396  }
397  if ( function_exists( 'wikidiff2_do_diff' ) ) {
398  //wfProfileIn( 'wikidiff2_do_diff' );
399  $text = wikidiff2_do_diff( $otext, $ntext, 2 );
400  //wfProfileOut( 'wikidiff2_do_diff' );
401  return $text;
402  }
403  }
404  if ( $wgExternalDiffEngine !== false ) {
405  # Diff via the shell
406  global $wgTmpDirectory;
407  $tempName1 = tempnam( $wgTmpDirectory, 'diff_' );
408  $tempName2 = tempnam( $wgTmpDirectory, 'diff_' );
409 
410  $tempFile1 = fopen( $tempName1, "w" );
411  if ( !$tempFile1 ) {
412  //wfProfileOut( $fname );
413  return false;
414  }
415  $tempFile2 = fopen( $tempName2, "w" );
416  if ( !$tempFile2 ) {
417  //wfProfileOut( $fname );
418  return false;
419  }
420  fwrite( $tempFile1, $otext );
421  fwrite( $tempFile2, $ntext );
422  fclose( $tempFile1 );
423  fclose( $tempFile2 );
424  $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
425  //wfProfileIn( "$fname-shellexec" );
426  $difftext = wfShellExec( $cmd );
427  //wfProfileOut( "$fname-shellexec" );
428  unlink( $tempName1 );
429  unlink( $tempName2 );
430  return $difftext;
431  }
432 
433  # Native PHP diff
434  $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
435  $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
436  $diffs = new Diff( $ota, $nta );
437  $formatter = new TableDiffFormatter();
438  return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) );
439  }
440 
441 
445  function localiseLineNumbers( $text ) {
446  return preg_replace_callback( '/<!--LINE (\d+)-->/',
447  array( &$this, 'localiseLineNumbersCb' ), $text );
448  }
449 
450  function localiseLineNumbersCb( $matches ) {
451  global $wgLang;
452  return wfMsgExt( 'lineno', array('parseinline'), $wgLang->formatNum( $matches[1] ) );
453  }
454 
455 
459  function getMultiNotice() {
460  if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) )
461  return '';
462 
463  if( !$this->mOldPage->equals( $this->mNewPage ) ) {
464  // Comparing two different pages? Count would be meaningless.
465  return '';
466  }
467 
468  $oldid = $this->mOldRev->getId();
469  $newid = $this->mNewRev->getId();
470  if ( $oldid > $newid ) {
471  $tmp = $oldid; $oldid = $newid; $newid = $tmp;
472  }
473 
474  $n = $this->mTitle->countRevisionsBetween( $oldid, $newid );
475  if ( !$n )
476  return '';
477 
478  return wfMsgExt( 'diff-multi', array( 'parseinline' ), $n );
479  }
480 
481 
485  function addHeader( $diff, $otitle, $ntitle, $multi = '' ) {
486  global $wgOut;
487 
488  if ( $this->mOldRev && $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) {
489  $otitle = '<span class="history-deleted">'.$otitle.'</span>';
490  }
491  if ( $this->mNewRev && $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
492  $ntitle = '<span class="history-deleted">'.$ntitle.'</span>';
493  }
494  $header = "
495  <table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>
496  <tr>
497  <td colspan='2' width='50%' align='center' class='diff-otitle'>{$otitle}</td>
498  <td colspan='2' width='50%' align='center' class='diff-ntitle'>{$ntitle}</td>
499  </tr>
500  ";
501 
502  if ( $multi != '' )
503  $header .= "<tr><td colspan='4' align='center' class='diff-multi'>{$multi}</td></tr>";
504 
505  return $header . $diff . "</table>";
506  }
507 
511  function setText( $oldText, $newText ) {
512  $this->mOldtext = $oldText;
513  $this->mNewtext = $newText;
514  $this->mTextLoaded = 2;
515  }
516 
527  function loadRevisionData() {
528  global $wgLang;
529  if ( $this->mRevisionsLoaded ) {
530  return true;
531  } else {
532  // Whether it succeeds or fails, we don't want to try again
533  $this->mRevisionsLoaded = true;
534  }
535 
536  // Load the new revision object
537  if( $this->mNewid ) {
538  $this->mNewRev = Revision::newFromId( $this->mNewid );
539  } else {
540  $this->mNewRev = Revision::newFromTitle( $this->mTitle );
541  }
542 
543  if( is_null( $this->mNewRev ) ) {
544  return false;
545  }
546 
547  // Set assorted variables
548  $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true );
549  $this->mNewPage = $this->mNewRev->getTitle();
550  if( $this->mNewRev->isCurrent() ) {
551  $newLink = $this->mNewPage->escapeLocalUrl();
552  $this->mPagetitle = htmlspecialchars( wfMsg( 'currentrev' ) );
553  $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit' );
554 
555  $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)"
556  . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)";
557 
558  } else {
559  $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid );
560  $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mNewid );
561  $this->mPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $timestamp ) );
562 
563  $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>"
564  . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)";
565  }
566 
567  // Load the old revision object
568  $this->mOldRev = false;
569  if( $this->mOldid ) {
570  $this->mOldRev = Revision::newFromId( $this->mOldid );
571  } elseif ( $this->mOldid === 0 ) {
572  $rev = $this->mNewRev->getPrevious();
573  if( $rev ) {
574  $this->mOldid = $rev->getId();
575  $this->mOldRev = $rev;
576  } else {
577  // No previous revision; mark to show as first-version only.
578  $this->mOldid = false;
579  $this->mOldRev = false;
580  }
581  }/* elseif ( $this->mOldid === false ) leave mOldRev false; */
582 
583  if( is_null( $this->mOldRev ) ) {
584  return false;
585  }
586 
587  if ( $this->mOldRev ) {
588  $this->mOldPage = $this->mOldRev->getTitle();
589 
590  $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true );
591  $oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid );
592  $oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid );
593  $this->mOldtitle = "<a href='$oldLink'>" . htmlspecialchars( wfMsg( 'revisionasof', $t ) )
594  . "</a> (<a href='$oldEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)";
595  //now that we considered old rev, we can make undo link (bug 8133, multi-edit undo)
596  $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid);
597  $this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)";
598  }
599 
600  return true;
601  }
602 
606  function loadText() {
607  if ( $this->mTextLoaded == 2 ) {
608  return true;
609  } else {
610  // Whether it succeeds or fails, we don't want to try again
611  $this->mTextLoaded = 2;
612  }
613 
614  if ( !$this->loadRevisionData() ) {
615  return false;
616  }
617  if ( $this->mOldRev ) {
618  // FIXME: permission tests
619  $this->mOldtext = $this->mOldRev->revText();
620  if ( $this->mOldtext === false ) {
621  return false;
622  }
623  }
624  if ( $this->mNewRev ) {
625  $this->mNewtext = $this->mNewRev->revText();
626  if ( $this->mNewtext === false ) {
627  return false;
628  }
629  }
630  return true;
631  }
632 
636  function loadNewText() {
637  if ( $this->mTextLoaded >= 1 ) {
638  return true;
639  } else {
640  $this->mTextLoaded = 1;
641  }
642  if ( !$this->loadRevisionData() ) {
643  return false;
644  }
645  $this->mNewtext = $this->mNewRev->getText();
646  return true;
647  }
648 
649 
650 }
652 // A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
653 //
654 // Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
655 // You may copy this code freely under the conditions of the GPL.
656 //
657 
658 define('USE_ASSERTS', function_exists('assert'));
659 
665 class _DiffOp {
666  var $type;
667  var $orig;
668  var $closing;
669 
670  function reverse() {
671  trigger_error('pure virtual', E_USER_ERROR);
672  }
673 
674  function norig() {
675  return $this->orig ? sizeof($this->orig) : 0;
676  }
678  function nclosing() {
679  return $this->closing ? sizeof($this->closing) : 0;
680  }
681 }
682 
688 class _DiffOp_Copy extends _DiffOp {
689  var $type = 'copy';
690 
691  function _DiffOp_Copy ($orig, $closing = false) {
692  if (!is_array($closing))
693  $closing = $orig;
694  $this->orig = $orig;
695  $this->closing = $closing;
696  }
698  function reverse() {
699  return new _DiffOp_Copy($this->closing, $this->orig);
700  }
701 }
708 class _DiffOp_Delete extends _DiffOp {
709  var $type = 'delete';
710 
711  function _DiffOp_Delete ($lines) {
712  $this->orig = $lines;
713  $this->closing = false;
714  }
716  function reverse() {
717  return new _DiffOp_Add($this->orig);
718  }
719 }
726 class _DiffOp_Add extends _DiffOp {
727  var $type = 'add';
728 
729  function _DiffOp_Add ($lines) {
730  $this->closing = $lines;
731  $this->orig = false;
732  }
734  function reverse() {
735  return new _DiffOp_Delete($this->closing);
736  }
737 }
744 class _DiffOp_Change extends _DiffOp {
745  var $type = 'change';
746 
747  function _DiffOp_Change ($orig, $closing) {
748  $this->orig = $orig;
749  $this->closing = $closing;
750  }
751 
752  function reverse() {
753  return new _DiffOp_Change($this->closing, $this->orig);
754  }
755 }
756 
757 
781 class _DiffEngine
782 {
783  const MAX_XREF_LENGTH = 10000;
784 
785  function diff ($from_lines, $to_lines) {
786  $fname = '_DiffEngine::diff';
787  //wfProfileIn( $fname );
788 
789  $n_from = sizeof($from_lines);
790  $n_to = sizeof($to_lines);
791 
792  $this->xchanged = $this->ychanged = array();
793  $this->xv = $this->yv = array();
794  $this->xind = $this->yind = array();
795  unset($this->seq);
796  unset($this->in_seq);
797  unset($this->lcs);
798 
799  // Skip leading common lines.
800  for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
801  if ($from_lines[$skip] !== $to_lines[$skip])
802  break;
803  $this->xchanged[$skip] = $this->ychanged[$skip] = false;
804  }
805  // Skip trailing common lines.
806  $xi = $n_from; $yi = $n_to;
807  for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
808  if ($from_lines[$xi] !== $to_lines[$yi])
809  break;
810  $this->xchanged[$xi] = $this->ychanged[$yi] = false;
811  }
812 
813  // Ignore lines which do not exist in both files.
814  for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
815  $xhash[$this->_line_hash($from_lines[$xi])] = 1;
816  }
817 
818  for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
819  $line = $to_lines[$yi];
820  if ( ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) )
821  continue;
822  $yhash[$this->_line_hash($line)] = 1;
823  $this->yv[] = $line;
824  $this->yind[] = $yi;
825  }
826  for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
827  $line = $from_lines[$xi];
828  if ( ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) )
829  continue;
830  $this->xv[] = $line;
831  $this->xind[] = $xi;
832  }
833 
834  // Find the LCS.
835  $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
836 
837  // Merge edits when possible
838  $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
839  $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
840 
841  // Compute the edit operations.
842  $edits = array();
843  $xi = $yi = 0;
844  while ($xi < $n_from || $yi < $n_to) {
845  USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
846  USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
847 
848  // Skip matching "snake".
849  $copy = array();
850  while ( $xi < $n_from && $yi < $n_to
851  && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
852  $copy[] = $from_lines[$xi++];
853  ++$yi;
854  }
855  if ($copy)
856  $edits[] = new _DiffOp_Copy($copy);
857 
858  // Find deletes & adds.
859  $delete = array();
860  while ($xi < $n_from && $this->xchanged[$xi])
861  $delete[] = $from_lines[$xi++];
862 
863  $add = array();
864  while ($yi < $n_to && $this->ychanged[$yi])
865  $add[] = $to_lines[$yi++];
866 
867  if ($delete && $add)
868  $edits[] = new _DiffOp_Change($delete, $add);
869  elseif ($delete)
870  $edits[] = new _DiffOp_Delete($delete);
871  elseif ($add)
872  $edits[] = new _DiffOp_Add($add);
873  }
874  //wfProfileOut( $fname );
875  return $edits;
876  }
877 
881  function _line_hash( $line ) {
882  if ( strlen( $line ) > self::MAX_XREF_LENGTH ) {
883  return md5( $line );
884  } else {
885  return $line;
886  }
887  }
888 
889 
890  /* Divide the Largest Common Subsequence (LCS) of the sequences
891  * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
892  * sized segments.
893  *
894  * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an
895  * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
896  * sub sequences. The first sub-sequence is contained in [X0, X1),
897  * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on. Note
898  * that (X0, Y0) == (XOFF, YOFF) and
899  * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
900  *
901  * This function assumes that the first lines of the specified portions
902  * of the two files do not match, and likewise that the last lines do not
903  * match. The caller must trim matching lines from the beginning and end
904  * of the portions it is going to specify.
905  */
906  function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) {
907  $fname = '_DiffEngine::_diag';
908  //wfProfileIn( $fname );
909  $flip = false;
910 
911  if ($xlim - $xoff > $ylim - $yoff) {
912  // Things seems faster (I'm not sure I understand why)
913  // when the shortest sequence in X.
914  $flip = true;
915  list ($xoff, $xlim, $yoff, $ylim)
916  = array( $yoff, $ylim, $xoff, $xlim);
917  }
918 
919  if ($flip)
920  for ($i = $ylim - 1; $i >= $yoff; $i--)
921  $ymatches[$this->xv[$i]][] = $i;
922  else
923  for ($i = $ylim - 1; $i >= $yoff; $i--)
924  $ymatches[$this->yv[$i]][] = $i;
925 
926  $this->lcs = 0;
927  $this->seq[0]= $yoff - 1;
928  $this->in_seq = array();
929  $ymids[0] = array();
930 
931  $numer = $xlim - $xoff + $nchunks - 1;
932  $x = $xoff;
933  for ($chunk = 0; $chunk < $nchunks; $chunk++) {
934  //wfProfileIn( "$fname-chunk" );
935  if ($chunk > 0)
936  for ($i = 0; $i <= $this->lcs; $i++)
937  $ymids[$i][$chunk-1] = $this->seq[$i];
938 
939  $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
940  for ( ; $x < $x1; $x++) {
941  $line = $flip ? $this->yv[$x] : $this->xv[$x];
942  if (empty($ymatches[$line]))
943  continue;
944  $matches = $ymatches[$line];
945  reset($matches);
946  while (list ($junk, $y) = each($matches))
947  if (empty($this->in_seq[$y])) {
948  $k = $this->_lcs_pos($y);
949  USE_ASSERTS && assert($k > 0);
950  $ymids[$k] = $ymids[$k-1];
951  break;
952  }
953  while (list ( /* $junk */, $y) = each($matches)) {
954  if ($y > $this->seq[$k-1]) {
955  USE_ASSERTS && assert($y < $this->seq[$k]);
956  // Optimization: this is a common case:
957  // next match is just replacing previous match.
958  $this->in_seq[$this->seq[$k]] = false;
959  $this->seq[$k] = $y;
960  $this->in_seq[$y] = 1;
961  } else if (empty($this->in_seq[$y])) {
962  $k = $this->_lcs_pos($y);
963  USE_ASSERTS && assert($k > 0);
964  $ymids[$k] = $ymids[$k-1];
965  }
966  }
967  }
968  //wfProfileOut( "$fname-chunk" );
969  }
971  $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
972  $ymid = $ymids[$this->lcs];
973  for ($n = 0; $n < $nchunks - 1; $n++) {
974  $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
975  $y1 = $ymid[$n] + 1;
976  $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
977  }
978  $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
979 
980  //wfProfileOut( $fname );
981  return array($this->lcs, $seps);
982  }
983 
984  function _lcs_pos ($ypos) {
985  $fname = '_DiffEngine::_lcs_pos';
986  //wfProfileIn( $fname );
987 
988  $end = $this->lcs;
989  if ($end == 0 || $ypos > $this->seq[$end]) {
990  $this->seq[++$this->lcs] = $ypos;
991  $this->in_seq[$ypos] = 1;
992  //wfProfileOut( $fname );
993  return $this->lcs;
994  }
995 
996  $beg = 1;
997  while ($beg < $end) {
998  $mid = (int)(($beg + $end) / 2);
999  if ( $ypos > $this->seq[$mid] )
1000  $beg = $mid + 1;
1001  else
1002  $end = $mid;
1003  }
1004 
1005  USE_ASSERTS && assert($ypos != $this->seq[$end]);
1006 
1007  $this->in_seq[$this->seq[$end]] = false;
1008  $this->seq[$end] = $ypos;
1009  $this->in_seq[$ypos] = 1;
1010  //wfProfileOut( $fname );
1011  return $end;
1012  }
1013 
1014  /* Find LCS of two sequences.
1015  *
1016  * The results are recorded in the vectors $this->{x,y}changed[], by
1017  * storing a 1 in the element for each line that is an insertion
1018  * or deletion (ie. is not in the LCS).
1019  *
1020  * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
1021  *
1022  * Note that XLIM, YLIM are exclusive bounds.
1023  * All line numbers are origin-0 and discarded lines are not counted.
1024  */
1025  function _compareseq ($xoff, $xlim, $yoff, $ylim) {
1026  $fname = '_DiffEngine::_compareseq';
1027  //wfProfileIn( $fname );
1028 
1029  // Slide down the bottom initial diagonal.
1030  while ($xoff < $xlim && $yoff < $ylim
1031  && $this->xv[$xoff] == $this->yv[$yoff]) {
1032  ++$xoff;
1033  ++$yoff;
1034  }
1035 
1036  // Slide up the top initial diagonal.
1037  while ($xlim > $xoff && $ylim > $yoff
1038  && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
1039  --$xlim;
1040  --$ylim;
1041  }
1042 
1043  if ($xoff == $xlim || $yoff == $ylim)
1044  $lcs = 0;
1045  else {
1046  // This is ad hoc but seems to work well.
1047  //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
1048  //$nchunks = max(2,min(8,(int)$nchunks));
1049  $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
1050  list ($lcs, $seps)
1051  = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks);
1052  }
1053 
1054  if ($lcs == 0) {
1055  // X and Y sequences have no common subsequence:
1056  // mark all changed.
1057  while ($yoff < $ylim)
1058  $this->ychanged[$this->yind[$yoff++]] = 1;
1059  while ($xoff < $xlim)
1060  $this->xchanged[$this->xind[$xoff++]] = 1;
1061  } else {
1062  // Use the partitions to split this problem into subproblems.
1063  reset($seps);
1064  $pt1 = $seps[0];
1065  while ($pt2 = next($seps)) {
1066  $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
1067  $pt1 = $pt2;
1068  }
1069  }
1070  //wfProfileOut( $fname );
1071  }
1072 
1073  /* Adjust inserts/deletes of identical lines to join changes
1074  * as much as possible.
1075  *
1076  * We do something when a run of changed lines include a
1077  * line at one end and has an excluded, identical line at the other.
1078  * We are free to choose which identical line is included.
1079  * `compareseq' usually chooses the one at the beginning,
1080  * but usually it is cleaner to consider the following identical line
1081  * to be the "change".
1082  *
1083  * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
1084  */
1085  function _shift_boundaries ($lines, &$changed, $other_changed) {
1086  $fname = '_DiffEngine::_shift_boundaries';
1087  //wfProfileIn( $fname );
1088  $i = 0;
1089  $j = 0;
1090 
1091  USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
1092  $len = sizeof($lines);
1093  $other_len = sizeof($other_changed);
1094 
1095  while (1) {
1096  /*
1097  * Scan forwards to find beginning of another run of changes.
1098  * Also keep track of the corresponding point in the other file.
1099  *
1100  * Throughout this code, $i and $j are adjusted together so that
1101  * the first $i elements of $changed and the first $j elements
1102  * of $other_changed both contain the same number of zeros
1103  * (unchanged lines).
1104  * Furthermore, $j is always kept so that $j == $other_len or
1105  * $other_changed[$j] == false.
1106  */
1107  while ($j < $other_len && $other_changed[$j])
1108  $j++;
1109 
1110  while ($i < $len && ! $changed[$i]) {
1111  USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
1112  $i++; $j++;
1113  while ($j < $other_len && $other_changed[$j])
1114  $j++;
1115  }
1116 
1117  if ($i == $len)
1118  break;
1119 
1120  $start = $i;
1121 
1122  // Find the end of this run of changes.
1123  while (++$i < $len && $changed[$i])
1124  continue;
1125 
1126  do {
1127  /*
1128  * Record the length of this run of changes, so that
1129  * we can later determine whether the run has grown.
1130  */
1131  $runlength = $i - $start;
1132 
1133  /*
1134  * Move the changed region back, so long as the
1135  * previous unchanged line matches the last changed one.
1136  * This merges with previous changed regions.
1137  */
1138  while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
1139  $changed[--$start] = 1;
1140  $changed[--$i] = false;
1141  while ($start > 0 && $changed[$start - 1])
1142  $start--;
1143  USE_ASSERTS && assert('$j > 0');
1144  while ($other_changed[--$j])
1145  continue;
1146  USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
1147  }
1148 
1149  /*
1150  * Set CORRESPONDING to the end of the changed run, at the last
1151  * point where it corresponds to a changed run in the other file.
1152  * CORRESPONDING == LEN means no such point has been found.
1153  */
1154  $corresponding = $j < $other_len ? $i : $len;
1155 
1156  /*
1157  * Move the changed region forward, so long as the
1158  * first changed line matches the following unchanged one.
1159  * This merges with following changed regions.
1160  * Do this second, so that if there are no merges,
1161  * the changed region is moved forward as far as possible.
1162  */
1163  while ($i < $len && $lines[$start] == $lines[$i]) {
1164  $changed[$start++] = false;
1165  $changed[$i++] = 1;
1166  while ($i < $len && $changed[$i])
1167  $i++;
1168 
1169  USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
1170  $j++;
1171  if ($j < $other_len && $other_changed[$j]) {
1172  $corresponding = $i;
1173  while ($j < $other_len && $other_changed[$j])
1174  $j++;
1175  }
1176  }
1177  } while ($runlength != $i - $start);
1178 
1179  /*
1180  * If possible, move the fully-merged run of changes
1181  * back to a corresponding run in the other file.
1182  */
1183  while ($corresponding < $i) {
1184  $changed[--$start] = 1;
1185  $changed[--$i] = 0;
1186  USE_ASSERTS && assert('$j > 0');
1187  while ($other_changed[--$j])
1188  continue;
1189  USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
1190  }
1191  }
1192  //wfProfileOut( $fname );
1193  }
1194 }
1195 
1202 class Diff
1203 {
1204  var $edits;
1205 
1214  function Diff($from_lines, $to_lines) {
1215  $eng = new _DiffEngine;
1216  $this->edits = $eng->diff($from_lines, $to_lines);
1217  //$this->_check($from_lines, $to_lines);
1218  }
1219 
1230  function reverse () {
1231  $rev = $this;
1232  $rev->edits = array();
1233  foreach ($this->edits as $edit) {
1234  $rev->edits[] = $edit->reverse();
1235  }
1236  return $rev;
1237  }
1238 
1244  function isEmpty () {
1245  foreach ($this->edits as $edit) {
1246  if ($edit->type != 'copy')
1247  return false;
1248  }
1249  return true;
1250  }
1251 
1259  function lcs () {
1260  $lcs = 0;
1261  foreach ($this->edits as $edit) {
1262  if ($edit->type == 'copy')
1263  $lcs += sizeof($edit->orig);
1264  }
1265  return $lcs;
1266  }
1267 
1276  function orig() {
1277  $lines = array();
1278 
1279  foreach ($this->edits as $edit) {
1280  if ($edit->orig)
1281  array_splice($lines, sizeof($lines), 0, $edit->orig);
1282  }
1283  return $lines;
1284  }
1285 
1294  function closing() {
1295  $lines = array();
1296 
1297  foreach ($this->edits as $edit) {
1298  if ($edit->closing)
1299  array_splice($lines, sizeof($lines), 0, $edit->closing);
1300  }
1301  return $lines;
1302  }
1303 
1309  function _check ($from_lines, $to_lines) {
1310  $fname = 'Diff::_check';
1311  //wfProfileIn( $fname );
1312  if (serialize($from_lines) != serialize($this->orig()))
1313  trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
1314  if (serialize($to_lines) != serialize($this->closing()))
1315  trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
1316 
1317  $rev = $this->reverse();
1318  if (serialize($to_lines) != serialize($rev->orig()))
1319  trigger_error("Reversed original doesn't match", E_USER_ERROR);
1320  if (serialize($from_lines) != serialize($rev->closing()))
1321  trigger_error("Reversed closing doesn't match", E_USER_ERROR);
1322 
1323 
1324  $prevtype = 'none';
1325  foreach ($this->edits as $edit) {
1326  if ( $prevtype == $edit->type )
1327  trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
1328  $prevtype = $edit->type;
1329  }
1330 
1331  $lcs = $this->lcs();
1332  trigger_error('Diff okay: LCS = '.$lcs, E_USER_NOTICE);
1333  //wfProfileOut( $fname );
1334  }
1335 }
1336 
1342 class MappedDiff extends Diff
1343 {
1367  function MappedDiff($from_lines, $to_lines,
1368  $mapped_from_lines, $mapped_to_lines) {
1369  $fname = 'MappedDiff::MappedDiff';
1370  //wfProfileIn( $fname );
1371 
1372  assert(sizeof($from_lines) == sizeof($mapped_from_lines));
1373  assert(sizeof($to_lines) == sizeof($mapped_to_lines));
1374 
1375  $this->Diff($mapped_from_lines, $mapped_to_lines);
1376 
1377  $xi = $yi = 0;
1378  for ($i = 0; $i < sizeof($this->edits); $i++) {
1379  $orig = &$this->edits[$i]->orig;
1380  if (is_array($orig)) {
1381  $orig = array_slice($from_lines, $xi, sizeof($orig));
1382  $xi += sizeof($orig);
1383  }
1384 
1385  $closing = &$this->edits[$i]->closing;
1386  if (is_array($closing)) {
1387  $closing = array_slice($to_lines, $yi, sizeof($closing));
1388  $yi += sizeof($closing);
1389  }
1390  }
1391  //wfProfileOut( $fname );
1392  }
1393 }
1394 
1405 class DiffFormatter
1406 {
1413  var $leading_context_lines = 0;
1414 
1421  var $trailing_context_lines = 0;
1422 
1429  function format($diff) {
1430  $fname = 'DiffFormatter::format';
1431  //wfProfileIn( $fname );
1432 
1433  $xi = $yi = 1;
1434  $block = false;
1435  $context = array();
1436 
1437  $nlead = $this->leading_context_lines;
1438  $ntrail = $this->trailing_context_lines;
1439 
1440  $this->_start_diff();
1441 
1442  foreach ($diff->edits as $edit) {
1443  if ($edit->type == 'copy') {
1444  if (is_array($block)) {
1445  if (sizeof($edit->orig) <= $nlead + $ntrail) {
1446  $block[] = $edit;
1447  }
1448  else{
1449  if ($ntrail) {
1450  $context = array_slice($edit->orig, 0, $ntrail);
1451  $block[] = new _DiffOp_Copy($context);
1452  }
1453  $this->_block($x0, $ntrail + $xi - $x0,
1454  $y0, $ntrail + $yi - $y0,
1455  $block);
1456  $block = false;
1457  }
1458  }
1459  $context = $edit->orig;
1460  }
1461  else {
1462  if (! is_array($block)) {
1463  $context = array_slice($context, sizeof($context) - $nlead);
1464  $x0 = $xi - sizeof($context);
1465  $y0 = $yi - sizeof($context);
1466  $block = array();
1467  if ($context)
1468  $block[] = new _DiffOp_Copy($context);
1469  }
1470  $block[] = $edit;
1471  }
1472 
1473  if ($edit->orig)
1474  $xi += sizeof($edit->orig);
1475  if ($edit->closing)
1476  $yi += sizeof($edit->closing);
1477  }
1478 
1479  if (is_array($block))
1480  $this->_block($x0, $xi - $x0,
1481  $y0, $yi - $y0,
1482  $block);
1483 
1484  $end = $this->_end_diff();
1485  //wfProfileOut( $fname );
1486  return $end;
1487  }
1488 
1489  function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
1490  $fname = 'DiffFormatter::_block';
1491  //wfProfileIn( $fname );
1492  $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
1493  foreach ($edits as $edit) {
1494  if ($edit->type == 'copy')
1495  $this->_context($edit->orig);
1496  elseif ($edit->type == 'add')
1497  $this->_added($edit->closing);
1498  elseif ($edit->type == 'delete')
1499  $this->_deleted($edit->orig);
1500  elseif ($edit->type == 'change')
1501  $this->_changed($edit->orig, $edit->closing);
1502  else
1503  trigger_error('Unknown edit type', E_USER_ERROR);
1504  }
1505  $this->_end_block();
1506  //wfProfileOut( $fname );
1507  }
1508 
1509  function _start_diff() {
1510  ob_start();
1511  }
1512 
1513  function _end_diff() {
1514  $val = ob_get_contents();
1515  ob_end_clean();
1516  return $val;
1517  }
1519  function _block_header($xbeg, $xlen, $ybeg, $ylen) {
1520  if ($xlen > 1)
1521  $xbeg .= "," . ($xbeg + $xlen - 1);
1522  if ($ylen > 1)
1523  $ybeg .= "," . ($ybeg + $ylen - 1);
1524 
1525  return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
1526  }
1527 
1528  function _start_block($header) {
1529  echo $header;
1530  }
1531 
1532  function _end_block() {
1533  }
1534 
1535  function _lines($lines, $prefix = ' ') {
1536  foreach ($lines as $line)
1537  echo "$prefix $line\n";
1538  }
1539 
1540  function _context($lines) {
1541  $this->_lines($lines);
1542  }
1543 
1544  function _added($lines) {
1545  $this->_lines($lines, '>');
1546  }
1547  function _deleted($lines) {
1548  $this->_lines($lines, '<');
1549  }
1551  function _changed($orig, $closing) {
1552  $this->_deleted($orig);
1553  echo "---\n";
1554  $this->_added($closing);
1555  }
1556 }
1564 define('NBSP', '&#160;'); // iso-8859-x non-breaking space.
1571 class _HWLDF_WordAccumulator {
1572  function _HWLDF_WordAccumulator () {
1573  $this->_lines = array();
1574  $this->_line = '';
1575  $this->_group = '';
1576  $this->_tag = '';
1577  }
1578 
1579  function _flushGroup ($new_tag) {
1580  if ($this->_group !== '') {
1581  if ($this->_tag == 'ins')
1582  $this->_line .= '[ilDiffInsStart]' .
1583  htmlspecialchars ( $this->_group ) . '[ilDiffInsEnd]';
1584  elseif ($this->_tag == 'del')
1585  $this->_line .= '[ilDiffDelStart]' .
1586  htmlspecialchars ( $this->_group ) . '[ilDiffDelEnd]';
1587  else
1588  $this->_line .= htmlspecialchars ( $this->_group );
1589  }
1590  $this->_group = '';
1591  $this->_tag = $new_tag;
1592  }
1593 
1594  function _flushLine ($new_tag) {
1595  $this->_flushGroup($new_tag);
1596  if ($this->_line != '')
1597  array_push ( $this->_lines, $this->_line );
1598  else
1599  # make empty lines visible by inserting an NBSP
1600  array_push ( $this->_lines, NBSP );
1601  $this->_line = '';
1602  }
1603 
1604  function addWords ($words, $tag = '') {
1605  if ($tag != $this->_tag)
1606  $this->_flushGroup($tag);
1608  foreach ($words as $word) {
1609  // new-line should only come as first char of word.
1610  if ($word == '')
1611  continue;
1612  if ($word[0] == "\n") {
1613  $this->_flushLine($tag);
1614  $word = substr($word, 1);
1615  }
1616  assert(!strstr($word, "\n"));
1617  $this->_group .= $word;
1618  }
1619  }
1621  function getLines() {
1622  $this->_flushLine('~done');
1623  return $this->_lines;
1624  }
1625 }
1626 
1632 class WordLevelDiff extends MappedDiff
1633 {
1634  const MAX_LINE_LENGTH = 10000;
1635 
1636  function WordLevelDiff ($orig_lines, $closing_lines) {
1637  $fname = 'WordLevelDiff::WordLevelDiff';
1638  //wfProfileIn( $fname );
1639 
1640  list ($orig_words, $orig_stripped) = $this->_split($orig_lines);
1641  list ($closing_words, $closing_stripped) = $this->_split($closing_lines);
1642 
1643  $this->MappedDiff($orig_words, $closing_words,
1644  $orig_stripped, $closing_stripped);
1645  //wfProfileOut( $fname );
1646  }
1647 
1648  function _split($lines) {
1649  $fname = 'WordLevelDiff::_split';
1650  //wfProfileIn( $fname );
1651 
1652  $words = array();
1653  $stripped = array();
1654  $first = true;
1655  foreach ( $lines as $line ) {
1656  # If the line is too long, just pretend the entire line is one big word
1657  # This prevents resource exhaustion problems
1658  if ( $first ) {
1659  $first = false;
1660  } else {
1661  $words[] = "\n";
1662  $stripped[] = "\n";
1663  }
1664  if ( strlen( $line ) > self::MAX_LINE_LENGTH ) {
1665  $words[] = $line;
1666  $stripped[] = $line;
1667  } else {
1668  $m = array();
1669  if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
1670  $line, $m))
1671  {
1672  $words = array_merge( $words, $m[0] );
1673  $stripped = array_merge( $stripped, $m[1] );
1674  }
1675  }
1676  }
1677  //wfProfileOut( $fname );
1678  return array($words, $stripped);
1679  }
1680 
1681  function orig () {
1682  $fname = 'WordLevelDiff::orig';
1683  //wfProfileIn( $fname );
1684  $orig = new _HWLDF_WordAccumulator;
1685 
1686  foreach ($this->edits as $edit) {
1687  if ($edit->type == 'copy')
1688  $orig->addWords($edit->orig);
1689  elseif ($edit->orig)
1690  $orig->addWords($edit->orig, 'del');
1691  }
1692  $lines = $orig->getLines();
1693  //wfProfileOut( $fname );
1694  return $lines;
1695  }
1696 
1697  function closing () {
1698  $fname = 'WordLevelDiff::closing';
1699  //wfProfileIn( $fname );
1700  $closing = new _HWLDF_WordAccumulator;
1701 
1702  foreach ($this->edits as $edit) {
1703  if ($edit->type == 'copy')
1704  $closing->addWords($edit->closing);
1705  elseif ($edit->closing)
1706  $closing->addWords($edit->closing, 'ins');
1707  }
1708  $lines = $closing->getLines();
1709  //wfProfileOut( $fname );
1710  return $lines;
1711  }
1712 }
1720 class TableDiffFormatter extends DiffFormatter
1721 {
1722  function TableDiffFormatter() {
1723  $this->leading_context_lines = 2;
1724  $this->trailing_context_lines = 2;
1725  }
1727  function _block_header( $xbeg, $xlen, $ybeg, $ylen ) {
1728  $r = '<tr><td colspan="2" align="left"><strong><!--LINE '.$xbeg."--></strong></td>\n" .
1729  '<td colspan="2" align="left"><strong><!--LINE '.$ybeg."--></strong></td></tr>\n";
1730  return $r;
1731  }
1732 
1733  function _start_block( $header ) {
1734  echo $header;
1735  }
1736 
1737  function _end_block() {
1738  }
1739 
1740  function _lines( $lines, $prefix=' ', $color='white' ) {
1741  }
1742 
1743  # HTML-escape parameter before calling this
1744  function addedLine( $line ) {
1745  return "<td>+</td><td class='diff-addedline'>{$line}</td>";
1746  }
1747 
1748  # HTML-escape parameter before calling this
1749  function deletedLine( $line ) {
1750  return "<td>-</td><td class='diff-deletedline'>{$line}</td>";
1751  }
1752 
1753  # HTML-escape parameter before calling this
1754  function contextLine( $line ) {
1755  return "<td> </td><td class='diff-context'>{$line}</td>";
1756  }
1757 
1758  function emptyLine() {
1759  return '<td colspan="2">&nbsp;</td>';
1760  }
1761 
1762  function _added( $lines ) {
1763  foreach ($lines as $line) {
1764  echo '<tr>' . $this->emptyLine() .
1765  $this->addedLine( htmlspecialchars ( $line ) ) . "</tr>\n";
1766  }
1767  }
1768 
1769  function _deleted($lines) {
1770  foreach ($lines as $line) {
1771  echo '<tr>' . $this->deletedLine( htmlspecialchars ( $line ) ) .
1772  $this->emptyLine() . "</tr>\n";
1773  }
1774  }
1775 
1776  function _context( $lines ) {
1777  foreach ($lines as $line) {
1778  echo '<tr>' .
1779  $this->contextLine( htmlspecialchars ( $line ) ) .
1780  $this->contextLine( htmlspecialchars ( $line ) ) . "</tr>\n";
1781  }
1782  }
1783 
1784  function _changed( $orig, $closing ) {
1785  $fname = 'TableDiffFormatter::_changed';
1786  //wfProfileIn( $fname );
1787 
1788  $diff = new WordLevelDiff( $orig, $closing );
1789  $del = $diff->orig();
1790  $add = $diff->closing();
1791 
1792  # Notice that WordLevelDiff returns HTML-escaped output.
1793  # Hence, we will be calling addedLine/deletedLine without HTML-escaping.
1794 
1795  while ( $line = array_shift( $del ) ) {
1796  $aline = array_shift( $add );
1797  echo '<tr>' . $this->deletedLine( $line ) .
1798  $this->addedLine( $aline ) . "</tr>\n";
1799  }
1800  foreach ($add as $line) { # If any leftovers
1801  echo '<tr>' . $this->emptyLine() .
1802  $this->addedLine( $line ) . "</tr>\n";
1803  }
1804  //wfProfileOut( $fname );
1805  }
1806 }
1807 
1808 ?>
getDiffBody()
Get the diff table body, without header Results are cached Returns false on error.
exit
Definition: login.php:54
diff($from_lines, $to_lines)
generateDiffBody( $otext, $ntext)
Generate a diff, no caching $otext and $ntext must be already segmented.
setText( $oldText, $newText)
Use specified text instead of loading from the database.
$cmd
Definition: sahs_server.php:35
showDiffPage( $diffOnly=false)
getDiff( $otitle, $ntitle)
Get diff table, including header Note that the interface has changed, it&#39;s no longer static...
localiseLineNumbers( $text)
Replace line numbers with the text in the user&#39;s language.
const USE_ASSERTS
renderNewRevision()
Show the new revision of the page.
wfMsg($x)
Definition: RandomTest.php:60
addHeader( $diff, $otitle, $ntitle, $multi='')
Add the header to a diff body.
getMultiNotice()
If there are revisions between the ones being compared, return a note saying so.
$n
Definition: RandomTest.php:80
foreach($mandatory_scripts as $file) $timestamp
Definition: buildRTE.php:81
showDiff( $otitle, $ntitle)
Get the diff text, send it to $wgOut Returns false if the diff could not be generated, otherwise returns true.
DifferenceEngine( $titleObj=null, $old=0, $new=0, $rcid=0)
#-
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.
localiseLineNumbersCb( $matches)
if( $out) else
loadText()
Load the text of the revisions, as well as revision data.
$r