]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/diff/DifferenceEngine.php
MediaWiki 1.14.0
[autoinstallsdev/mediawiki.git] / includes / diff / DifferenceEngine.php
1 <?php
2 /**
3  * @defgroup DifferenceEngine DifferenceEngine
4  */
5
6 /**
7  * Constant to indicate diff cache compatibility.
8  * Bump this when changing the diff formatting in a way that
9  * fixes important bugs or such to force cached diff views to
10  * clear.
11  */
12 define( 'MW_DIFF_VERSION', '1.11a' );
13
14 /**
15  * @todo document
16  * @ingroup DifferenceEngine
17  */
18 class DifferenceEngine {
19         /**#@+
20          * @private
21          */
22         var $mOldid, $mNewid, $mTitle;
23         var $mOldtitle, $mNewtitle, $mPagetitle;
24         var $mOldtext, $mNewtext;
25         var $mOldPage, $mNewPage;
26         var $mRcidMarkPatrolled;
27         var $mOldRev, $mNewRev;
28         var $mRevisionsLoaded = false; // Have the revisions been loaded
29         var $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2?
30         var $htmldiff;
31         /**#@-*/
32
33         /**
34          * Constructor
35          * @param $titleObj Title object that the diff is associated with
36          * @param $old Integer: old ID we want to show and diff with.
37          * @param $new String: either 'prev' or 'next'.
38          * @param $rcid Integer: ??? FIXME (default 0)
39          * @param $refreshCache boolean If set, refreshes the diff cache
40          * @param $htmldiff boolean If set, output using HTMLDiff instead of raw wikicode diff
41          */
42         function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0, $refreshCache = false , $htmldiff = false) {
43                 $this->mTitle = $titleObj;
44                 wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
45
46                 if ( 'prev' === $new ) {
47                         # Show diff between revision $old and the previous one.
48                         # Get previous one from DB.
49                         #
50                         $this->mNewid = intval($old);
51
52                         $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid );
53
54                 } elseif ( 'next' === $new ) {
55                         # Show diff between revision $old and the previous one.
56                         # Get previous one from DB.
57                         #
58                         $this->mOldid = intval($old);
59                         $this->mNewid = $this->mTitle->getNextRevisionID( $this->mOldid );
60                         if ( false === $this->mNewid ) {
61                                 # if no result, NewId points to the newest old revision. The only newer
62                                 # revision is cur, which is "0".
63                                 $this->mNewid = 0;
64                         }
65
66                 } else {
67                         $this->mOldid = intval($old);
68                         $this->mNewid = intval($new);
69                 }
70                 $this->mRcidMarkPatrolled = intval($rcid);  # force it to be an integer
71                 $this->mRefreshCache = $refreshCache;
72                 $this->htmldiff = $htmldiff;
73         }
74
75         function getTitle() {
76                 return $this->mTitle;
77         }
78
79         function showDiffPage( $diffOnly = false ) {
80                 global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol, $wgEnableHtmlDiff;
81                 wfProfileIn( __METHOD__ );
82
83
84                 # If external diffs are enabled both globally and for the user,
85                 # we'll use the application/x-external-editor interface to call
86                 # an external diff tool like kompare, kdiff3, etc.
87                 if($wgUseExternalEditor && $wgUser->getOption('externaldiff')) {
88                         global $wgInputEncoding,$wgServer,$wgScript,$wgLang;
89                         $wgOut->disable();
90                         header ( "Content-type: application/x-external-editor; charset=".$wgInputEncoding );
91                         $url1=$this->mTitle->getFullURL("action=raw&oldid=".$this->mOldid);
92                         $url2=$this->mTitle->getFullURL("action=raw&oldid=".$this->mNewid);
93                         $special=$wgLang->getNsText(NS_SPECIAL);
94                         $control=<<<CONTROL
95                         [Process]
96                         Type=Diff text
97                         Engine=MediaWiki
98                         Script={$wgServer}{$wgScript}
99                         Special namespace={$special}
100
101                         [File]
102                         Extension=wiki
103                         URL=$url1
104
105                         [File 2]
106                         Extension=wiki
107                         URL=$url2
108 CONTROL;
109                         echo($control);
110                         return;
111                 }
112
113                 $wgOut->setArticleFlag( false );
114                 if ( ! $this->loadRevisionData() ) {
115                         $t = $this->mTitle->getPrefixedText();
116                         $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
117                         $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
118                         $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
119                         wfProfileOut( __METHOD__ );
120                         return;
121                 }
122
123                 wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
124
125                 if ( $this->mNewRev->isCurrent() ) {
126                         $wgOut->setArticleFlag( true );
127                 }
128
129                 # mOldid is false if the difference engine is called with a "vague" query for
130                 # a diff between a version V and its previous version V' AND the version V
131                 # is the first version of that article. In that case, V' does not exist.
132                 if ( $this->mOldid === false ) {
133                         $this->showFirstRevision();
134                         $this->renderNewRevision();  // should we respect $diffOnly here or not?
135                         wfProfileOut( __METHOD__ );
136                         return;
137                 }
138
139                 $wgOut->suppressQuickbar();
140
141                 $oldTitle = $this->mOldPage->getPrefixedText();
142                 $newTitle = $this->mNewPage->getPrefixedText();
143                 if( $oldTitle == $newTitle ) {
144                         $wgOut->setPageTitle( $newTitle );
145                 } else {
146                         $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle );
147                 }
148                 $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
149                 $wgOut->setRobotPolicy( 'noindex,nofollow' );
150
151                 if ( !$this->mOldPage->userCanRead() || !$this->mNewPage->userCanRead() ) {
152                         $wgOut->loginToUse();
153                         $wgOut->output();
154                         $wgOut->disable();
155                         wfProfileOut( __METHOD__ );
156                         return;
157                 }
158
159                 $sk = $wgUser->getSkin();
160
161                 // Check if page is editable
162                 $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
163                 if ( $editable && $this->mNewRev->isCurrent() && $wgUser->isAllowed( 'rollback' ) ) {
164                         $rollback = '&nbsp;&nbsp;&nbsp;' . $sk->generateRollback( $this->mNewRev );
165                 } else {
166                         $rollback = '';
167                 }
168
169                 // Prepare a change patrol link, if applicable
170                 if( $wgUseRCPatrol && $this->mTitle->userCan('patrol') ) {
171                         // If we've been given an explicit change identifier, use it; saves time
172                         if( $this->mRcidMarkPatrolled ) {
173                                 $rcid = $this->mRcidMarkPatrolled;
174                         } else {
175                                 // Look for an unpatrolled change corresponding to this diff
176                                 $db = wfGetDB( DB_SLAVE );
177                                 $change = RecentChange::newFromConds(
178                                 array(
179                                 // Add redundant user,timestamp condition so we can use the existing index
180                                                 'rc_user_text'  => $this->mNewRev->getUserText( Revision::FOR_THIS_USER ),
181                                                 'rc_timestamp'  => $db->timestamp( $this->mNewRev->getTimestamp() ),
182                                                 'rc_this_oldid' => $this->mNewid,
183                                                 'rc_last_oldid' => $this->mOldid,
184                                                 'rc_patrolled'  => 0
185                                 ),
186                                 __METHOD__
187                                 );
188                                 if( $change instanceof RecentChange ) {
189                                         $rcid = $change->mAttribs['rc_id'];
190                                 } else {
191                                         // None found
192                                         $rcid = 0;
193                                 }
194                         }
195                         // Build the link
196                         if( $rcid ) {
197                                 $patrol = ' <span class="patrollink">[' . $sk->makeKnownLinkObj(
198                                         $this->mTitle,
199                                         wfMsgHtml( 'markaspatrolleddiff' ),
200                                                 "action=markpatrolled&rcid={$rcid}"
201                                         ) . ']</span>';
202                         } else {
203                                 $patrol = '';
204                         }
205                 } else {
206                         $patrol = '';
207                 }
208
209                 $htmldiffarg = $this->htmlDiffArgument();
210                 $prevlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousdiff' ),
211                         'diff=prev&oldid='.$this->mOldid.$htmldiffarg, '', '', 'id="differences-prevlink"' );
212                 if ( $this->mNewRev->isCurrent() ) {
213                         $nextlink = '&nbsp;';
214                 } else {
215                         $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ),
216                                 'diff=next&oldid='.$this->mNewid.$htmldiffarg, '', '', 'id="differences-nextlink"' );
217                 }
218
219                 $oldminor = '';
220                 $newminor = '';
221
222                 if ($this->mOldRev->mMinorEdit == 1) {
223                         $oldminor = Xml::span( wfMsg( 'minoreditletter' ), 'minor' ) . ' ';
224                 }
225
226                 if ($this->mNewRev->mMinorEdit == 1) {
227                         $newminor = Xml::span( wfMsg( 'minoreditletter' ), 'minor' ) . ' ';
228                 }
229
230                 $rdel = ''; $ldel = '';
231                 if( $wgUser->isAllowed( 'deleterevision' ) ) {
232                         $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
233                         if( !$this->mOldRev->userCan( Revision::DELETED_RESTRICTED ) ) {
234                                 // If revision was hidden from sysops
235                                 $ldel = wfMsgHtml( 'rev-delundel' );
236                         } else {
237                                 $ldel = $sk->makeKnownLinkObj( $revdel,
238                                 wfMsgHtml( 'rev-delundel' ),
239                                         'target=' . urlencode( $this->mOldRev->mTitle->getPrefixedDbkey() ) .
240                                         '&oldid=' . urlencode( $this->mOldRev->getId() ) );
241                                 // Bolden oversighted content
242                                 if( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) )
243                                 $ldel = "<strong>$ldel</strong>";
244                         }
245                         $ldel = "&nbsp;&nbsp;&nbsp;<tt>(<small>$ldel</small>)</tt> ";
246                         // We don't currently handle well changing the top revision's settings
247                         if( $this->mNewRev->isCurrent() ) {
248                                 // If revision was hidden from sysops
249                                 $rdel = wfMsgHtml( 'rev-delundel' );
250                         } else if( !$this->mNewRev->userCan( Revision::DELETED_RESTRICTED ) ) {
251                                 // If revision was hidden from sysops
252                                 $rdel = wfMsgHtml( 'rev-delundel' );
253                         } else {
254                                 $rdel = $sk->makeKnownLinkObj( $revdel,
255                                 wfMsgHtml( 'rev-delundel' ),
256                                         'target=' . urlencode( $this->mNewRev->mTitle->getPrefixedDbkey() ) .
257                                         '&oldid=' . urlencode( $this->mNewRev->getId() ) );
258                                 // Bolden oversighted content
259                                 if( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) )
260                                 $rdel = "<strong>$rdel</strong>";
261                         }
262                         $rdel = "&nbsp;&nbsp;&nbsp;<tt>(<small>$rdel</small>)</tt> ";
263                 }
264
265                 $oldHeader = '<div id="mw-diff-otitle1"><strong>'.$this->mOldtitle.'</strong></div>' .
266                         '<div id="mw-diff-otitle2">' . $sk->revUserTools( $this->mOldRev, true ) . "</div>" .
267                         '<div id="mw-diff-otitle3">' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly, true ) . $ldel . "</div>" .
268                         '<div id="mw-diff-otitle4">' . $prevlink .'</div>';
269                 $newHeader = '<div id="mw-diff-ntitle1"><strong>'.$this->mNewtitle.'</strong></div>' .
270                         '<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev, true ) . " $rollback</div>" .
271                         '<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly, true ) . $rdel . "</div>" .
272                         '<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>';
273
274                 if( $wgEnableHtmlDiff && $this->htmldiff) {
275                         $multi = $this->getMultiNotice();
276                         $wgOut->addHTML('<div class="diff-switchtype">'.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'wikicodecomparison' ),
277                         'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=0', '', '', 'id="differences-switchtype"' ).'</div>');
278                         $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
279                         $this->renderHtmlDiff();
280                 } else {
281                         if($wgEnableHtmlDiff){
282                                 $wgOut->addHTML('<div class="diff-switchtype">'.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'visualcomparison' ),
283                                 'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=1', '', '', 'id="differences-switchtype"' ).'</div>');
284                         }
285                         $this->showDiff( $oldHeader, $newHeader );
286                         if( !$diffOnly ) {
287                                 $this->renderNewRevision();
288                         }
289                 }
290                 wfProfileOut( __METHOD__ );
291         }
292
293         /**
294          * Show the new revision of the page.
295          */
296         function renderNewRevision() {
297                 global $wgOut;
298                 wfProfileIn( __METHOD__ );
299
300                 $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
301                 #add deleted rev tag if needed
302                 if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
303                         $wgOut->addWikiMsg( 'rev-deleted-text-permission' );
304                 } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
305                         $wgOut->addWikiMsg( 'rev-deleted-text-view' );
306                 }
307
308                 if( !$this->mNewRev->isCurrent() ) {
309                         $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
310                 }
311
312                 $this->loadNewText();
313                 if( is_object( $this->mNewRev ) ) {
314                         $wgOut->setRevisionId( $this->mNewRev->getId() );
315                 }
316
317                 if ($this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage()) {
318                         // Stolen from Article::view --AG 2007-10-11
319
320                         // Give hooks a chance to customise the output
321                         if( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mTitle, $wgOut ) ) ) {
322                                 // Wrap the whole lot in a <pre> and don't parse
323                                 $m = array();
324                                 preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
325                                 $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
326                                 $wgOut->addHTML( htmlspecialchars( $this->mNewtext ) );
327                                 $wgOut->addHTML( "\n</pre>\n" );
328                         }
329                 } else
330                 $wgOut->addWikiTextTidy( $this->mNewtext );
331
332                 if( !$this->mNewRev->isCurrent() ) {
333                         $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
334                 }
335
336                 wfProfileOut( __METHOD__ );
337         }
338
339
340         function renderHtmlDiff() {
341                 global $wgOut, $wgTitle, $wgParser, $wgDebugComments;
342                 wfProfileIn( __METHOD__ );
343
344                 $this->showDiffStyle();
345
346                 $wgOut->addHTML( '<h2>'.wfMsgHtml( 'visual-comparison' )."</h2>\n" );
347                 #add deleted rev tag if needed
348                 if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
349                         $wgOut->addWikiMsg( 'rev-deleted-text-permission' );
350                 } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
351                         $wgOut->addWikiMsg( 'rev-deleted-text-view' );
352                 }
353
354                 if( !$this->mNewRev->isCurrent() ) {
355                         $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
356                 }
357
358                 $this->loadText();
359
360                 // Old revision
361                 if( is_object( $this->mOldRev ) ) {
362                         $wgOut->setRevisionId( $this->mOldRev->getId() );
363                 }
364
365                 $popts = $wgOut->parserOptions();
366                 $oldTidy = $popts->setTidy( true );
367                 $popts->setEditSection( false );
368
369                 $parserOutput = $wgParser->parse( $this->mOldtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() );
370                 $popts->setTidy( $oldTidy );
371
372                 //only for new?
373                 //$wgOut->addParserOutputNoText( $parserOutput );
374                 $oldHtml = $parserOutput->getText();
375                 wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$oldHtml ) );
376
377                 // New revision
378                 if( is_object( $this->mNewRev ) ) {
379                         $wgOut->setRevisionId( $this->mNewRev->getId() );
380                 }
381
382                 $popts = $wgOut->parserOptions();
383                 $oldTidy = $popts->setTidy( true );
384
385                 $parserOutput = $wgParser->parse( $this->mNewtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() );
386                 $popts->setTidy( $oldTidy );
387
388                 $wgOut->addParserOutputNoText( $parserOutput );
389                 $newHtml = $parserOutput->getText();
390                 wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$newHtml ) );
391
392                 unset($parserOutput, $popts);
393
394                 $differ = new HTMLDiffer(new DelegatingContentHandler($wgOut));
395                 $differ->htmlDiff($oldHtml, $newHtml);
396                 if ( $wgDebugComments ) {
397                         $wgOut->addHTML( "\n<!-- HtmlDiff Debug Output:\n" . HTMLDiffer::getDebugOutput() . " End Debug -->" );
398                 }
399
400                 wfProfileOut( __METHOD__ );
401         }
402
403         /**
404          * Show the first revision of an article. Uses normal diff headers in
405          * contrast to normal "old revision" display style.
406          */
407         function showFirstRevision() {
408                 global $wgOut, $wgUser;
409                 wfProfileIn( __METHOD__ );
410
411                 # Get article text from the DB
412                 #
413                 if ( ! $this->loadNewText() ) {
414                         $t = $this->mTitle->getPrefixedText();
415                         $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
416                         $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
417                         $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
418                         wfProfileOut( __METHOD__ );
419                         return;
420                 }
421                 if ( $this->mNewRev->isCurrent() ) {
422                         $wgOut->setArticleFlag( true );
423                 }
424
425                 # Check if user is allowed to look at this page. If not, bail out.
426                 #
427                 if ( !$this->mTitle->userCanRead() ) {
428                         $wgOut->loginToUse();
429                         $wgOut->output();
430                         wfProfileOut( __METHOD__ );
431                         throw new MWException("Permission Error: you do not have access to view this page");
432                 }
433
434                 # Prepare the header box
435                 #
436                 $sk = $wgUser->getSkin();
437
438                 $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid.$this->htmlDiffArgument(), '', '', 'id="differences-nextlink"' );
439                 $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\"><strong>{$this->mOldtitle}</strong><br />" .
440                 $sk->revUserTools( $this->mNewRev ) . "<br />" .
441                 $sk->revComment( $this->mNewRev ) . "<br />" .
442                 $nextlink . "</div>\n";
443
444                 $wgOut->addHTML( $header );
445
446                 $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
447                 $wgOut->setRobotPolicy( 'noindex,nofollow' );
448
449                 wfProfileOut( __METHOD__ );
450         }
451
452         function htmlDiffArgument(){
453                 global $wgEnableHtmlDiff;
454                 if($wgEnableHtmlDiff){
455                         if($this->htmldiff){
456                                 return '&htmldiff=1';
457                         }else{
458                                 return '&htmldiff=0';
459                         }
460                 }else{
461                         return '';
462                 }
463         }
464
465         /**
466          * Get the diff text, send it to $wgOut
467          * Returns false if the diff could not be generated, otherwise returns true
468          */
469         function showDiff( $otitle, $ntitle ) {
470                 global $wgOut;
471                 $diff = $this->getDiff( $otitle, $ntitle );
472                 if ( $diff === false ) {
473                         $wgOut->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' );
474                         return false;
475                 } else {
476                         $this->showDiffStyle();
477                         $wgOut->addHTML( $diff );
478                         return true;
479                 }
480         }
481
482         /**
483          * Add style sheets and supporting JS for diff display.
484          */
485         function showDiffStyle() {
486                 global $wgStylePath, $wgStyleVersion, $wgOut;
487                 $wgOut->addStyle( 'common/diff.css' );
488
489                 // JS is needed to detect old versions of Mozilla to work around an annoyance bug.
490                 $wgOut->addScript( "<script type=\"text/javascript\" src=\"$wgStylePath/common/diff.js?$wgStyleVersion\"></script>" );
491         }
492
493         /**
494          * Get complete diff table, including header
495          *
496          * @param Title $otitle Old title
497          * @param Title $ntitle New title
498          * @return mixed
499          */
500         function getDiff( $otitle, $ntitle ) {
501                 $body = $this->getDiffBody();
502                 if ( $body === false ) {
503                         return false;
504                 } else {
505                         $multi = $this->getMultiNotice();
506                         return $this->addHeader( $body, $otitle, $ntitle, $multi );
507                 }
508         }
509
510         /**
511          * Get the diff table body, without header
512          *
513          * @return mixed
514          */
515         function getDiffBody() {
516                 global $wgMemc;
517                 wfProfileIn( __METHOD__ );
518                 // Check if the diff should be hidden from this user
519                 if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
520                         return '';
521                 } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
522                         return '';
523                 }
524                 // Cacheable?
525                 $key = false;
526                 if ( $this->mOldid && $this->mNewid ) {
527                         $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid );
528                         // Try cache
529                         if ( !$this->mRefreshCache ) {
530                                 $difftext = $wgMemc->get( $key );
531                                 if ( $difftext ) {
532                                         wfIncrStats( 'diff_cache_hit' );
533                                         $difftext = $this->localiseLineNumbers( $difftext );
534                                         $difftext .= "\n<!-- diff cache key $key -->\n";
535                                         wfProfileOut( __METHOD__ );
536                                         return $difftext;
537                                 }
538                         } // don't try to load but save the result
539                 }
540
541                 // Loadtext is permission safe, this just clears out the diff
542                 if ( !$this->loadText() ) {
543                         wfProfileOut( __METHOD__ );
544                         return false;
545                 }
546
547                 $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
548
549                 // Save to cache for 7 days
550                 if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
551                         wfIncrStats( 'diff_uncacheable' );
552                 } else if ( $key !== false && $difftext !== false ) {
553                         wfIncrStats( 'diff_cache_miss' );
554                         $wgMemc->set( $key, $difftext, 7*86400 );
555                 } else {
556                         wfIncrStats( 'diff_uncacheable' );
557                 }
558                 // Replace line numbers with the text in the user's language
559                 if ( $difftext !== false ) {
560                         $difftext = $this->localiseLineNumbers( $difftext );
561                 }
562                 wfProfileOut( __METHOD__ );
563                 return $difftext;
564         }
565
566         /**
567          * Generate a diff, no caching
568          * $otext and $ntext must be already segmented
569          */
570         function generateDiffBody( $otext, $ntext ) {
571                 global $wgExternalDiffEngine, $wgContLang;
572
573                 $otext = str_replace( "\r\n", "\n", $otext );
574                 $ntext = str_replace( "\r\n", "\n", $ntext );
575
576                 if ( $wgExternalDiffEngine == 'wikidiff' ) {
577                         # For historical reasons, external diff engine expects
578                         # input text to be HTML-escaped already
579                         $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) );
580                         $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) );
581                         if( !function_exists( 'wikidiff_do_diff' ) ) {
582                                 dl('php_wikidiff.so');
583                         }
584                         return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) .
585                         $this->debug( 'wikidiff1' );
586                 }
587
588                 if ( $wgExternalDiffEngine == 'wikidiff2' ) {
589                         # Better external diff engine, the 2 may some day be dropped
590                         # This one does the escaping and segmenting itself
591                         if ( !function_exists( 'wikidiff2_do_diff' ) ) {
592                                 wfProfileIn( __METHOD__ . "-dl" );
593                                 @dl('php_wikidiff2.so');
594                                 wfProfileOut( __METHOD__ . "-dl" );
595                         }
596                         if ( function_exists( 'wikidiff2_do_diff' ) ) {
597                                 wfProfileIn( 'wikidiff2_do_diff' );
598                                 $text = wikidiff2_do_diff( $otext, $ntext, 2 );
599                                 $text .= $this->debug( 'wikidiff2' );
600                                 wfProfileOut( 'wikidiff2_do_diff' );
601                                 return $text;
602                         }
603                 }
604                 if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) {
605                         # Diff via the shell
606                         global $wgTmpDirectory;
607                         $tempName1 = tempnam( $wgTmpDirectory, 'diff_' );
608                         $tempName2 = tempnam( $wgTmpDirectory, 'diff_' );
609
610                         $tempFile1 = fopen( $tempName1, "w" );
611                         if ( !$tempFile1 ) {
612                                 wfProfileOut( __METHOD__ );
613                                 return false;
614                         }
615                         $tempFile2 = fopen( $tempName2, "w" );
616                         if ( !$tempFile2 ) {
617                                 wfProfileOut( __METHOD__ );
618                                 return false;
619                         }
620                         fwrite( $tempFile1, $otext );
621                         fwrite( $tempFile2, $ntext );
622                         fclose( $tempFile1 );
623                         fclose( $tempFile2 );
624                         $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
625                         wfProfileIn( __METHOD__ . "-shellexec" );
626                         $difftext = wfShellExec( $cmd );
627                         $difftext .= $this->debug( "external $wgExternalDiffEngine" );
628                         wfProfileOut( __METHOD__ . "-shellexec" );
629                         unlink( $tempName1 );
630                         unlink( $tempName2 );
631                         return $difftext;
632                 }
633
634                 # Native PHP diff
635                 $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
636                 $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
637                 $diffs = new Diff( $ota, $nta );
638                 $formatter = new TableDiffFormatter();
639                 return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) .
640                 $this->debug();
641         }
642
643         /**
644          * Generate a debug comment indicating diff generating time,
645          * server node, and generator backend.
646          */
647         protected function debug( $generator="internal" ) {
648                 global $wgShowHostnames;
649                 $data = array( $generator );
650                 if( $wgShowHostnames ) {
651                         $data[] = wfHostname();
652                 }
653                 $data[] = wfTimestamp( TS_DB );
654                 return "<!-- diff generator: " .
655                 implode( " ",
656                 array_map(
657                                         "htmlspecialchars",
658                 $data ) ) .
659                         " -->\n";
660         }
661
662         /**
663          * Replace line numbers with the text in the user's language
664          */
665         function localiseLineNumbers( $text ) {
666                 return preg_replace_callback( '/<!--LINE (\d+)-->/',
667                 array( &$this, 'localiseLineNumbersCb' ), $text );
668         }
669
670         function localiseLineNumbersCb( $matches ) {
671                 global $wgLang;
672                 return wfMsgExt( 'lineno', array( 'parseinline' ), $wgLang->formatNum( $matches[1] ) );
673         }
674
675
676         /**
677          * If there are revisions between the ones being compared, return a note saying so.
678          */
679         function getMultiNotice() {
680                 if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) )
681                 return '';
682
683                 if( !$this->mOldPage->equals( $this->mNewPage ) ) {
684                         // Comparing two different pages? Count would be meaningless.
685                         return '';
686                 }
687
688                 $oldid = $this->mOldRev->getId();
689                 $newid = $this->mNewRev->getId();
690                 if ( $oldid > $newid ) {
691                         $tmp = $oldid; $oldid = $newid; $newid = $tmp;
692                 }
693
694                 $n = $this->mTitle->countRevisionsBetween( $oldid, $newid );
695                 if ( !$n )
696                 return '';
697
698                 return wfMsgExt( 'diff-multi', array( 'parseinline' ), $n );
699         }
700
701
702         /**
703          * Add the header to a diff body
704          */
705         static function addHeader( $diff, $otitle, $ntitle, $multi = '' ) {
706                 $header = "
707                 <table class='diff'>
708                 <col class='diff-marker' />
709                 <col class='diff-content' />
710                 <col class='diff-marker' />
711                 <col class='diff-content' />
712                 <tr valign='top'>
713                 <td colspan='2' class='diff-otitle'>{$otitle}</td>
714                 <td colspan='2' class='diff-ntitle'>{$ntitle}</td>
715                 </tr>
716                 ";
717
718                 if ( $multi != '' )
719                 $header .= "<tr><td colspan='4' align='center' class='diff-multi'>{$multi}</td></tr>";
720
721                 return $header . $diff . "</table>";
722         }
723
724         /**
725          * Use specified text instead of loading from the database
726          */
727         function setText( $oldText, $newText ) {
728                 $this->mOldtext = $oldText;
729                 $this->mNewtext = $newText;
730                 $this->mTextLoaded = 2;
731         }
732
733         /**
734          * Load revision metadata for the specified articles. If newid is 0, then compare
735          * the old article in oldid to the current article; if oldid is 0, then
736          * compare the current article to the immediately previous one (ignoring the
737          * value of newid).
738          *
739          * If oldid is false, leave the corresponding revision object set
740          * to false. This is impossible via ordinary user input, and is provided for
741          * API convenience.
742          */
743         function loadRevisionData() {
744                 global $wgLang, $wgUser;
745                 if ( $this->mRevisionsLoaded ) {
746                         return true;
747                 } else {
748                         // Whether it succeeds or fails, we don't want to try again
749                         $this->mRevisionsLoaded = true;
750                 }
751
752                 // Load the new revision object
753                 $this->mNewRev = $this->mNewid
754                 ? Revision::newFromId( $this->mNewid )
755                 : Revision::newFromTitle( $this->mTitle );
756                 if( !$this->mNewRev instanceof Revision )
757                 return false;
758
759                 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
760                 $this->mNewid = $this->mNewRev->getId();
761
762                 // Check if page is editable
763                 $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
764
765                 // Set assorted variables
766                 $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true );
767                 $this->mNewPage = $this->mNewRev->getTitle();
768                 if( $this->mNewRev->isCurrent() ) {
769                         $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid );
770                         $this->mPagetitle = wfMsgHTML( 'currentrev-asof', $timestamp );
771                         $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit' );
772
773                         $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
774                         $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
775
776                 } else {
777                         $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid );
778                         $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mNewid );
779                         $this->mPagetitle = wfMsgHTML( 'revisionasof', $timestamp );
780
781                         $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
782                         $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
783                 }
784                 if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
785                         $this->mNewtitle = "<span class='history-deleted'>{$this->mPagetitle}</span>";
786                 } else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
787                         $this->mNewtitle = '<span class="history-deleted">'.$this->mNewtitle.'</span>';
788                 }
789
790                 // Load the old revision object
791                 $this->mOldRev = false;
792                 if( $this->mOldid ) {
793                         $this->mOldRev = Revision::newFromId( $this->mOldid );
794                 } elseif ( $this->mOldid === 0 ) {
795                         $rev = $this->mNewRev->getPrevious();
796                         if( $rev ) {
797                                 $this->mOldid = $rev->getId();
798                                 $this->mOldRev = $rev;
799                         } else {
800                                 // No previous revision; mark to show as first-version only.
801                                 $this->mOldid = false;
802                                 $this->mOldRev = false;
803                         }
804                 }/* elseif ( $this->mOldid === false ) leave mOldRev false; */
805
806                 if( is_null( $this->mOldRev ) ) {
807                         return false;
808                 }
809
810                 if ( $this->mOldRev ) {
811                         $this->mOldPage = $this->mOldRev->getTitle();
812
813                         $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true );
814                         $oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid );
815                         $oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid );
816                         $this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t ) );
817
818                         $this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>"
819                         . " (<a href='$oldEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
820                         // Add an "undo" link
821                         $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid);
822                         $htmlLink = htmlspecialchars( wfMsg( 'editundo' ) );
823                         $htmlTitle = $wgUser->getSkin()->tooltip( 'undo' );
824                         if( $editable && !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
825                                 $this->mNewtitle .= " (<a href='$newUndo' $htmlTitle>" . $htmlLink . "</a>)";
826                         }
827
828                         if( !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) {
829                                 $this->mOldtitle = '<span class="history-deleted">' . $this->mOldPagetitle . '</span>';
830                         } else if( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
831                                 $this->mOldtitle = '<span class="history-deleted">' . $this->mOldtitle . '</span>';
832                         }
833                 }
834
835                 return true;
836         }
837
838         /**
839          * Load the text of the revisions, as well as revision data.
840          */
841         function loadText() {
842                 if ( $this->mTextLoaded == 2 ) {
843                         return true;
844                 } else {
845                         // Whether it succeeds or fails, we don't want to try again
846                         $this->mTextLoaded = 2;
847                 }
848
849                 if ( !$this->loadRevisionData() ) {
850                         return false;
851                 }
852                 if ( $this->mOldRev ) {
853                         $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER );
854                         if ( $this->mOldtext === false ) {
855                                 return false;
856                         }
857                 }
858                 if ( $this->mNewRev ) {
859                         $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
860                         if ( $this->mNewtext === false ) {
861                                 return false;
862                         }
863                 }
864                 return true;
865         }
866
867         /**
868          * Load the text of the new revision, not the old one
869          */
870         function loadNewText() {
871                 if ( $this->mTextLoaded >= 1 ) {
872                         return true;
873                 } else {
874                         $this->mTextLoaded = 1;
875                 }
876                 if ( !$this->loadRevisionData() ) {
877                         return false;
878                 }
879                 $this->mNewtext = $this->mNewRev->getText();
880                 return true;
881         }
882
883
884 }
885
886 // A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
887 //
888 // Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
889 // You may copy this code freely under the conditions of the GPL.
890 //
891
892 define('USE_ASSERTS', function_exists('assert'));
893
894 /**
895  * @todo document
896  * @private
897  * @ingroup DifferenceEngine
898  */
899 class _DiffOp {
900         var $type;
901         var $orig;
902         var $closing;
903
904         function reverse() {
905                 trigger_error('pure virtual', E_USER_ERROR);
906         }
907
908         function norig() {
909                 return $this->orig ? sizeof($this->orig) : 0;
910         }
911
912         function nclosing() {
913                 return $this->closing ? sizeof($this->closing) : 0;
914         }
915 }
916
917 /**
918  * @todo document
919  * @private
920  * @ingroup DifferenceEngine
921  */
922 class _DiffOp_Copy extends _DiffOp {
923         var $type = 'copy';
924
925         function _DiffOp_Copy ($orig, $closing = false) {
926                 if (!is_array($closing))
927                 $closing = $orig;
928                 $this->orig = $orig;
929                 $this->closing = $closing;
930         }
931
932         function reverse() {
933                 return new _DiffOp_Copy($this->closing, $this->orig);
934         }
935 }
936
937 /**
938  * @todo document
939  * @private
940  * @ingroup DifferenceEngine
941  */
942 class _DiffOp_Delete extends _DiffOp {
943         var $type = 'delete';
944
945         function _DiffOp_Delete ($lines) {
946                 $this->orig = $lines;
947                 $this->closing = false;
948         }
949
950         function reverse() {
951                 return new _DiffOp_Add($this->orig);
952         }
953 }
954
955 /**
956  * @todo document
957  * @private
958  * @ingroup DifferenceEngine
959  */
960 class _DiffOp_Add extends _DiffOp {
961         var $type = 'add';
962
963         function _DiffOp_Add ($lines) {
964                 $this->closing = $lines;
965                 $this->orig = false;
966         }
967
968         function reverse() {
969                 return new _DiffOp_Delete($this->closing);
970         }
971 }
972
973 /**
974  * @todo document
975  * @private
976  * @ingroup DifferenceEngine
977  */
978 class _DiffOp_Change extends _DiffOp {
979         var $type = 'change';
980
981         function _DiffOp_Change ($orig, $closing) {
982                 $this->orig = $orig;
983                 $this->closing = $closing;
984         }
985
986         function reverse() {
987                 return new _DiffOp_Change($this->closing, $this->orig);
988         }
989 }
990
991 /**
992  * Class used internally by Diff to actually compute the diffs.
993  *
994  * The algorithm used here is mostly lifted from the perl module
995  * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
996  *       http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
997  *
998  * More ideas are taken from:
999  *       http://www.ics.uci.edu/~eppstein/161/960229.html
1000  *
1001  * Some ideas are (and a bit of code) are from from analyze.c, from GNU
1002  * diffutils-2.7, which can be found at:
1003  *       ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
1004  *
1005  * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
1006  * are my own.
1007  *
1008  * Line length limits for robustness added by Tim Starling, 2005-08-31
1009  * Alternative implementation added by Guy Van den Broeck, 2008-07-30
1010  *
1011  * @author Geoffrey T. Dairiki, Tim Starling, Guy Van den Broeck
1012  * @private
1013  * @ingroup DifferenceEngine
1014  */
1015 class _DiffEngine {
1016
1017         const MAX_XREF_LENGTH =  10000;
1018
1019         function diff ($from_lines, $to_lines){
1020                 wfProfileIn( __METHOD__ );
1021
1022                 // Diff and store locally
1023                 $this->diff_local($from_lines, $to_lines);
1024
1025                 // Merge edits when possible
1026                 $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
1027                 $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
1028
1029                 // Compute the edit operations.
1030                 $n_from = sizeof($from_lines);
1031                 $n_to = sizeof($to_lines);
1032
1033                 $edits = array();
1034                 $xi = $yi = 0;
1035                 while ($xi < $n_from || $yi < $n_to) {
1036                         USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
1037                         USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
1038
1039                         // Skip matching "snake".
1040                         $copy = array();
1041                         while ( $xi < $n_from && $yi < $n_to
1042                         && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
1043                                 $copy[] = $from_lines[$xi++];
1044                                 ++$yi;
1045                         }
1046                         if ($copy)
1047                         $edits[] = new _DiffOp_Copy($copy);
1048
1049                         // Find deletes & adds.
1050                         $delete = array();
1051                         while ($xi < $n_from && $this->xchanged[$xi])
1052                         $delete[] = $from_lines[$xi++];
1053
1054                         $add = array();
1055                         while ($yi < $n_to && $this->ychanged[$yi])
1056                         $add[] = $to_lines[$yi++];
1057
1058                         if ($delete && $add)
1059                         $edits[] = new _DiffOp_Change($delete, $add);
1060                         elseif ($delete)
1061                         $edits[] = new _DiffOp_Delete($delete);
1062                         elseif ($add)
1063                         $edits[] = new _DiffOp_Add($add);
1064                 }
1065                 wfProfileOut( __METHOD__ );
1066                 return $edits;
1067         }
1068
1069         function diff_local ($from_lines, $to_lines) {
1070                 global $wgExternalDiffEngine;
1071                 wfProfileIn( __METHOD__);
1072
1073                 if($wgExternalDiffEngine == 'wikidiff3'){
1074                         // wikidiff3
1075                         $wikidiff3 = new WikiDiff3();
1076                         $wikidiff3->diff($from_lines, $to_lines);
1077                         $this->xchanged = $wikidiff3->removed;
1078                         $this->ychanged = $wikidiff3->added;
1079                         unset($wikidiff3);
1080                 }else{
1081                         // old diff
1082                         $n_from = sizeof($from_lines);
1083                         $n_to = sizeof($to_lines);
1084                         $this->xchanged = $this->ychanged = array();
1085                         $this->xv = $this->yv = array();
1086                         $this->xind = $this->yind = array();
1087                         unset($this->seq);
1088                         unset($this->in_seq);
1089                         unset($this->lcs);
1090
1091                         // Skip leading common lines.
1092                         for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
1093                                 if ($from_lines[$skip] !== $to_lines[$skip])
1094                                 break;
1095                                 $this->xchanged[$skip] = $this->ychanged[$skip] = false;
1096                         }
1097                         // Skip trailing common lines.
1098                         $xi = $n_from; $yi = $n_to;
1099                         for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
1100                                 if ($from_lines[$xi] !== $to_lines[$yi])
1101                                 break;
1102                                 $this->xchanged[$xi] = $this->ychanged[$yi] = false;
1103                         }
1104
1105                         // Ignore lines which do not exist in both files.
1106                         for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
1107                                 $xhash[$this->_line_hash($from_lines[$xi])] = 1;
1108                         }
1109
1110                         for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
1111                                 $line = $to_lines[$yi];
1112                                 if ( ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) )
1113                                 continue;
1114                                 $yhash[$this->_line_hash($line)] = 1;
1115                                 $this->yv[] = $line;
1116                                 $this->yind[] = $yi;
1117                         }
1118                         for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
1119                                 $line = $from_lines[$xi];
1120                                 if ( ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) )
1121                                 continue;
1122                                 $this->xv[] = $line;
1123                                 $this->xind[] = $xi;
1124                         }
1125
1126                         // Find the LCS.
1127                         $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
1128                 }
1129                 wfProfileOut( __METHOD__ );
1130         }
1131
1132         /**
1133          * Returns the whole line if it's small enough, or the MD5 hash otherwise
1134          */
1135         function _line_hash( $line ) {
1136                 if ( strlen( $line ) > self::MAX_XREF_LENGTH ) {
1137                         return md5( $line );
1138                 } else {
1139                         return $line;
1140                 }
1141         }
1142
1143         /* Divide the Largest Common Subsequence (LCS) of the sequences
1144          * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
1145          * sized segments.
1146          *
1147          * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an
1148          * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
1149          * sub sequences.  The first sub-sequence is contained in [X0, X1),
1150          * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on.  Note
1151          * that (X0, Y0) == (XOFF, YOFF) and
1152          * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
1153          *
1154          * This function assumes that the first lines of the specified portions
1155          * of the two files do not match, and likewise that the last lines do not
1156          * match.  The caller must trim matching lines from the beginning and end
1157          * of the portions it is going to specify.
1158          */
1159         function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) {
1160                 $flip = false;
1161
1162                 if ($xlim - $xoff > $ylim - $yoff) {
1163                         // Things seems faster (I'm not sure I understand why)
1164                         // when the shortest sequence in X.
1165                         $flip = true;
1166                         list ($xoff, $xlim, $yoff, $ylim)
1167                         = array( $yoff, $ylim, $xoff, $xlim);
1168                 }
1169
1170                 if ($flip)
1171                 for ($i = $ylim - 1; $i >= $yoff; $i--)
1172                 $ymatches[$this->xv[$i]][] = $i;
1173                 else
1174                 for ($i = $ylim - 1; $i >= $yoff; $i--)
1175                 $ymatches[$this->yv[$i]][] = $i;
1176
1177                 $this->lcs = 0;
1178                 $this->seq[0]= $yoff - 1;
1179                 $this->in_seq = array();
1180                 $ymids[0] = array();
1181
1182                 $numer = $xlim - $xoff + $nchunks - 1;
1183                 $x = $xoff;
1184                 for ($chunk = 0; $chunk < $nchunks; $chunk++) {
1185                         if ($chunk > 0)
1186                         for ($i = 0; $i <= $this->lcs; $i++)
1187                         $ymids[$i][$chunk-1] = $this->seq[$i];
1188
1189                         $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
1190                         for ( ; $x < $x1; $x++) {
1191                                 $line = $flip ? $this->yv[$x] : $this->xv[$x];
1192                                 if (empty($ymatches[$line]))
1193                                 continue;
1194                                 $matches = $ymatches[$line];
1195                                 reset($matches);
1196                                 while (list ($junk, $y) = each($matches))
1197                                 if (empty($this->in_seq[$y])) {
1198                                         $k = $this->_lcs_pos($y);
1199                                         USE_ASSERTS && assert($k > 0);
1200                                         $ymids[$k] = $ymids[$k-1];
1201                                         break;
1202                                 }
1203                                 while (list ( /* $junk */, $y) = each($matches)) {
1204                                         if ($y > $this->seq[$k-1]) {
1205                                                 USE_ASSERTS && assert($y < $this->seq[$k]);
1206                                                 // Optimization: this is a common case:
1207                                                 //      next match is just replacing previous match.
1208                                                 $this->in_seq[$this->seq[$k]] = false;
1209                                                 $this->seq[$k] = $y;
1210                                                 $this->in_seq[$y] = 1;
1211                                         } else if (empty($this->in_seq[$y])) {
1212                                                 $k = $this->_lcs_pos($y);
1213                                                 USE_ASSERTS && assert($k > 0);
1214                                                 $ymids[$k] = $ymids[$k-1];
1215                                         }
1216                                 }
1217                         }
1218                 }
1219
1220                 $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
1221                 $ymid = $ymids[$this->lcs];
1222                 for ($n = 0; $n < $nchunks - 1; $n++) {
1223                         $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
1224                         $y1 = $ymid[$n] + 1;
1225                         $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
1226                 }
1227                 $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
1228
1229                 return array($this->lcs, $seps);
1230         }
1231
1232         function _lcs_pos ($ypos) {
1233                 $end = $this->lcs;
1234                 if ($end == 0 || $ypos > $this->seq[$end]) {
1235                         $this->seq[++$this->lcs] = $ypos;
1236                         $this->in_seq[$ypos] = 1;
1237                         return $this->lcs;
1238                 }
1239
1240                 $beg = 1;
1241                 while ($beg < $end) {
1242                         $mid = (int)(($beg + $end) / 2);
1243                         if ( $ypos > $this->seq[$mid] )
1244                         $beg = $mid + 1;
1245                         else
1246                         $end = $mid;
1247                 }
1248
1249                 USE_ASSERTS && assert($ypos != $this->seq[$end]);
1250
1251                 $this->in_seq[$this->seq[$end]] = false;
1252                 $this->seq[$end] = $ypos;
1253                 $this->in_seq[$ypos] = 1;
1254                 return $end;
1255         }
1256
1257         /* Find LCS of two sequences.
1258          *
1259          * The results are recorded in the vectors $this->{x,y}changed[], by
1260          * storing a 1 in the element for each line that is an insertion
1261          * or deletion (ie. is not in the LCS).
1262          *
1263          * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
1264          *
1265          * Note that XLIM, YLIM are exclusive bounds.
1266          * All line numbers are origin-0 and discarded lines are not counted.
1267          */
1268         function _compareseq ($xoff, $xlim, $yoff, $ylim) {
1269                 // Slide down the bottom initial diagonal.
1270                 while ($xoff < $xlim && $yoff < $ylim
1271                 && $this->xv[$xoff] == $this->yv[$yoff]) {
1272                         ++$xoff;
1273                         ++$yoff;
1274                 }
1275
1276                 // Slide up the top initial diagonal.
1277                 while ($xlim > $xoff && $ylim > $yoff
1278                 && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
1279                         --$xlim;
1280                         --$ylim;
1281                 }
1282
1283                 if ($xoff == $xlim || $yoff == $ylim)
1284                 $lcs = 0;
1285                 else {
1286                         // This is ad hoc but seems to work well.
1287                         //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
1288                         //$nchunks = max(2,min(8,(int)$nchunks));
1289                         $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
1290                         list ($lcs, $seps)
1291                         = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks);
1292                 }
1293
1294                 if ($lcs == 0) {
1295                         // X and Y sequences have no common subsequence:
1296                         // mark all changed.
1297                         while ($yoff < $ylim)
1298                         $this->ychanged[$this->yind[$yoff++]] = 1;
1299                         while ($xoff < $xlim)
1300                         $this->xchanged[$this->xind[$xoff++]] = 1;
1301                 } else {
1302                         // Use the partitions to split this problem into subproblems.
1303                         reset($seps);
1304                         $pt1 = $seps[0];
1305                         while ($pt2 = next($seps)) {
1306                                 $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
1307                                 $pt1 = $pt2;
1308                         }
1309                 }
1310         }
1311
1312         /* Adjust inserts/deletes of identical lines to join changes
1313          * as much as possible.
1314          *
1315          * We do something when a run of changed lines include a
1316          * line at one end and has an excluded, identical line at the other.
1317          * We are free to choose which identical line is included.
1318          * `compareseq' usually chooses the one at the beginning,
1319          * but usually it is cleaner to consider the following identical line
1320          * to be the "change".
1321          *
1322          * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
1323          */
1324         function _shift_boundaries ($lines, &$changed, $other_changed) {
1325                 wfProfileIn( __METHOD__ );
1326                 $i = 0;
1327                 $j = 0;
1328
1329                 USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
1330                 $len = sizeof($lines);
1331                 $other_len = sizeof($other_changed);
1332
1333                 while (1) {
1334                         /*
1335                          * Scan forwards to find beginning of another run of changes.
1336                          * Also keep track of the corresponding point in the other file.
1337                          *
1338                          * Throughout this code, $i and $j are adjusted together so that
1339                          * the first $i elements of $changed and the first $j elements
1340                          * of $other_changed both contain the same number of zeros
1341                          * (unchanged lines).
1342                          * Furthermore, $j is always kept so that $j == $other_len or
1343                          * $other_changed[$j] == false.
1344                          */
1345                         while ($j < $other_len && $other_changed[$j])
1346                         $j++;
1347
1348                         while ($i < $len && ! $changed[$i]) {
1349                                 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
1350                                 $i++; $j++;
1351                                 while ($j < $other_len && $other_changed[$j])
1352                                 $j++;
1353                         }
1354
1355                         if ($i == $len)
1356                         break;
1357
1358                         $start = $i;
1359
1360                         // Find the end of this run of changes.
1361                         while (++$i < $len && $changed[$i])
1362                         continue;
1363
1364                         do {
1365                                 /*
1366                                  * Record the length of this run of changes, so that
1367                                  * we can later determine whether the run has grown.
1368                                  */
1369                                 $runlength = $i - $start;
1370
1371                                 /*
1372                                  * Move the changed region back, so long as the
1373                                  * previous unchanged line matches the last changed one.
1374                                  * This merges with previous changed regions.
1375                                  */
1376                                 while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
1377                                         $changed[--$start] = 1;
1378                                         $changed[--$i] = false;
1379                                         while ($start > 0 && $changed[$start - 1])
1380                                         $start--;
1381                                         USE_ASSERTS && assert('$j > 0');
1382                                         while ($other_changed[--$j])
1383                                         continue;
1384                                         USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
1385                                 }
1386
1387                                 /*
1388                                  * Set CORRESPONDING to the end of the changed run, at the last
1389                                  * point where it corresponds to a changed run in the other file.
1390                                  * CORRESPONDING == LEN means no such point has been found.
1391                                  */
1392                                 $corresponding = $j < $other_len ? $i : $len;
1393
1394                                 /*
1395                                  * Move the changed region forward, so long as the
1396                                  * first changed line matches the following unchanged one.
1397                                  * This merges with following changed regions.
1398                                  * Do this second, so that if there are no merges,
1399                                  * the changed region is moved forward as far as possible.
1400                                  */
1401                                 while ($i < $len && $lines[$start] == $lines[$i]) {
1402                                         $changed[$start++] = false;
1403                                         $changed[$i++] = 1;
1404                                         while ($i < $len && $changed[$i])
1405                                         $i++;
1406
1407                                         USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
1408                                         $j++;
1409                                         if ($j < $other_len && $other_changed[$j]) {
1410                                                 $corresponding = $i;
1411                                                 while ($j < $other_len && $other_changed[$j])
1412                                                 $j++;
1413                                         }
1414                                 }
1415                         } while ($runlength != $i - $start);
1416
1417                         /*
1418                          * If possible, move the fully-merged run of changes
1419                          * back to a corresponding run in the other file.
1420                          */
1421                         while ($corresponding < $i) {
1422                                 $changed[--$start] = 1;
1423                                 $changed[--$i] = 0;
1424                                 USE_ASSERTS && assert('$j > 0');
1425                                 while ($other_changed[--$j])
1426                                 continue;
1427                                 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
1428                         }
1429                 }
1430                 wfProfileOut( __METHOD__ );
1431         }
1432 }
1433
1434 /**
1435  * Class representing a 'diff' between two sequences of strings.
1436  * @todo document
1437  * @private
1438  * @ingroup DifferenceEngine
1439  */
1440 class Diff
1441 {
1442         var $edits;
1443
1444         /**
1445          * Constructor.
1446          * Computes diff between sequences of strings.
1447          *
1448          * @param $from_lines array An array of strings.
1449          *                (Typically these are lines from a file.)
1450          * @param $to_lines array An array of strings.
1451          */
1452         function Diff($from_lines, $to_lines) {
1453                 $eng = new _DiffEngine;
1454                 $this->edits = $eng->diff($from_lines, $to_lines);
1455                 //$this->_check($from_lines, $to_lines);
1456         }
1457
1458         /**
1459          * Compute reversed Diff.
1460          *
1461          * SYNOPSIS:
1462          *
1463          *      $diff = new Diff($lines1, $lines2);
1464          *      $rev = $diff->reverse();
1465          * @return object A Diff object representing the inverse of the
1466          *                                original diff.
1467          */
1468         function reverse () {
1469                 $rev = $this;
1470                 $rev->edits = array();
1471                 foreach ($this->edits as $edit) {
1472                         $rev->edits[] = $edit->reverse();
1473                 }
1474                 return $rev;
1475         }
1476
1477         /**
1478          * Check for empty diff.
1479          *
1480          * @return bool True iff two sequences were identical.
1481          */
1482         function isEmpty () {
1483                 foreach ($this->edits as $edit) {
1484                         if ($edit->type != 'copy')
1485                         return false;
1486                 }
1487                 return true;
1488         }
1489
1490         /**
1491          * Compute the length of the Longest Common Subsequence (LCS).
1492          *
1493          * This is mostly for diagnostic purposed.
1494          *
1495          * @return int The length of the LCS.
1496          */
1497         function lcs () {
1498                 $lcs = 0;
1499                 foreach ($this->edits as $edit) {
1500                         if ($edit->type == 'copy')
1501                         $lcs += sizeof($edit->orig);
1502                 }
1503                 return $lcs;
1504         }
1505
1506         /**
1507          * Get the original set of lines.
1508          *
1509          * This reconstructs the $from_lines parameter passed to the
1510          * constructor.
1511          *
1512          * @return array The original sequence of strings.
1513          */
1514         function orig() {
1515                 $lines = array();
1516
1517                 foreach ($this->edits as $edit) {
1518                         if ($edit->orig)
1519                         array_splice($lines, sizeof($lines), 0, $edit->orig);
1520                 }
1521                 return $lines;
1522         }
1523
1524         /**
1525          * Get the closing set of lines.
1526          *
1527          * This reconstructs the $to_lines parameter passed to the
1528          * constructor.
1529          *
1530          * @return array The sequence of strings.
1531          */
1532         function closing() {
1533                 $lines = array();
1534
1535                 foreach ($this->edits as $edit) {
1536                         if ($edit->closing)
1537                         array_splice($lines, sizeof($lines), 0, $edit->closing);
1538                 }
1539                 return $lines;
1540         }
1541
1542         /**
1543          * Check a Diff for validity.
1544          *
1545          * This is here only for debugging purposes.
1546          */
1547         function _check ($from_lines, $to_lines) {
1548                 wfProfileIn( __METHOD__ );
1549                 if (serialize($from_lines) != serialize($this->orig()))
1550                 trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
1551                 if (serialize($to_lines) != serialize($this->closing()))
1552                 trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
1553
1554                 $rev = $this->reverse();
1555                 if (serialize($to_lines) != serialize($rev->orig()))
1556                 trigger_error("Reversed original doesn't match", E_USER_ERROR);
1557                 if (serialize($from_lines) != serialize($rev->closing()))
1558                 trigger_error("Reversed closing doesn't match", E_USER_ERROR);
1559
1560
1561                 $prevtype = 'none';
1562                 foreach ($this->edits as $edit) {
1563                         if ( $prevtype == $edit->type )
1564                         trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
1565                         $prevtype = $edit->type;
1566                 }
1567
1568                 $lcs = $this->lcs();
1569                 trigger_error('Diff okay: LCS = '.$lcs, E_USER_NOTICE);
1570                 wfProfileOut( __METHOD__ );
1571         }
1572 }
1573
1574 /**
1575  * @todo document, bad name.
1576  * @private
1577  * @ingroup DifferenceEngine
1578  */
1579 class MappedDiff extends Diff
1580 {
1581         /**
1582          * Constructor.
1583          *
1584          * Computes diff between sequences of strings.
1585          *
1586          * This can be used to compute things like
1587          * case-insensitve diffs, or diffs which ignore
1588          * changes in white-space.
1589          *
1590          * @param $from_lines array An array of strings.
1591          *      (Typically these are lines from a file.)
1592          *
1593          * @param $to_lines array An array of strings.
1594          *
1595          * @param $mapped_from_lines array This array should
1596          *      have the same size number of elements as $from_lines.
1597          *      The elements in $mapped_from_lines and
1598          *      $mapped_to_lines are what is actually compared
1599          *      when computing the diff.
1600          *
1601          * @param $mapped_to_lines array This array should
1602          *      have the same number of elements as $to_lines.
1603          */
1604         function MappedDiff($from_lines, $to_lines,
1605         $mapped_from_lines, $mapped_to_lines) {
1606                 wfProfileIn( __METHOD__ );
1607
1608                 assert(sizeof($from_lines) == sizeof($mapped_from_lines));
1609                 assert(sizeof($to_lines) == sizeof($mapped_to_lines));
1610
1611                 $this->Diff($mapped_from_lines, $mapped_to_lines);
1612
1613                 $xi = $yi = 0;
1614                 for ($i = 0; $i < sizeof($this->edits); $i++) {
1615                         $orig = &$this->edits[$i]->orig;
1616                         if (is_array($orig)) {
1617                                 $orig = array_slice($from_lines, $xi, sizeof($orig));
1618                                 $xi += sizeof($orig);
1619                         }
1620
1621                         $closing = &$this->edits[$i]->closing;
1622                         if (is_array($closing)) {
1623                                 $closing = array_slice($to_lines, $yi, sizeof($closing));
1624                                 $yi += sizeof($closing);
1625                         }
1626                 }
1627                 wfProfileOut( __METHOD__ );
1628         }
1629 }
1630
1631 /**
1632  * A class to format Diffs
1633  *
1634  * This class formats the diff in classic diff format.
1635  * It is intended that this class be customized via inheritance,
1636  * to obtain fancier outputs.
1637  * @todo document
1638  * @private
1639  * @ingroup DifferenceEngine
1640  */
1641 class DiffFormatter {
1642         /**
1643          * Number of leading context "lines" to preserve.
1644          *
1645          * This should be left at zero for this class, but subclasses
1646          * may want to set this to other values.
1647          */
1648         var $leading_context_lines = 0;
1649
1650         /**
1651          * Number of trailing context "lines" to preserve.
1652          *
1653          * This should be left at zero for this class, but subclasses
1654          * may want to set this to other values.
1655          */
1656         var $trailing_context_lines = 0;
1657
1658         /**
1659          * Format a diff.
1660          *
1661          * @param $diff object A Diff object.
1662          * @return string The formatted output.
1663          */
1664         function format($diff) {
1665                 wfProfileIn( __METHOD__ );
1666
1667                 $xi = $yi = 1;
1668                 $block = false;
1669                 $context = array();
1670
1671                 $nlead = $this->leading_context_lines;
1672                 $ntrail = $this->trailing_context_lines;
1673
1674                 $this->_start_diff();
1675
1676                 foreach ($diff->edits as $edit) {
1677                         if ($edit->type == 'copy') {
1678                                 if (is_array($block)) {
1679                                         if (sizeof($edit->orig) <= $nlead + $ntrail) {
1680                                                 $block[] = $edit;
1681                                         }
1682                                         else{
1683                                                 if ($ntrail) {
1684                                                         $context = array_slice($edit->orig, 0, $ntrail);
1685                                                         $block[] = new _DiffOp_Copy($context);
1686                                                 }
1687                                                 $this->_block($x0, $ntrail + $xi - $x0,
1688                                                 $y0, $ntrail + $yi - $y0,
1689                                                 $block);
1690                                                 $block = false;
1691                                         }
1692                                 }
1693                                 $context = $edit->orig;
1694                         }
1695                         else {
1696                                 if (! is_array($block)) {
1697                                         $context = array_slice($context, sizeof($context) - $nlead);
1698                                         $x0 = $xi - sizeof($context);
1699                                         $y0 = $yi - sizeof($context);
1700                                         $block = array();
1701                                         if ($context)
1702                                         $block[] = new _DiffOp_Copy($context);
1703                                 }
1704                                 $block[] = $edit;
1705                         }
1706
1707                         if ($edit->orig)
1708                         $xi += sizeof($edit->orig);
1709                         if ($edit->closing)
1710                         $yi += sizeof($edit->closing);
1711                 }
1712
1713                 if (is_array($block))
1714                 $this->_block($x0, $xi - $x0,
1715                 $y0, $yi - $y0,
1716                 $block);
1717
1718                 $end = $this->_end_diff();
1719                 wfProfileOut( __METHOD__ );
1720                 return $end;
1721         }
1722
1723         function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
1724                 wfProfileIn( __METHOD__ );
1725                 $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
1726                 foreach ($edits as $edit) {
1727                         if ($edit->type == 'copy')
1728                         $this->_context($edit->orig);
1729                         elseif ($edit->type == 'add')
1730                         $this->_added($edit->closing);
1731                         elseif ($edit->type == 'delete')
1732                         $this->_deleted($edit->orig);
1733                         elseif ($edit->type == 'change')
1734                         $this->_changed($edit->orig, $edit->closing);
1735                         else
1736                         trigger_error('Unknown edit type', E_USER_ERROR);
1737                 }
1738                 $this->_end_block();
1739                 wfProfileOut( __METHOD__ );
1740         }
1741
1742         function _start_diff() {
1743                 ob_start();
1744         }
1745
1746         function _end_diff() {
1747                 $val = ob_get_contents();
1748                 ob_end_clean();
1749                 return $val;
1750         }
1751
1752         function _block_header($xbeg, $xlen, $ybeg, $ylen) {
1753                 if ($xlen > 1)
1754                 $xbeg .= "," . ($xbeg + $xlen - 1);
1755                 if ($ylen > 1)
1756                 $ybeg .= "," . ($ybeg + $ylen - 1);
1757
1758                 return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
1759         }
1760
1761         function _start_block($header) {
1762                 echo $header . "\n";
1763         }
1764
1765         function _end_block() {
1766         }
1767
1768         function _lines($lines, $prefix = ' ') {
1769                 foreach ($lines as $line)
1770                 echo "$prefix $line\n";
1771         }
1772
1773         function _context($lines) {
1774                 $this->_lines($lines);
1775         }
1776
1777         function _added($lines) {
1778                 $this->_lines($lines, '>');
1779         }
1780         function _deleted($lines) {
1781                 $this->_lines($lines, '<');
1782         }
1783
1784         function _changed($orig, $closing) {
1785                 $this->_deleted($orig);
1786                 echo "---\n";
1787                 $this->_added($closing);
1788         }
1789 }
1790
1791 /**
1792  * A formatter that outputs unified diffs
1793  * @ingroup DifferenceEngine
1794  */
1795
1796 class UnifiedDiffFormatter extends DiffFormatter {
1797         var $leading_context_lines = 2;
1798         var $trailing_context_lines = 2;
1799
1800         function _added($lines) {
1801                 $this->_lines($lines, '+');
1802         }
1803         function _deleted($lines) {
1804                 $this->_lines($lines, '-');
1805         }
1806         function _changed($orig, $closing) {
1807                 $this->_deleted($orig);
1808                 $this->_added($closing);
1809         }
1810         function _block_header($xbeg, $xlen, $ybeg, $ylen) {
1811                 return "@@ -$xbeg,$xlen +$ybeg,$ylen @@";
1812         }
1813 }
1814
1815 /**
1816  * A pseudo-formatter that just passes along the Diff::$edits array
1817  * @ingroup DifferenceEngine
1818  */
1819 class ArrayDiffFormatter extends DiffFormatter {
1820         function format($diff) {
1821                 $oldline = 1;
1822                 $newline = 1;
1823                 $retval = array();
1824                 foreach($diff->edits as $edit)
1825                 switch($edit->type) {
1826                         case 'add':
1827                                 foreach($edit->closing as $l) {
1828                                         $retval[] = array(
1829                                                         'action' => 'add',
1830                                                         'new'=> $l,
1831                                                         'newline' => $newline++
1832                                         );
1833                                 }
1834                                 break;
1835                         case 'delete':
1836                                 foreach($edit->orig as $l) {
1837                                         $retval[] = array(
1838                                                         'action' => 'delete',
1839                                                         'old' => $l,
1840                                                         'oldline' => $oldline++,
1841                                         );
1842                                 }
1843                                 break;
1844                         case 'change':
1845                                 foreach($edit->orig as $i => $l) {
1846                                         $retval[] = array(
1847                                                         'action' => 'change',
1848                                                         'old' => $l,
1849                                                         'new' => @$edit->closing[$i],
1850                                                         'oldline' => $oldline++,
1851                                                         'newline' => $newline++,
1852                                         );
1853                                 }
1854                                 break;
1855                         case 'copy':
1856                                 $oldline += count($edit->orig);
1857                                 $newline += count($edit->orig);
1858                 }
1859                 return $retval;
1860         }
1861 }
1862
1863 /**
1864  *      Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
1865  *
1866  */
1867
1868 define('NBSP', '&#160;'); // iso-8859-x non-breaking space.
1869
1870 /**
1871  * @todo document
1872  * @private
1873  * @ingroup DifferenceEngine
1874  */
1875 class _HWLDF_WordAccumulator {
1876         function _HWLDF_WordAccumulator () {
1877                 $this->_lines = array();
1878                 $this->_line = '';
1879                 $this->_group = '';
1880                 $this->_tag = '';
1881         }
1882
1883         function _flushGroup ($new_tag) {
1884                 if ($this->_group !== '') {
1885                         if ($this->_tag == 'ins')
1886                         $this->_line .= '<ins class="diffchange diffchange-inline">' .
1887                         htmlspecialchars ( $this->_group ) . '</ins>';
1888                         elseif ($this->_tag == 'del')
1889                         $this->_line .= '<del class="diffchange diffchange-inline">' .
1890                         htmlspecialchars ( $this->_group ) . '</del>';
1891                         else
1892                         $this->_line .= htmlspecialchars ( $this->_group );
1893                 }
1894                 $this->_group = '';
1895                 $this->_tag = $new_tag;
1896         }
1897
1898         function _flushLine ($new_tag) {
1899                 $this->_flushGroup($new_tag);
1900                 if ($this->_line != '')
1901                 array_push ( $this->_lines, $this->_line );
1902                 else
1903                 # make empty lines visible by inserting an NBSP
1904                 array_push ( $this->_lines, NBSP );
1905                 $this->_line = '';
1906         }
1907
1908         function addWords ($words, $tag = '') {
1909                 if ($tag != $this->_tag)
1910                 $this->_flushGroup($tag);
1911
1912                 foreach ($words as $word) {
1913                         // new-line should only come as first char of word.
1914                         if ($word == '')
1915                         continue;
1916                         if ($word[0] == "\n") {
1917                                 $this->_flushLine($tag);
1918                                 $word = substr($word, 1);
1919                         }
1920                         assert(!strstr($word, "\n"));
1921                         $this->_group .= $word;
1922                 }
1923         }
1924
1925         function getLines() {
1926                 $this->_flushLine('~done');
1927                 return $this->_lines;
1928         }
1929 }
1930
1931 /**
1932  * @todo document
1933  * @private
1934  * @ingroup DifferenceEngine
1935  */
1936 class WordLevelDiff extends MappedDiff {
1937         const MAX_LINE_LENGTH = 10000;
1938
1939         function WordLevelDiff ($orig_lines, $closing_lines) {
1940                 wfProfileIn( __METHOD__ );
1941
1942                 list ($orig_words, $orig_stripped) = $this->_split($orig_lines);
1943                 list ($closing_words, $closing_stripped) = $this->_split($closing_lines);
1944
1945                 $this->MappedDiff($orig_words, $closing_words,
1946                 $orig_stripped, $closing_stripped);
1947                 wfProfileOut( __METHOD__ );
1948         }
1949
1950         function _split($lines) {
1951                 wfProfileIn( __METHOD__ );
1952
1953                 $words = array();
1954                 $stripped = array();
1955                 $first = true;
1956                 foreach ( $lines as $line ) {
1957                         # If the line is too long, just pretend the entire line is one big word
1958                         # This prevents resource exhaustion problems
1959                         if ( $first ) {
1960                                 $first = false;
1961                         } else {
1962                                 $words[] = "\n";
1963                                 $stripped[] = "\n";
1964                         }
1965                         if ( strlen( $line ) > self::MAX_LINE_LENGTH ) {
1966                                 $words[] = $line;
1967                                 $stripped[] = $line;
1968                         } else {
1969                                 $m = array();
1970                                 if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
1971                                 $line, $m))
1972                                 {
1973                                         $words = array_merge( $words, $m[0] );
1974                                         $stripped = array_merge( $stripped, $m[1] );
1975                                 }
1976                         }
1977                 }
1978                 wfProfileOut( __METHOD__ );
1979                 return array($words, $stripped);
1980         }
1981
1982         function orig () {
1983                 wfProfileIn( __METHOD__ );
1984                 $orig = new _HWLDF_WordAccumulator;
1985
1986                 foreach ($this->edits as $edit) {
1987                         if ($edit->type == 'copy')
1988                         $orig->addWords($edit->orig);
1989                         elseif ($edit->orig)
1990                         $orig->addWords($edit->orig, 'del');
1991                 }
1992                 $lines = $orig->getLines();
1993                 wfProfileOut( __METHOD__ );
1994                 return $lines;
1995         }
1996
1997         function closing () {
1998                 wfProfileIn( __METHOD__ );
1999                 $closing = new _HWLDF_WordAccumulator;
2000
2001                 foreach ($this->edits as $edit) {
2002                         if ($edit->type == 'copy')
2003                         $closing->addWords($edit->closing);
2004                         elseif ($edit->closing)
2005                         $closing->addWords($edit->closing, 'ins');
2006                 }
2007                 $lines = $closing->getLines();
2008                 wfProfileOut( __METHOD__ );
2009                 return $lines;
2010         }
2011 }
2012
2013 /**
2014  * Wikipedia Table style diff formatter.
2015  * @todo document
2016  * @private
2017  * @ingroup DifferenceEngine
2018  */
2019 class TableDiffFormatter extends DiffFormatter {
2020         function TableDiffFormatter() {
2021                 $this->leading_context_lines = 2;
2022                 $this->trailing_context_lines = 2;
2023         }
2024
2025         public static function escapeWhiteSpace( $msg ) {
2026                 $msg = preg_replace( '/^ /m', '&nbsp; ', $msg );
2027                 $msg = preg_replace( '/ $/m', ' &nbsp;', $msg );
2028                 $msg = preg_replace( '/  /', '&nbsp; ', $msg );
2029                 return $msg;
2030         }
2031
2032         function _block_header( $xbeg, $xlen, $ybeg, $ylen ) {
2033                 $r = '<tr><td colspan="2" class="diff-lineno"><!--LINE '.$xbeg."--></td>\n" .
2034                   '<td colspan="2" class="diff-lineno"><!--LINE '.$ybeg."--></td></tr>\n";
2035                 return $r;
2036         }
2037
2038         function _start_block( $header ) {
2039                 echo $header;
2040         }
2041
2042         function _end_block() {
2043         }
2044
2045         function _lines( $lines, $prefix=' ', $color='white' ) {
2046         }
2047
2048         # HTML-escape parameter before calling this
2049         function addedLine( $line ) {
2050                 return $this->wrapLine( '+', 'diff-addedline', $line );
2051         }
2052
2053         # HTML-escape parameter before calling this
2054         function deletedLine( $line ) {
2055                 return $this->wrapLine( '-', 'diff-deletedline', $line );
2056         }
2057
2058         # HTML-escape parameter before calling this
2059         function contextLine( $line ) {
2060                 return $this->wrapLine( ' ', 'diff-context', $line );
2061         }
2062
2063         private function wrapLine( $marker, $class, $line ) {
2064                 if( $line !== '' ) {
2065                         // The <div> wrapper is needed for 'overflow: auto' style to scroll properly
2066                         $line = Xml::tags( 'div', null, $this->escapeWhiteSpace( $line ) );
2067                 }
2068                 return "<td class='diff-marker'>$marker</td><td class='$class'>$line</td>";
2069         }
2070
2071         function emptyLine() {
2072                 return '<td colspan="2">&nbsp;</td>';
2073         }
2074
2075         function _added( $lines ) {
2076                 foreach ($lines as $line) {
2077                         echo '<tr>' . $this->emptyLine() .
2078                         $this->addedLine( '<ins class="diffchange">' .
2079                         htmlspecialchars ( $line ) . '</ins>' ) . "</tr>\n";
2080                 }
2081         }
2082
2083         function _deleted($lines) {
2084                 foreach ($lines as $line) {
2085                         echo '<tr>' . $this->deletedLine( '<del class="diffchange">' .
2086                         htmlspecialchars ( $line ) . '</del>' ) .
2087                         $this->emptyLine() . "</tr>\n";
2088                 }
2089         }
2090
2091         function _context( $lines ) {
2092                 foreach ($lines as $line) {
2093                         echo '<tr>' .
2094                         $this->contextLine( htmlspecialchars ( $line ) ) .
2095                         $this->contextLine( htmlspecialchars ( $line ) ) . "</tr>\n";
2096                 }
2097         }
2098
2099         function _changed( $orig, $closing ) {
2100                 wfProfileIn( __METHOD__ );
2101
2102                 $diff = new WordLevelDiff( $orig, $closing );
2103                 $del = $diff->orig();
2104                 $add = $diff->closing();
2105
2106                 # Notice that WordLevelDiff returns HTML-escaped output.
2107                 # Hence, we will be calling addedLine/deletedLine without HTML-escaping.
2108
2109                 while ( $line = array_shift( $del ) ) {
2110                         $aline = array_shift( $add );
2111                         echo '<tr>' . $this->deletedLine( $line ) .
2112                         $this->addedLine( $aline ) . "</tr>\n";
2113                 }
2114                 foreach ($add as $line) {       # If any leftovers
2115                         echo '<tr>' . $this->emptyLine() .
2116                         $this->addedLine( $line ) . "</tr>\n";
2117                 }
2118                 wfProfileOut( __METHOD__ );
2119         }
2120 }