00001 <?php
00012 define( 'MW_DIFF_VERSION', '1.11a' );
00013
00018 class DifferenceEngine {
00022 var $mOldid, $mNewid, $mTitle;
00023 var $mOldtitle, $mNewtitle, $mPagetitle;
00024 var $mOldtext, $mNewtext;
00025 var $mOldPage, $mNewPage;
00026 var $mRcidMarkPatrolled;
00027 var $mOldRev, $mNewRev;
00028 var $mRevisionsLoaded = false;
00029 var $mTextLoaded = 0;
00030 var $mCacheHit = false;
00031 var $htmldiff;
00032
00033 protected $unhide = false;
00046 function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0, $refreshCache = false , $htmldiff = false, $unhide = false ) {
00047 $this->mTitle = $titleObj;
00048 wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
00049
00050 if ( 'prev' === $new ) {
00051 # Show diff between revision $old and the previous one.
00052 # Get previous one from DB.
00053 $this->mNewid = intval($old);
00054 $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid );
00055 } elseif ( 'next' === $new ) {
00056 # Show diff between revision $old and the next one.
00057 # Get next one from DB.
00058 $this->mOldid = intval($old);
00059 $this->mNewid = $this->mTitle->getNextRevisionID( $this->mOldid );
00060 if ( false === $this->mNewid ) {
00061 # if no result, NewId points to the newest old revision. The only newer
00062 # revision is cur, which is "0".
00063 $this->mNewid = 0;
00064 }
00065 } else {
00066 $this->mOldid = intval($old);
00067 $this->mNewid = intval($new);
00068 wfRunHooks( 'NewDifferenceEngine', array(&$titleObj, &$this->mOldid, &$this->mNewid, $old, $new) );
00069 }
00070 $this->mRcidMarkPatrolled = intval($rcid); # force it to be an integer
00071 $this->mRefreshCache = $refreshCache;
00072 $this->htmldiff = $htmldiff;
00073 $this->unhide = $unhide;
00074 }
00075
00076 function getTitle() {
00077 return $this->mTitle;
00078 }
00079
00080 function wasCacheHit() {
00081 return $this->mCacheHit;
00082 }
00083
00084 function getOldid() {
00085 return $this->mOldid;
00086 }
00087
00088 function getNewid() {
00089 return $this->mNewid;
00090 }
00091
00092 function showDiffPage( $diffOnly = false ) {
00093 global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol, $wgEnableHtmlDiff;
00094 wfProfileIn( __METHOD__ );
00095
00096
00097 # If external diffs are enabled both globally and for the user,
00098 # we'll use the application/x-external-editor interface to call
00099 # an external diff tool like kompare, kdiff3, etc.
00100 if($wgUseExternalEditor && $wgUser->getOption('externaldiff')) {
00101 global $wgInputEncoding,$wgServer,$wgScript,$wgLang;
00102 $wgOut->disable();
00103 header ( "Content-type: application/x-external-editor; charset=".$wgInputEncoding );
00104 $url1=$this->mTitle->getFullURL("action=raw&oldid=".$this->mOldid);
00105 $url2=$this->mTitle->getFullURL("action=raw&oldid=".$this->mNewid);
00106 $special=$wgLang->getNsText(NS_SPECIAL);
00107 $control=<<<CONTROL
00108 [Process]
00109 Type=Diff text
00110 Engine=MediaWiki
00111 Script={$wgServer}{$wgScript}
00112 Special namespace={$special}
00113
00114 [File]
00115 Extension=wiki
00116 URL=$url1
00117
00118 [File 2]
00119 Extension=wiki
00120 URL=$url2
00121 CONTROL;
00122 echo($control);
00123 return;
00124 }
00125
00126 $wgOut->setArticleFlag( false );
00127 if ( !$this->loadRevisionData() ) {
00128 $t = $this->mTitle->getPrefixedText();
00129 $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
00130 $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
00131 $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
00132 wfProfileOut( __METHOD__ );
00133 return;
00134 }
00135
00136 wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
00137
00138 if ( $this->mNewRev->isCurrent() ) {
00139 $wgOut->setArticleFlag( true );
00140 }
00141
00142 # mOldid is false if the difference engine is called with a "vague" query for
00143 # a diff between a version V and its previous version V' AND the version V
00144 # is the first version of that article. In that case, V' does not exist.
00145 if ( $this->mOldid === false ) {
00146 $this->showFirstRevision();
00147 $this->renderNewRevision();
00148 wfProfileOut( __METHOD__ );
00149 return;
00150 }
00151
00152 $wgOut->suppressQuickbar();
00153
00154 $oldTitle = $this->mOldPage->getPrefixedText();
00155 $newTitle = $this->mNewPage->getPrefixedText();
00156 if( $oldTitle == $newTitle ) {
00157 $wgOut->setPageTitle( $newTitle );
00158 } else {
00159 $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle );
00160 }
00161 $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
00162 $wgOut->setRobotPolicy( 'noindex,nofollow' );
00163
00164 if ( !$this->mOldPage->userCanRead() || !$this->mNewPage->userCanRead() ) {
00165 $wgOut->loginToUse();
00166 $wgOut->output();
00167 $wgOut->disable();
00168 wfProfileOut( __METHOD__ );
00169 return;
00170 }
00171
00172 $sk = $wgUser->getSkin();
00173
00174
00175 $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
00176 if ( $editable && $this->mNewRev->isCurrent() && $wgUser->isAllowed( 'rollback' ) ) {
00177 $rollback = ' ' . $sk->generateRollback( $this->mNewRev );
00178 } else {
00179 $rollback = '';
00180 }
00181
00182
00183 if( $wgUseRCPatrol && $this->mTitle->userCan('patrol') ) {
00184
00185 if( $this->mRcidMarkPatrolled ) {
00186 $rcid = $this->mRcidMarkPatrolled;
00187 $rc = RecentChange::newFromId( $rcid );
00188
00189 $rcid = is_object($rc) && !$rc->getAttribute('rc_patrolled') ? $rcid : 0;
00190 } else {
00191
00192 $db = wfGetDB( DB_SLAVE );
00193 $change = RecentChange::newFromConds(
00194 array(
00195
00196 'rc_user_text' => $this->mNewRev->getRawUserText(),
00197 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
00198 'rc_this_oldid' => $this->mNewid,
00199 'rc_last_oldid' => $this->mOldid,
00200 'rc_patrolled' => 0
00201 ),
00202 __METHOD__
00203 );
00204 if( $change instanceof RecentChange ) {
00205 $rcid = $change->mAttribs['rc_id'];
00206 $this->mRcidMarkPatrolled = $rcid;
00207 } else {
00208
00209 $rcid = 0;
00210 }
00211 }
00212
00213 if( $rcid ) {
00214 $patrol = ' <span class="patrollink">[' . $sk->makeKnownLinkObj( $this->mTitle,
00215 wfMsgHtml( 'markaspatrolleddiff' ), "action=markpatrolled&rcid={$rcid}" ) . ']</span>';
00216 } else {
00217 $patrol = '';
00218 }
00219 } else {
00220 $patrol = '';
00221 }
00222
00223 $diffOnlyArg = '';
00224 # Carry over 'diffonly' param via navigation links
00225 if( $diffOnly != $wgUser->getBoolOption('diffonly') ) {
00226 $diffOnlyArg = '&diffonly='.$diffOnly;
00227 }
00228 $htmldiffarg = $this->htmlDiffArgument();
00229 # Make "previous revision link"
00230 $prevlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousdiff' ),
00231 "diff=prev&oldid={$this->mOldid}{$htmldiffarg}{$diffOnlyArg}", '', '', 'id="differences-prevlink"' );
00232 # Make "next revision link"
00233 if( $this->mNewRev->isCurrent() ) {
00234 $nextlink = ' ';
00235 } else {
00236 $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ),
00237 "diff=next&oldid={$this->mNewid}{$htmldiffarg}{$diffOnlyArg}", '', '', 'id="differences-nextlink"' );
00238 }
00239
00240 $oldminor = '';
00241 $newminor = '';
00242
00243 if( $this->mOldRev->isMinor() ) {
00244 $oldminor = Xml::span( wfMsg( 'minoreditletter' ), 'minor' ) . ' ';
00245 }
00246 if( $this->mNewRev->isMinor() ) {
00247 $newminor = Xml::span( wfMsg( 'minoreditletter' ), 'minor' ) . ' ';
00248 }
00249
00250 $rdel = ''; $ldel = '';
00251 if( $wgUser->isAllowed( 'deleterevision' ) ) {
00252 if( !$this->mOldRev->userCan( Revision::DELETED_RESTRICTED ) ) {
00253
00254 $ldel = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.wfMsgHtml( 'rev-delundel' ).')' );
00255 } else {
00256 $query = array( 'target' => $this->mOldRev->mTitle->getPrefixedDbkey(),
00257 'oldid' => $this->mOldRev->getId()
00258 );
00259 $ldel = $sk->revDeleteLink( $query, $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) );
00260 }
00261 $ldel = " $ldel ";
00262
00263 if( $this->mNewRev->isCurrent() ) {
00264 $rdel = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.wfMsgHtml( 'rev-delundel' ).')' );
00265 } else if( !$this->mNewRev->userCan( Revision::DELETED_RESTRICTED ) ) {
00266
00267 $rdel = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.wfMsgHtml( 'rev-delundel' ).')' );
00268 } else {
00269 $query = array( 'target' => $this->mNewRev->mTitle->getPrefixedDbkey(),
00270 'oldid' => $this->mNewRev->getId()
00271 );
00272 $rdel = $sk->revDeleteLink( $query, $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) );
00273 }
00274 $rdel = " $rdel ";
00275 }
00276
00277 $oldHeader = '<div id="mw-diff-otitle1"><strong>'.$this->mOldtitle.'</strong></div>' .
00278 '<div id="mw-diff-otitle2">' . $sk->revUserTools( $this->mOldRev, !$this->unhide ) . "</div>" .
00279 '<div id="mw-diff-otitle3">' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly, !$this->unhide ).$ldel."</div>" .
00280 '<div id="mw-diff-otitle4">' . $prevlink .'</div>';
00281 $newHeader = '<div id="mw-diff-ntitle1"><strong>'.$this->mNewtitle.'</strong></div>' .
00282 '<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev, !$this->unhide ) . " $rollback</div>" .
00283 '<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly, !$this->unhide ).$rdel."</div>" .
00284 '<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>';
00285
00286 # Check if this user can see the revisions
00287 $allowed = $this->mOldRev->userCan(Revision::DELETED_TEXT)
00288 && $this->mNewRev->userCan(Revision::DELETED_TEXT);
00289 $deleted = $this->mOldRev->isDeleted(Revision::DELETED_TEXT)
00290 || $this->mNewRev->isDeleted(Revision::DELETED_TEXT);
00291 # Output the diff if allowed...
00292 if( $deleted && (!$this->unhide || !$allowed) ) {
00293 $this->showDiffStyle();
00294 $multi = $this->getMultiNotice();
00295 $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
00296 if( !$allowed ) {
00297 # Give explanation for why revision is not visible
00298 $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n",
00299 array( 'rev-deleted-no-diff' ) );
00300 } else {
00301 # Give explanation and add a link to view the diff...
00302 $link = $this->mTitle->getFullUrl( "diff={$this->mNewid}&oldid={$this->mOldid}".
00303 '&unhide=1&token='.urlencode( $wgUser->editToken($this->mNewid) ) );
00304 $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n",
00305 array( 'rev-deleted-unhide-diff', $link ) );
00306 }
00307 } else if( $wgEnableHtmlDiff && $this->htmldiff ) {
00308 $multi = $this->getMultiNotice();
00309 $wgOut->addHTML('<div class="diff-switchtype">'.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'wikicodecomparison' ),
00310 'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=0', '', '', 'id="differences-switchtype"' ).'</div>');
00311 $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
00312 $this->renderHtmlDiff();
00313 } else {
00314 if( $wgEnableHtmlDiff ) {
00315 $wgOut->addHTML('<div class="diff-switchtype">'.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'visualcomparison' ),
00316 'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=1', '', '', 'id="differences-switchtype"' ).'</div>');
00317 }
00318 $this->showDiff( $oldHeader, $newHeader );
00319 if( !$diffOnly ) {
00320 $this->renderNewRevision();
00321 }
00322 }
00323 wfProfileOut( __METHOD__ );
00324 }
00325
00329 function renderNewRevision() {
00330 global $wgOut, $wgUser;
00331 wfProfileIn( __METHOD__ );
00332
00333 $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
00334 # Add deleted rev tag if needed
00335 if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
00336 $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-permission' );
00337 } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
00338 $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-view' );
00339 }
00340
00341 if( !$this->mNewRev->isCurrent() ) {
00342 $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
00343 }
00344
00345 $this->loadNewText();
00346 if( is_object( $this->mNewRev ) ) {
00347 $wgOut->setRevisionId( $this->mNewRev->getId() );
00348 }
00349
00350 if( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
00351
00352
00353 if( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mTitle, $wgOut ) ) ) {
00354
00355 $m = array();
00356 preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
00357 $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
00358 $wgOut->addHTML( htmlspecialchars( $this->mNewtext ) );
00359 $wgOut->addHTML( "\n</pre>\n" );
00360 }
00361 } else {
00362 $wgOut->addWikiTextTidy( $this->mNewtext );
00363 }
00364
00365 if( is_object( $this->mNewRev ) && !$this->mNewRev->isCurrent() ) {
00366 $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
00367 }
00368 # Add redundant patrol link on bottom...
00369 if( $this->mRcidMarkPatrolled && $this->mTitle->quickUserCan('patrol') ) {
00370 $sk = $wgUser->getSkin();
00371 $wgOut->addHTML(
00372 "<div class='patrollink'>[" . $sk->makeKnownLinkObj( $this->mTitle,
00373 wfMsgHtml( 'markaspatrolleddiff' ), "action=markpatrolled&rcid={$this->mRcidMarkPatrolled}" ) .
00374 ']</div>'
00375 );
00376 }
00377
00378 wfProfileOut( __METHOD__ );
00379 }
00380
00381
00382 function renderHtmlDiff() {
00383 global $wgOut, $wgTitle, $wgParser, $wgDebugComments;
00384 wfProfileIn( __METHOD__ );
00385
00386 $this->showDiffStyle();
00387
00388 $wgOut->addHTML( '<h2>'.wfMsgHtml( 'visual-comparison' )."</h2>\n" );
00389 #add deleted rev tag if needed
00390 if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
00391 $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-permission' );
00392 } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
00393 $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-view' );
00394 }
00395
00396 if( !$this->mNewRev->isCurrent() ) {
00397 $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
00398 }
00399
00400 $this->loadText();
00401
00402
00403 if( is_object( $this->mOldRev ) ) {
00404 $wgOut->setRevisionId( $this->mOldRev->getId() );
00405 }
00406
00407 $popts = $wgOut->parserOptions();
00408 $oldTidy = $popts->setTidy( true );
00409 $popts->setEditSection( false );
00410
00411 $parserOutput = $wgParser->parse( $this->mOldtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() );
00412 $popts->setTidy( $oldTidy );
00413
00414
00415
00416 $oldHtml = $parserOutput->getText();
00417 wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$oldHtml ) );
00418
00419
00420 if( is_object( $this->mNewRev ) ) {
00421 $wgOut->setRevisionId( $this->mNewRev->getId() );
00422 }
00423
00424 $popts = $wgOut->parserOptions();
00425 $oldTidy = $popts->setTidy( true );
00426
00427 $parserOutput = $wgParser->parse( $this->mNewtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() );
00428 $popts->setTidy( $oldTidy );
00429
00430 $wgOut->addParserOutputNoText( $parserOutput );
00431 $newHtml = $parserOutput->getText();
00432 wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$newHtml ) );
00433
00434 unset($parserOutput, $popts);
00435
00436 $differ = new HTMLDiffer(new DelegatingContentHandler($wgOut));
00437 $differ->htmlDiff($oldHtml, $newHtml);
00438 if ( $wgDebugComments ) {
00439 $wgOut->addHTML( "\n<!-- HtmlDiff Debug Output:\n" . HTMLDiffer::getDebugOutput() . " End Debug -->" );
00440 }
00441
00442 wfProfileOut( __METHOD__ );
00443 }
00444
00449 function showFirstRevision() {
00450 global $wgOut, $wgUser;
00451 wfProfileIn( __METHOD__ );
00452
00453 # Get article text from the DB
00454 #
00455 if ( ! $this->loadNewText() ) {
00456 $t = $this->mTitle->getPrefixedText();
00457 $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
00458 $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
00459 $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
00460 wfProfileOut( __METHOD__ );
00461 return;
00462 }
00463 if ( $this->mNewRev->isCurrent() ) {
00464 $wgOut->setArticleFlag( true );
00465 }
00466
00467 # Check if user is allowed to look at this page. If not, bail out.
00468 #
00469 if ( !$this->mTitle->userCanRead() ) {
00470 $wgOut->loginToUse();
00471 $wgOut->output();
00472 wfProfileOut( __METHOD__ );
00473 throw new MWException("Permission Error: you do not have access to view this page");
00474 }
00475
00476 # Prepare the header box
00477 #
00478 $sk = $wgUser->getSkin();
00479
00480 $next = $this->mTitle->getNextRevisionID( $this->mNewid );
00481 if( !$next ) {
00482 $nextlink = '';
00483 } else {
00484 $nextlink = '<br/>' . $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ),
00485 'diff=next&oldid=' . $this->mNewid.$this->htmlDiffArgument(), '', '', 'id="differences-nextlink"' );
00486 }
00487 $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\">" .
00488 $sk->revUserTools( $this->mNewRev ) . "<br/>" . $sk->revComment( $this->mNewRev ) . $nextlink . "</div>\n";
00489
00490 $wgOut->addHTML( $header );
00491
00492 $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
00493 $wgOut->setRobotPolicy( 'noindex,nofollow' );
00494
00495 wfProfileOut( __METHOD__ );
00496 }
00497
00498 function htmlDiffArgument(){
00499 global $wgEnableHtmlDiff;
00500 if($wgEnableHtmlDiff){
00501 if($this->htmldiff){
00502 return '&htmldiff=1';
00503 }else{
00504 return '&htmldiff=0';
00505 }
00506 }else{
00507 return '';
00508 }
00509 }
00510
00515 function showDiff( $otitle, $ntitle ) {
00516 global $wgOut;
00517 $diff = $this->getDiff( $otitle, $ntitle );
00518 if ( $diff === false ) {
00519 $wgOut->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' );
00520 return false;
00521 } else {
00522 $this->showDiffStyle();
00523 $wgOut->addHTML( $diff );
00524 return true;
00525 }
00526 }
00527
00531 function showDiffStyle() {
00532 global $wgStylePath, $wgStyleVersion, $wgOut;
00533 $wgOut->addStyle( 'common/diff.css' );
00534
00535
00536 $wgOut->addScript( "<script type=\"text/javascript\" src=\"$wgStylePath/common/diff.js?$wgStyleVersion\"></script>" );
00537 }
00538
00546 function getDiff( $otitle, $ntitle ) {
00547 $body = $this->getDiffBody();
00548 if ( $body === false ) {
00549 return false;
00550 } else {
00551 $multi = $this->getMultiNotice();
00552 return $this->addHeader( $body, $otitle, $ntitle, $multi );
00553 }
00554 }
00555
00561 function getDiffBody() {
00562 global $wgMemc;
00563 wfProfileIn( __METHOD__ );
00564 $this->mCacheHit = true;
00565
00566 if ( !$this->loadRevisionData() )
00567 return '';
00568 if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
00569 return '';
00570 } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
00571 return '';
00572 } else if ( $this->mOldRev && $this->mNewRev && $this->mOldRev->getID() == $this->mNewRev->getID() ) {
00573 return '';
00574 }
00575
00576 $key = false;
00577 if ( $this->mOldid && $this->mNewid ) {
00578 $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid );
00579
00580 if ( !$this->mRefreshCache ) {
00581 $difftext = $wgMemc->get( $key );
00582 if ( $difftext ) {
00583 wfIncrStats( 'diff_cache_hit' );
00584 $difftext = $this->localiseLineNumbers( $difftext );
00585 $difftext .= "\n<!-- diff cache key $key -->\n";
00586 wfProfileOut( __METHOD__ );
00587 return $difftext;
00588 }
00589 }
00590 }
00591 $this->mCacheHit = false;
00592
00593
00594 if ( !$this->loadText() ) {
00595 wfProfileOut( __METHOD__ );
00596 return false;
00597 }
00598
00599 $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
00600
00601
00602 if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
00603 wfIncrStats( 'diff_uncacheable' );
00604 } else if ( $key !== false && $difftext !== false ) {
00605 wfIncrStats( 'diff_cache_miss' );
00606 $wgMemc->set( $key, $difftext, 7*86400 );
00607 } else {
00608 wfIncrStats( 'diff_uncacheable' );
00609 }
00610
00611 if ( $difftext !== false ) {
00612 $difftext = $this->localiseLineNumbers( $difftext );
00613 }
00614 wfProfileOut( __METHOD__ );
00615 return $difftext;
00616 }
00617
00622 function generateDiffBody( $otext, $ntext ) {
00623 global $wgExternalDiffEngine, $wgContLang;
00624
00625 $otext = str_replace( "\r\n", "\n", $otext );
00626 $ntext = str_replace( "\r\n", "\n", $ntext );
00627
00628 if ( $wgExternalDiffEngine == 'wikidiff' ) {
00629 # For historical reasons, external diff engine expects
00630 # input text to be HTML-escaped already
00631 $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) );
00632 $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) );
00633 if( !function_exists( 'wikidiff_do_diff' ) ) {
00634 dl('php_wikidiff.so');
00635 }
00636 return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) .
00637 $this->debug( 'wikidiff1' );
00638 }
00639
00640 if ( $wgExternalDiffEngine == 'wikidiff2' ) {
00641 # Better external diff engine, the 2 may some day be dropped
00642 # This one does the escaping and segmenting itself
00643 if ( !function_exists( 'wikidiff2_do_diff' ) ) {
00644 wfProfileIn( __METHOD__ . "-dl" );
00645 @dl('php_wikidiff2.so');
00646 wfProfileOut( __METHOD__ . "-dl" );
00647 }
00648 if ( function_exists( 'wikidiff2_do_diff' ) ) {
00649 wfProfileIn( 'wikidiff2_do_diff' );
00650 $text = wikidiff2_do_diff( $otext, $ntext, 2 );
00651 $text .= $this->debug( 'wikidiff2' );
00652 wfProfileOut( 'wikidiff2_do_diff' );
00653 return $text;
00654 }
00655 }
00656 if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) {
00657 # Diff via the shell
00658 global $wgTmpDirectory;
00659 $tempName1 = tempnam( $wgTmpDirectory, 'diff_' );
00660 $tempName2 = tempnam( $wgTmpDirectory, 'diff_' );
00661
00662 $tempFile1 = fopen( $tempName1, "w" );
00663 if ( !$tempFile1 ) {
00664 wfProfileOut( __METHOD__ );
00665 return false;
00666 }
00667 $tempFile2 = fopen( $tempName2, "w" );
00668 if ( !$tempFile2 ) {
00669 wfProfileOut( __METHOD__ );
00670 return false;
00671 }
00672 fwrite( $tempFile1, $otext );
00673 fwrite( $tempFile2, $ntext );
00674 fclose( $tempFile1 );
00675 fclose( $tempFile2 );
00676 $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
00677 wfProfileIn( __METHOD__ . "-shellexec" );
00678 $difftext = wfShellExec( $cmd );
00679 $difftext .= $this->debug( "external $wgExternalDiffEngine" );
00680 wfProfileOut( __METHOD__ . "-shellexec" );
00681 unlink( $tempName1 );
00682 unlink( $tempName2 );
00683 return $difftext;
00684 }
00685
00686 # Native PHP diff
00687 $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
00688 $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
00689 $diffs = new Diff( $ota, $nta );
00690 $formatter = new TableDiffFormatter();
00691 return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) .
00692 $this->debug();
00693 }
00694
00699 protected function debug( $generator="internal" ) {
00700 global $wgShowHostnames;
00701 $data = array( $generator );
00702 if( $wgShowHostnames ) {
00703 $data[] = wfHostname();
00704 }
00705 $data[] = wfTimestamp( TS_DB );
00706 return "<!-- diff generator: " .
00707 implode( " ",
00708 array_map(
00709 "htmlspecialchars",
00710 $data ) ) .
00711 " -->\n";
00712 }
00713
00717 function localiseLineNumbers( $text ) {
00718 return preg_replace_callback( '/<!--LINE (\d+)-->/',
00719 array( &$this, 'localiseLineNumbersCb' ), $text );
00720 }
00721
00722 function localiseLineNumbersCb( $matches ) {
00723 global $wgLang;
00724 return wfMsgExt( 'lineno', array (), $wgLang->formatNum( $matches[1] ) );
00725 }
00726
00727
00731 function getMultiNotice() {
00732 if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) )
00733 return '';
00734
00735 if( !$this->mOldPage->equals( $this->mNewPage ) ) {
00736
00737 return '';
00738 }
00739
00740 $oldid = $this->mOldRev->getId();
00741 $newid = $this->mNewRev->getId();
00742 if ( $oldid > $newid ) {
00743 $tmp = $oldid; $oldid = $newid; $newid = $tmp;
00744 }
00745
00746 $n = $this->mTitle->countRevisionsBetween( $oldid, $newid );
00747 if ( !$n )
00748 return '';
00749
00750 return wfMsgExt( 'diff-multi', array( 'parseinline' ), $n );
00751 }
00752
00753
00757 static function addHeader( $diff, $otitle, $ntitle, $multi = '' ) {
00758 $header = "
00759 <table class='diff'>
00760 <col class='diff-marker' />
00761 <col class='diff-content' />
00762 <col class='diff-marker' />
00763 <col class='diff-content' />
00764 <tr valign='top'>
00765 <td colspan='2' class='diff-otitle'>{$otitle}</td>
00766 <td colspan='2' class='diff-ntitle'>{$ntitle}</td>
00767 </tr>
00768 ";
00769
00770 if ( $multi != '' )
00771 $header .= "<tr><td colspan='4' align='center' class='diff-multi'>{$multi}</td></tr>";
00772
00773 return $header . $diff . "</table>";
00774 }
00775
00779 function setText( $oldText, $newText ) {
00780 $this->mOldtext = $oldText;
00781 $this->mNewtext = $newText;
00782 $this->mTextLoaded = 2;
00783 $this->mRevisionsLoaded = true;
00784 }
00785
00796 function loadRevisionData() {
00797 global $wgLang, $wgUser;
00798 if ( $this->mRevisionsLoaded ) {
00799 return true;
00800 } else {
00801
00802 $this->mRevisionsLoaded = true;
00803 }
00804
00805
00806 $this->mNewRev = $this->mNewid
00807 ? Revision::newFromId( $this->mNewid )
00808 : Revision::newFromTitle( $this->mTitle );
00809 if( !$this->mNewRev instanceof Revision )
00810 return false;
00811
00812
00813 $this->mNewid = $this->mNewRev->getId();
00814
00815
00816 $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
00817
00818
00819 $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true );
00820 $this->mNewPage = $this->mNewRev->getTitle();
00821 if( $this->mNewRev->isCurrent() ) {
00822 $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid );
00823 $this->mPagetitle = wfMsgHTML( 'currentrev-asof', $timestamp );
00824 $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit' );
00825
00826 $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
00827 $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
00828
00829 } else {
00830 $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid );
00831 $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mNewid );
00832 $this->mPagetitle = wfMsgHTML( 'revisionasof', $timestamp );
00833
00834 $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
00835 $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
00836 }
00837 if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
00838 $this->mNewtitle = "<span class='history-deleted'>{$this->mPagetitle}</span>";
00839 } else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
00840 $this->mNewtitle = '<span class="history-deleted">'.$this->mNewtitle.'</span>';
00841 }
00842
00843
00844 $this->mOldRev = false;
00845 if( $this->mOldid ) {
00846 $this->mOldRev = Revision::newFromId( $this->mOldid );
00847 } elseif ( $this->mOldid === 0 ) {
00848 $rev = $this->mNewRev->getPrevious();
00849 if( $rev ) {
00850 $this->mOldid = $rev->getId();
00851 $this->mOldRev = $rev;
00852 } else {
00853
00854 $this->mOldid = false;
00855 $this->mOldRev = false;
00856 }
00857 }
00858
00859 if( is_null( $this->mOldRev ) ) {
00860 return false;
00861 }
00862
00863 if ( $this->mOldRev ) {
00864 $this->mOldPage = $this->mOldRev->getTitle();
00865
00866 $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true );
00867 $oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid );
00868 $oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid );
00869 $this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t ) );
00870
00871 $this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>"
00872 . " (<a href='$oldEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
00873
00874 $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid);
00875 $htmlLink = htmlspecialchars( wfMsg( 'editundo' ) );
00876 $htmlTitle = $wgUser->getSkin()->tooltip( 'undo' );
00877 if( $editable && !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
00878 $this->mNewtitle .= " (<a href='$newUndo' $htmlTitle>" . $htmlLink . "</a>)";
00879 }
00880
00881 if( !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) {
00882 $this->mOldtitle = '<span class="history-deleted">' . $this->mOldPagetitle . '</span>';
00883 } else if( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
00884 $this->mOldtitle = '<span class="history-deleted">' . $this->mOldtitle . '</span>';
00885 }
00886 }
00887
00888 return true;
00889 }
00890
00894 function loadText() {
00895 if ( $this->mTextLoaded == 2 ) {
00896 return true;
00897 } else {
00898
00899 $this->mTextLoaded = 2;
00900 }
00901
00902 if ( !$this->loadRevisionData() ) {
00903 return false;
00904 }
00905 if ( $this->mOldRev ) {
00906 $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER );
00907 if ( $this->mOldtext === false ) {
00908 return false;
00909 }
00910 }
00911 if ( $this->mNewRev ) {
00912 $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
00913 if ( $this->mNewtext === false ) {
00914 return false;
00915 }
00916 }
00917 return true;
00918 }
00919
00923 function loadNewText() {
00924 if ( $this->mTextLoaded >= 1 ) {
00925 return true;
00926 } else {
00927 $this->mTextLoaded = 1;
00928 }
00929 if ( !$this->loadRevisionData() ) {
00930 return false;
00931 }
00932 $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
00933 return true;
00934 }
00935
00936
00937 }
00938
00939
00940
00941
00942
00943
00944
00945 define('USE_ASSERTS', function_exists('assert'));
00946
00952 class _DiffOp {
00953 var $type;
00954 var $orig;
00955 var $closing;
00956
00957 function reverse() {
00958 trigger_error('pure virtual', E_USER_ERROR);
00959 }
00960
00961 function norig() {
00962 return $this->orig ? sizeof($this->orig) : 0;
00963 }
00964
00965 function nclosing() {
00966 return $this->closing ? sizeof($this->closing) : 0;
00967 }
00968 }
00969
00975 class _DiffOp_Copy extends _DiffOp {
00976 var $type = 'copy';
00977
00978 function _DiffOp_Copy ($orig, $closing = false) {
00979 if (!is_array($closing))
00980 $closing = $orig;
00981 $this->orig = $orig;
00982 $this->closing = $closing;
00983 }
00984
00985 function reverse() {
00986 return new _DiffOp_Copy($this->closing, $this->orig);
00987 }
00988 }
00989
00995 class _DiffOp_Delete extends _DiffOp {
00996 var $type = 'delete';
00997
00998 function _DiffOp_Delete ($lines) {
00999 $this->orig = $lines;
01000 $this->closing = false;
01001 }
01002
01003 function reverse() {
01004 return new _DiffOp_Add($this->orig);
01005 }
01006 }
01007
01013 class _DiffOp_Add extends _DiffOp {
01014 var $type = 'add';
01015
01016 function _DiffOp_Add ($lines) {
01017 $this->closing = $lines;
01018 $this->orig = false;
01019 }
01020
01021 function reverse() {
01022 return new _DiffOp_Delete($this->closing);
01023 }
01024 }
01025
01031 class _DiffOp_Change extends _DiffOp {
01032 var $type = 'change';
01033
01034 function _DiffOp_Change ($orig, $closing) {
01035 $this->orig = $orig;
01036 $this->closing = $closing;
01037 }
01038
01039 function reverse() {
01040 return new _DiffOp_Change($this->closing, $this->orig);
01041 }
01042 }
01043
01068 class _DiffEngine {
01069
01070 const MAX_XREF_LENGTH = 10000;
01071
01072 function diff ($from_lines, $to_lines){
01073 wfProfileIn( __METHOD__ );
01074
01075
01076 $this->diff_local($from_lines, $to_lines);
01077
01078
01079 $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
01080 $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
01081
01082
01083 $n_from = sizeof($from_lines);
01084 $n_to = sizeof($to_lines);
01085
01086 $edits = array();
01087 $xi = $yi = 0;
01088 while ($xi < $n_from || $yi < $n_to) {
01089 USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
01090 USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
01091
01092
01093 $copy = array();
01094 while ( $xi < $n_from && $yi < $n_to
01095 && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
01096 $copy[] = $from_lines[$xi++];
01097 ++$yi;
01098 }
01099 if ($copy)
01100 $edits[] = new _DiffOp_Copy($copy);
01101
01102
01103 $delete = array();
01104 while ($xi < $n_from && $this->xchanged[$xi])
01105 $delete[] = $from_lines[$xi++];
01106
01107 $add = array();
01108 while ($yi < $n_to && $this->ychanged[$yi])
01109 $add[] = $to_lines[$yi++];
01110
01111 if ($delete && $add)
01112 $edits[] = new _DiffOp_Change($delete, $add);
01113 elseif ($delete)
01114 $edits[] = new _DiffOp_Delete($delete);
01115 elseif ($add)
01116 $edits[] = new _DiffOp_Add($add);
01117 }
01118 wfProfileOut( __METHOD__ );
01119 return $edits;
01120 }
01121
01122 function diff_local ($from_lines, $to_lines) {
01123 global $wgExternalDiffEngine;
01124 wfProfileIn( __METHOD__);
01125
01126 if($wgExternalDiffEngine == 'wikidiff3'){
01127
01128 $wikidiff3 = new WikiDiff3();
01129 $wikidiff3->diff($from_lines, $to_lines);
01130 $this->xchanged = $wikidiff3->removed;
01131 $this->ychanged = $wikidiff3->added;
01132 unset($wikidiff3);
01133 }else{
01134
01135 $n_from = sizeof($from_lines);
01136 $n_to = sizeof($to_lines);
01137 $this->xchanged = $this->ychanged = array();
01138 $this->xv = $this->yv = array();
01139 $this->xind = $this->yind = array();
01140 unset($this->seq);
01141 unset($this->in_seq);
01142 unset($this->lcs);
01143
01144
01145 for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
01146 if ($from_lines[$skip] !== $to_lines[$skip])
01147 break;
01148 $this->xchanged[$skip] = $this->ychanged[$skip] = false;
01149 }
01150
01151 $xi = $n_from; $yi = $n_to;
01152 for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
01153 if ($from_lines[$xi] !== $to_lines[$yi])
01154 break;
01155 $this->xchanged[$xi] = $this->ychanged[$yi] = false;
01156 }
01157
01158
01159 for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
01160 $xhash[$this->_line_hash($from_lines[$xi])] = 1;
01161 }
01162
01163 for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
01164 $line = $to_lines[$yi];
01165 if ( ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) )
01166 continue;
01167 $yhash[$this->_line_hash($line)] = 1;
01168 $this->yv[] = $line;
01169 $this->yind[] = $yi;
01170 }
01171 for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
01172 $line = $from_lines[$xi];
01173 if ( ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) )
01174 continue;
01175 $this->xv[] = $line;
01176 $this->xind[] = $xi;
01177 }
01178
01179
01180 $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
01181 }
01182 wfProfileOut( __METHOD__ );
01183 }
01184
01188 function _line_hash( $line ) {
01189 if ( strlen( $line ) > self::MAX_XREF_LENGTH ) {
01190 return md5( $line );
01191 } else {
01192 return $line;
01193 }
01194 }
01195
01196
01197
01198
01199
01200
01201
01202
01203
01204
01205
01206
01207
01208
01209
01210
01211
01212 function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) {
01213 $flip = false;
01214
01215 if ($xlim - $xoff > $ylim - $yoff) {
01216
01217
01218 $flip = true;
01219 list ($xoff, $xlim, $yoff, $ylim)
01220 = array( $yoff, $ylim, $xoff, $xlim);
01221 }
01222
01223 if ($flip)
01224 for ($i = $ylim - 1; $i >= $yoff; $i--)
01225 $ymatches[$this->xv[$i]][] = $i;
01226 else
01227 for ($i = $ylim - 1; $i >= $yoff; $i--)
01228 $ymatches[$this->yv[$i]][] = $i;
01229
01230 $this->lcs = 0;
01231 $this->seq[0]= $yoff - 1;
01232 $this->in_seq = array();
01233 $ymids[0] = array();
01234
01235 $numer = $xlim - $xoff + $nchunks - 1;
01236 $x = $xoff;
01237 for ($chunk = 0; $chunk < $nchunks; $chunk++) {
01238 if ($chunk > 0)
01239 for ($i = 0; $i <= $this->lcs; $i++)
01240 $ymids[$i][$chunk-1] = $this->seq[$i];
01241
01242 $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
01243 for ( ; $x < $x1; $x++) {
01244 $line = $flip ? $this->yv[$x] : $this->xv[$x];
01245 if (empty($ymatches[$line]))
01246 continue;
01247 $matches = $ymatches[$line];
01248 reset($matches);
01249 while (list ($junk, $y) = each($matches))
01250 if (empty($this->in_seq[$y])) {
01251 $k = $this->_lcs_pos($y);
01252 USE_ASSERTS && assert($k > 0);
01253 $ymids[$k] = $ymids[$k-1];
01254 break;
01255 }
01256 while (list ( , $y) = each($matches)) {
01257 if ($y > $this->seq[$k-1]) {
01258 USE_ASSERTS && assert($y < $this->seq[$k]);
01259
01260
01261 $this->in_seq[$this->seq[$k]] = false;
01262 $this->seq[$k] = $y;
01263 $this->in_seq[$y] = 1;
01264 } else if (empty($this->in_seq[$y])) {
01265 $k = $this->_lcs_pos($y);
01266 USE_ASSERTS && assert($k > 0);
01267 $ymids[$k] = $ymids[$k-1];
01268 }
01269 }
01270 }
01271 }
01272
01273 $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
01274 $ymid = $ymids[$this->lcs];
01275 for ($n = 0; $n < $nchunks - 1; $n++) {
01276 $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
01277 $y1 = $ymid[$n] + 1;
01278 $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
01279 }
01280 $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
01281
01282 return array($this->lcs, $seps);
01283 }
01284
01285 function _lcs_pos ($ypos) {
01286 $end = $this->lcs;
01287 if ($end == 0 || $ypos > $this->seq[$end]) {
01288 $this->seq[++$this->lcs] = $ypos;
01289 $this->in_seq[$ypos] = 1;
01290 return $this->lcs;
01291 }
01292
01293 $beg = 1;
01294 while ($beg < $end) {
01295 $mid = (int)(($beg + $end) / 2);
01296 if ( $ypos > $this->seq[$mid] )
01297 $beg = $mid + 1;
01298 else
01299 $end = $mid;
01300 }
01301
01302 USE_ASSERTS && assert($ypos != $this->seq[$end]);
01303
01304 $this->in_seq[$this->seq[$end]] = false;
01305 $this->seq[$end] = $ypos;
01306 $this->in_seq[$ypos] = 1;
01307 return $end;
01308 }
01309
01310
01311
01312
01313
01314
01315
01316
01317
01318
01319
01320
01321 function _compareseq ($xoff, $xlim, $yoff, $ylim) {
01322
01323 while ($xoff < $xlim && $yoff < $ylim
01324 && $this->xv[$xoff] == $this->yv[$yoff]) {
01325 ++$xoff;
01326 ++$yoff;
01327 }
01328
01329
01330 while ($xlim > $xoff && $ylim > $yoff
01331 && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
01332 --$xlim;
01333 --$ylim;
01334 }
01335
01336 if ($xoff == $xlim || $yoff == $ylim)
01337 $lcs = 0;
01338 else {
01339
01340
01341
01342 $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
01343 list ($lcs, $seps)
01344 = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks);
01345 }
01346
01347 if ($lcs == 0) {
01348
01349
01350 while ($yoff < $ylim)
01351 $this->ychanged[$this->yind[$yoff++]] = 1;
01352 while ($xoff < $xlim)
01353 $this->xchanged[$this->xind[$xoff++]] = 1;
01354 } else {
01355
01356 reset($seps);
01357 $pt1 = $seps[0];
01358 while ($pt2 = next($seps)) {
01359 $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
01360 $pt1 = $pt2;
01361 }
01362 }
01363 }
01364
01365
01366
01367
01368
01369
01370
01371
01372
01373
01374
01375
01376
01377 function _shift_boundaries ($lines, &$changed, $other_changed) {
01378 wfProfileIn( __METHOD__ );
01379 $i = 0;
01380 $j = 0;
01381
01382 USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
01383 $len = sizeof($lines);
01384 $other_len = sizeof($other_changed);
01385
01386 while (1) {
01387
01388
01389
01390
01391
01392
01393
01394
01395
01396
01397
01398 while ($j < $other_len && $other_changed[$j])
01399 $j++;
01400
01401 while ($i < $len && ! $changed[$i]) {
01402 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
01403 $i++; $j++;
01404 while ($j < $other_len && $other_changed[$j])
01405 $j++;
01406 }
01407
01408 if ($i == $len)
01409 break;
01410
01411 $start = $i;
01412
01413
01414 while (++$i < $len && $changed[$i])
01415 continue;
01416
01417 do {
01418
01419
01420
01421
01422 $runlength = $i - $start;
01423
01424
01425
01426
01427
01428
01429 while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
01430 $changed[--$start] = 1;
01431 $changed[--$i] = false;
01432 while ($start > 0 && $changed[$start - 1])
01433 $start--;
01434 USE_ASSERTS && assert('$j > 0');
01435 while ($other_changed[--$j])
01436 continue;
01437 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
01438 }
01439
01440
01441
01442
01443
01444
01445 $corresponding = $j < $other_len ? $i : $len;
01446
01447
01448
01449
01450
01451
01452
01453
01454 while ($i < $len && $lines[$start] == $lines[$i]) {
01455 $changed[$start++] = false;
01456 $changed[$i++] = 1;
01457 while ($i < $len && $changed[$i])
01458 $i++;
01459
01460 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
01461 $j++;
01462 if ($j < $other_len && $other_changed[$j]) {
01463 $corresponding = $i;
01464 while ($j < $other_len && $other_changed[$j])
01465 $j++;
01466 }
01467 }
01468 } while ($runlength != $i - $start);
01469
01470
01471
01472
01473
01474 while ($corresponding < $i) {
01475 $changed[--$start] = 1;
01476 $changed[--$i] = 0;
01477 USE_ASSERTS && assert('$j > 0');
01478 while ($other_changed[--$j])
01479 continue;
01480 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
01481 }
01482 }
01483 wfProfileOut( __METHOD__ );
01484 }
01485 }
01486
01493 class Diff
01494 {
01495 var $edits;
01496
01505 function Diff($from_lines, $to_lines) {
01506 $eng = new _DiffEngine;
01507 $this->edits = $eng->diff($from_lines, $to_lines);
01508
01509 }
01510
01521 function reverse () {
01522 $rev = $this;
01523 $rev->edits = array();
01524 foreach ($this->edits as $edit) {
01525 $rev->edits[] = $edit->reverse();
01526 }
01527 return $rev;
01528 }
01529
01535 function isEmpty () {
01536 foreach ($this->edits as $edit) {
01537 if ($edit->type != 'copy')
01538 return false;
01539 }
01540 return true;
01541 }
01542
01550 function lcs () {
01551 $lcs = 0;
01552 foreach ($this->edits as $edit) {
01553 if ($edit->type == 'copy')
01554 $lcs += sizeof($edit->orig);
01555 }
01556 return $lcs;
01557 }
01558
01567 function orig() {
01568 $lines = array();
01569
01570 foreach ($this->edits as $edit) {
01571 if ($edit->orig)
01572 array_splice($lines, sizeof($lines), 0, $edit->orig);
01573 }
01574 return $lines;
01575 }
01576
01585 function closing() {
01586 $lines = array();
01587
01588 foreach ($this->edits as $edit) {
01589 if ($edit->closing)
01590 array_splice($lines, sizeof($lines), 0, $edit->closing);
01591 }
01592 return $lines;
01593 }
01594
01600 function _check ($from_lines, $to_lines) {
01601 wfProfileIn( __METHOD__ );
01602 if (serialize($from_lines) != serialize($this->orig()))
01603 trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
01604 if (serialize($to_lines) != serialize($this->closing()))
01605 trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
01606
01607 $rev = $this->reverse();
01608 if (serialize($to_lines) != serialize($rev->orig()))
01609 trigger_error("Reversed original doesn't match", E_USER_ERROR);
01610 if (serialize($from_lines) != serialize($rev->closing()))
01611 trigger_error("Reversed closing doesn't match", E_USER_ERROR);
01612
01613
01614 $prevtype = 'none';
01615 foreach ($this->edits as $edit) {
01616 if ( $prevtype == $edit->type )
01617 trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
01618 $prevtype = $edit->type;
01619 }
01620
01621 $lcs = $this->lcs();
01622 trigger_error('Diff okay: LCS = '.$lcs, E_USER_NOTICE);
01623 wfProfileOut( __METHOD__ );
01624 }
01625 }
01626
01632 class MappedDiff extends Diff
01633 {
01657 function MappedDiff($from_lines, $to_lines,
01658 $mapped_from_lines, $mapped_to_lines) {
01659 wfProfileIn( __METHOD__ );
01660
01661 assert(sizeof($from_lines) == sizeof($mapped_from_lines));
01662 assert(sizeof($to_lines) == sizeof($mapped_to_lines));
01663
01664 $this->Diff($mapped_from_lines, $mapped_to_lines);
01665
01666 $xi = $yi = 0;
01667 for ($i = 0; $i < sizeof($this->edits); $i++) {
01668 $orig = &$this->edits[$i]->orig;
01669 if (is_array($orig)) {
01670 $orig = array_slice($from_lines, $xi, sizeof($orig));
01671 $xi += sizeof($orig);
01672 }
01673
01674 $closing = &$this->edits[$i]->closing;
01675 if (is_array($closing)) {
01676 $closing = array_slice($to_lines, $yi, sizeof($closing));
01677 $yi += sizeof($closing);
01678 }
01679 }
01680 wfProfileOut( __METHOD__ );
01681 }
01682 }
01683
01694 class DiffFormatter {
01701 var $leading_context_lines = 0;
01702
01709 var $trailing_context_lines = 0;
01710
01717 function format($diff) {
01718 wfProfileIn( __METHOD__ );
01719
01720 $xi = $yi = 1;
01721 $block = false;
01722 $context = array();
01723
01724 $nlead = $this->leading_context_lines;
01725 $ntrail = $this->trailing_context_lines;
01726
01727 $this->_start_diff();
01728
01729 foreach ($diff->edits as $edit) {
01730 if ($edit->type == 'copy') {
01731 if (is_array($block)) {
01732 if (sizeof($edit->orig) <= $nlead + $ntrail) {
01733 $block[] = $edit;
01734 }
01735 else{
01736 if ($ntrail) {
01737 $context = array_slice($edit->orig, 0, $ntrail);
01738 $block[] = new _DiffOp_Copy($context);
01739 }
01740 $this->_block($x0, $ntrail + $xi - $x0,
01741 $y0, $ntrail + $yi - $y0,
01742 $block);
01743 $block = false;
01744 }
01745 }
01746 $context = $edit->orig;
01747 }
01748 else {
01749 if (! is_array($block)) {
01750 $context = array_slice($context, sizeof($context) - $nlead);
01751 $x0 = $xi - sizeof($context);
01752 $y0 = $yi - sizeof($context);
01753 $block = array();
01754 if ($context)
01755 $block[] = new _DiffOp_Copy($context);
01756 }
01757 $block[] = $edit;
01758 }
01759
01760 if ($edit->orig)
01761 $xi += sizeof($edit->orig);
01762 if ($edit->closing)
01763 $yi += sizeof($edit->closing);
01764 }
01765
01766 if (is_array($block))
01767 $this->_block($x0, $xi - $x0,
01768 $y0, $yi - $y0,
01769 $block);
01770
01771 $end = $this->_end_diff();
01772 wfProfileOut( __METHOD__ );
01773 return $end;
01774 }
01775
01776 function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
01777 wfProfileIn( __METHOD__ );
01778 $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
01779 foreach ($edits as $edit) {
01780 if ($edit->type == 'copy')
01781 $this->_context($edit->orig);
01782 elseif ($edit->type == 'add')
01783 $this->_added($edit->closing);
01784 elseif ($edit->type == 'delete')
01785 $this->_deleted($edit->orig);
01786 elseif ($edit->type == 'change')
01787 $this->_changed($edit->orig, $edit->closing);
01788 else
01789 trigger_error('Unknown edit type', E_USER_ERROR);
01790 }
01791 $this->_end_block();
01792 wfProfileOut( __METHOD__ );
01793 }
01794
01795 function _start_diff() {
01796 ob_start();
01797 }
01798
01799 function _end_diff() {
01800 $val = ob_get_contents();
01801 ob_end_clean();
01802 return $val;
01803 }
01804
01805 function _block_header($xbeg, $xlen, $ybeg, $ylen) {
01806 if ($xlen > 1)
01807 $xbeg .= "," . ($xbeg + $xlen - 1);
01808 if ($ylen > 1)
01809 $ybeg .= "," . ($ybeg + $ylen - 1);
01810
01811 return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
01812 }
01813
01814 function _start_block($header) {
01815 echo $header . "\n";
01816 }
01817
01818 function _end_block() {
01819 }
01820
01821 function _lines($lines, $prefix = ' ') {
01822 foreach ($lines as $line)
01823 echo "$prefix $line\n";
01824 }
01825
01826 function _context($lines) {
01827 $this->_lines($lines);
01828 }
01829
01830 function _added($lines) {
01831 $this->_lines($lines, '>');
01832 }
01833 function _deleted($lines) {
01834 $this->_lines($lines, '<');
01835 }
01836
01837 function _changed($orig, $closing) {
01838 $this->_deleted($orig);
01839 echo "---\n";
01840 $this->_added($closing);
01841 }
01842 }
01843
01849 class UnifiedDiffFormatter extends DiffFormatter {
01850 var $leading_context_lines = 2;
01851 var $trailing_context_lines = 2;
01852
01853 function _added($lines) {
01854 $this->_lines($lines, '+');
01855 }
01856 function _deleted($lines) {
01857 $this->_lines($lines, '-');
01858 }
01859 function _changed($orig, $closing) {
01860 $this->_deleted($orig);
01861 $this->_added($closing);
01862 }
01863 function _block_header($xbeg, $xlen, $ybeg, $ylen) {
01864 return "@@ -$xbeg,$xlen +$ybeg,$ylen @@";
01865 }
01866 }
01867
01872 class ArrayDiffFormatter extends DiffFormatter {
01873 function format($diff) {
01874 $oldline = 1;
01875 $newline = 1;
01876 $retval = array();
01877 foreach($diff->edits as $edit)
01878 switch($edit->type) {
01879 case 'add':
01880 foreach($edit->closing as $l) {
01881 $retval[] = array(
01882 'action' => 'add',
01883 'new'=> $l,
01884 'newline' => $newline++
01885 );
01886 }
01887 break;
01888 case 'delete':
01889 foreach($edit->orig as $l) {
01890 $retval[] = array(
01891 'action' => 'delete',
01892 'old' => $l,
01893 'oldline' => $oldline++,
01894 );
01895 }
01896 break;
01897 case 'change':
01898 foreach($edit->orig as $i => $l) {
01899 $retval[] = array(
01900 'action' => 'change',
01901 'old' => $l,
01902 'new' => @$edit->closing[$i],
01903 'oldline' => $oldline++,
01904 'newline' => $newline++,
01905 );
01906 }
01907 break;
01908 case 'copy':
01909 $oldline += count($edit->orig);
01910 $newline += count($edit->orig);
01911 }
01912 return $retval;
01913 }
01914 }
01915
01921 define('NBSP', ' ');
01922
01928 class _HWLDF_WordAccumulator {
01929 function _HWLDF_WordAccumulator () {
01930 $this->_lines = array();
01931 $this->_line = '';
01932 $this->_group = '';
01933 $this->_tag = '';
01934 }
01935
01936 function _flushGroup ($new_tag) {
01937 if ($this->_group !== '') {
01938 if ($this->_tag == 'ins')
01939 $this->_line .= '<ins class="diffchange diffchange-inline">' .
01940 htmlspecialchars ( $this->_group ) . '</ins>';
01941 elseif ($this->_tag == 'del')
01942 $this->_line .= '<del class="diffchange diffchange-inline">' .
01943 htmlspecialchars ( $this->_group ) . '</del>';
01944 else
01945 $this->_line .= htmlspecialchars ( $this->_group );
01946 }
01947 $this->_group = '';
01948 $this->_tag = $new_tag;
01949 }
01950
01951 function _flushLine ($new_tag) {
01952 $this->_flushGroup($new_tag);
01953 if ($this->_line != '')
01954 array_push ( $this->_lines, $this->_line );
01955 else
01956 # make empty lines visible by inserting an NBSP
01957 array_push ( $this->_lines, NBSP );
01958 $this->_line = '';
01959 }
01960
01961 function addWords ($words, $tag = '') {
01962 if ($tag != $this->_tag)
01963 $this->_flushGroup($tag);
01964
01965 foreach ($words as $word) {
01966
01967 if ($word == '')
01968 continue;
01969 if ($word[0] == "\n") {
01970 $this->_flushLine($tag);
01971 $word = substr($word, 1);
01972 }
01973 assert(!strstr($word, "\n"));
01974 $this->_group .= $word;
01975 }
01976 }
01977
01978 function getLines() {
01979 $this->_flushLine('~done');
01980 return $this->_lines;
01981 }
01982 }
01983
01989 class WordLevelDiff extends MappedDiff {
01990 const MAX_LINE_LENGTH = 10000;
01991
01992 function WordLevelDiff ($orig_lines, $closing_lines) {
01993 wfProfileIn( __METHOD__ );
01994
01995 list ($orig_words, $orig_stripped) = $this->_split($orig_lines);
01996 list ($closing_words, $closing_stripped) = $this->_split($closing_lines);
01997
01998 $this->MappedDiff($orig_words, $closing_words,
01999 $orig_stripped, $closing_stripped);
02000 wfProfileOut( __METHOD__ );
02001 }
02002
02003 function _split($lines) {
02004 wfProfileIn( __METHOD__ );
02005
02006 $words = array();
02007 $stripped = array();
02008 $first = true;
02009 foreach ( $lines as $line ) {
02010 # If the line is too long, just pretend the entire line is one big word
02011 # This prevents resource exhaustion problems
02012 if ( $first ) {
02013 $first = false;
02014 } else {
02015 $words[] = "\n";
02016 $stripped[] = "\n";
02017 }
02018 if ( strlen( $line ) > self::MAX_LINE_LENGTH ) {
02019 $words[] = $line;
02020 $stripped[] = $line;
02021 } else {
02022 $m = array();
02023 if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
02024 $line, $m))
02025 {
02026 $words = array_merge( $words, $m[0] );
02027 $stripped = array_merge( $stripped, $m[1] );
02028 }
02029 }
02030 }
02031 wfProfileOut( __METHOD__ );
02032 return array($words, $stripped);
02033 }
02034
02035 function orig () {
02036 wfProfileIn( __METHOD__ );
02037 $orig = new _HWLDF_WordAccumulator;
02038
02039 foreach ($this->edits as $edit) {
02040 if ($edit->type == 'copy')
02041 $orig->addWords($edit->orig);
02042 elseif ($edit->orig)
02043 $orig->addWords($edit->orig, 'del');
02044 }
02045 $lines = $orig->getLines();
02046 wfProfileOut( __METHOD__ );
02047 return $lines;
02048 }
02049
02050 function closing () {
02051 wfProfileIn( __METHOD__ );
02052 $closing = new _HWLDF_WordAccumulator;
02053
02054 foreach ($this->edits as $edit) {
02055 if ($edit->type == 'copy')
02056 $closing->addWords($edit->closing);
02057 elseif ($edit->closing)
02058 $closing->addWords($edit->closing, 'ins');
02059 }
02060 $lines = $closing->getLines();
02061 wfProfileOut( __METHOD__ );
02062 return $lines;
02063 }
02064 }
02065
02072 class TableDiffFormatter extends DiffFormatter {
02073 function TableDiffFormatter() {
02074 $this->leading_context_lines = 2;
02075 $this->trailing_context_lines = 2;
02076 }
02077
02078 public static function escapeWhiteSpace( $msg ) {
02079 $msg = preg_replace( '/^ /m', ' ', $msg );
02080 $msg = preg_replace( '/ $/m', ' ', $msg );
02081 $msg = preg_replace( '/ /', ' ', $msg );
02082 return $msg;
02083 }
02084
02085 function _block_header( $xbeg, $xlen, $ybeg, $ylen ) {
02086 $r = '<tr><td colspan="2" class="diff-lineno"><!--LINE '.$xbeg."--></td>\n" .
02087 '<td colspan="2" class="diff-lineno"><!--LINE '.$ybeg."--></td></tr>\n";
02088 return $r;
02089 }
02090
02091 function _start_block( $header ) {
02092 echo $header;
02093 }
02094
02095 function _end_block() {
02096 }
02097
02098 function _lines( $lines, $prefix=' ', $color='white' ) {
02099 }
02100
02101 # HTML-escape parameter before calling this
02102 function addedLine( $line ) {
02103 return $this->wrapLine( '+', 'diff-addedline', $line );
02104 }
02105
02106 # HTML-escape parameter before calling this
02107 function deletedLine( $line ) {
02108 return $this->wrapLine( '-', 'diff-deletedline', $line );
02109 }
02110
02111 # HTML-escape parameter before calling this
02112 function contextLine( $line ) {
02113 return $this->wrapLine( ' ', 'diff-context', $line );
02114 }
02115
02116 private function wrapLine( $marker, $class, $line ) {
02117 if( $line !== '' ) {
02118
02119 $line = Xml::tags( 'div', null, $this->escapeWhiteSpace( $line ) );
02120 }
02121 return "<td class='diff-marker'>$marker</td><td class='$class'>$line</td>";
02122 }
02123
02124 function emptyLine() {
02125 return '<td colspan="2"> </td>';
02126 }
02127
02128 function _added( $lines ) {
02129 foreach ($lines as $line) {
02130 echo '<tr>' . $this->emptyLine() .
02131 $this->addedLine( '<ins class="diffchange">' .
02132 htmlspecialchars ( $line ) . '</ins>' ) . "</tr>\n";
02133 }
02134 }
02135
02136 function _deleted($lines) {
02137 foreach ($lines as $line) {
02138 echo '<tr>' . $this->deletedLine( '<del class="diffchange">' .
02139 htmlspecialchars ( $line ) . '</del>' ) .
02140 $this->emptyLine() . "</tr>\n";
02141 }
02142 }
02143
02144 function _context( $lines ) {
02145 foreach ($lines as $line) {
02146 echo '<tr>' .
02147 $this->contextLine( htmlspecialchars ( $line ) ) .
02148 $this->contextLine( htmlspecialchars ( $line ) ) . "</tr>\n";
02149 }
02150 }
02151
02152 function _changed( $orig, $closing ) {
02153 wfProfileIn( __METHOD__ );
02154
02155 $diff = new WordLevelDiff( $orig, $closing );
02156 $del = $diff->orig();
02157 $add = $diff->closing();
02158
02159 # Notice that WordLevelDiff returns HTML-escaped output.
02160 # Hence, we will be calling addedLine/deletedLine without HTML-escaping.
02161
02162 while ( $line = array_shift( $del ) ) {
02163 $aline = array_shift( $add );
02164 echo '<tr>' . $this->deletedLine( $line ) .
02165 $this->addedLine( $aline ) . "</tr>\n";
02166 }
02167 foreach ($add as $line) { # If any leftovers
02168 echo '<tr>' . $this->emptyLine() .
02169 $this->addedLine( $line ) . "</tr>\n";
02170 }
02171 wfProfileOut( __METHOD__ );
02172 }
02173 }