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
00037 var $enableDebugComment = false;
00038
00039
00040
00041 protected $mReducedLineNumbers = false;
00042
00043 protected $unhide = false;
00055 function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0,
00056 $refreshCache = false, $unhide = false )
00057 {
00058 if ( $titleObj ) {
00059 $this->mTitle = $titleObj;
00060 } else {
00061 global $wgTitle;
00062 $this->mTitle = $wgTitle;
00063 }
00064 wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
00065
00066 if ( 'prev' === $new ) {
00067 # Show diff between revision $old and the previous one.
00068 # Get previous one from DB.
00069 $this->mNewid = intval($old);
00070 $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid );
00071 } elseif ( 'next' === $new ) {
00072 # Show diff between revision $old and the next one.
00073 # Get next one from DB.
00074 $this->mOldid = intval($old);
00075 $this->mNewid = $this->mTitle->getNextRevisionID( $this->mOldid );
00076 if ( false === $this->mNewid ) {
00077 # if no result, NewId points to the newest old revision. The only newer
00078 # revision is cur, which is "0".
00079 $this->mNewid = 0;
00080 }
00081 } else {
00082 $this->mOldid = intval($old);
00083 $this->mNewid = intval($new);
00084 wfRunHooks( 'NewDifferenceEngine', array(&$titleObj, &$this->mOldid, &$this->mNewid, $old, $new) );
00085 }
00086 $this->mRcidMarkPatrolled = intval($rcid); # force it to be an integer
00087 $this->mRefreshCache = $refreshCache;
00088 $this->unhide = $unhide;
00089 }
00090
00091 function setReducedLineNumbers( $value = true ) {
00092 $this->mReducedLineNumbers = $value;
00093 }
00094
00095 function getTitle() {
00096 return $this->mTitle;
00097 }
00098
00099 function wasCacheHit() {
00100 return $this->mCacheHit;
00101 }
00102
00103 function getOldid() {
00104 return $this->mOldid;
00105 }
00106
00107 function getNewid() {
00108 return $this->mNewid;
00109 }
00110
00111 function showDiffPage( $diffOnly = false ) {
00112 global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol;
00113 wfProfileIn( __METHOD__ );
00114
00115 # Allow frames except in certain special cases
00116 $wgOut->allowClickjacking();
00117
00118 # If external diffs are enabled both globally and for the user,
00119 # we'll use the application/x-external-editor interface to call
00120 # an external diff tool like kompare, kdiff3, etc.
00121 if($wgUseExternalEditor && $wgUser->getOption('externaldiff')) {
00122 global $wgInputEncoding,$wgServer,$wgScript,$wgLang;
00123 $wgOut->disable();
00124 header ( "Content-type: application/x-external-editor; charset=".$wgInputEncoding );
00125 $url1=$this->mTitle->getFullURL( array(
00126 'action' => 'raw',
00127 'oldid' => $this->mOldid
00128 ) );
00129 $url2=$this->mTitle->getFullURL( array(
00130 'action' => 'raw',
00131 'oldid' => $this->mNewid
00132 ) );
00133 $special=$wgLang->getNsText(NS_SPECIAL);
00134 $control=<<<CONTROL
00135 [Process]
00136 Type=Diff text
00137 Engine=MediaWiki
00138 Script={$wgServer}{$wgScript}
00139 Special namespace={$special}
00140
00141 [File]
00142 Extension=wiki
00143 URL=$url1
00144
00145 [File 2]
00146 Extension=wiki
00147 URL=$url2
00148 CONTROL;
00149 echo($control);
00150 return;
00151 }
00152
00153 $wgOut->setArticleFlag( false );
00154 if ( !$this->loadRevisionData() ) {
00155 $t = $this->mTitle->getPrefixedText();
00156 $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
00157 $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
00158 $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
00159 wfProfileOut( __METHOD__ );
00160 return;
00161 }
00162
00163 wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
00164
00165 if ( $this->mNewRev->isCurrent() ) {
00166 $wgOut->setArticleFlag( true );
00167 }
00168
00169 # mOldid is false if the difference engine is called with a "vague" query for
00170 # a diff between a version V and its previous version V' AND the version V
00171 # is the first version of that article. In that case, V' does not exist.
00172 if ( $this->mOldid === false ) {
00173 $this->showFirstRevision();
00174 $this->renderNewRevision();
00175 wfProfileOut( __METHOD__ );
00176 return;
00177 }
00178
00179 $wgOut->suppressQuickbar();
00180
00181 $oldTitle = $this->mOldPage->getPrefixedText();
00182 $newTitle = $this->mNewPage->getPrefixedText();
00183 if( $oldTitle == $newTitle ) {
00184 $wgOut->setPageTitle( $newTitle );
00185 } else {
00186 $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle );
00187 }
00188 $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
00189 $wgOut->setRobotPolicy( 'noindex,nofollow' );
00190
00191 if ( !$this->mOldPage->userCanRead() || !$this->mNewPage->userCanRead() ) {
00192 $wgOut->loginToUse();
00193 $wgOut->output();
00194 $wgOut->disable();
00195 wfProfileOut( __METHOD__ );
00196 return;
00197 }
00198
00199 $sk = $wgUser->getSkin();
00200
00201
00202 $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
00203 if ( $editable && $this->mNewRev->isCurrent() && $wgUser->isAllowed( 'rollback' ) ) {
00204 $wgOut->preventClickjacking();
00205 $rollback = ' ' . $sk->generateRollback( $this->mNewRev );
00206 } else {
00207 $rollback = '';
00208 }
00209
00210
00211 if( $wgUseRCPatrol && $this->mTitle->userCan('patrol') ) {
00212
00213 if( $this->mRcidMarkPatrolled ) {
00214 $rcid = $this->mRcidMarkPatrolled;
00215 $rc = RecentChange::newFromId( $rcid );
00216
00217 $rcid = is_object($rc) && !$rc->getAttribute('rc_patrolled') ? $rcid : 0;
00218 } else {
00219
00220 $db = wfGetDB( DB_SLAVE );
00221 $change = RecentChange::newFromConds(
00222 array(
00223
00224 'rc_user_text' => $this->mNewRev->getRawUserText(),
00225 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
00226 'rc_this_oldid' => $this->mNewid,
00227 'rc_last_oldid' => $this->mOldid,
00228 'rc_patrolled' => 0
00229 ),
00230 __METHOD__
00231 );
00232 if( $change instanceof RecentChange ) {
00233 $rcid = $change->mAttribs['rc_id'];
00234 $this->mRcidMarkPatrolled = $rcid;
00235 } else {
00236
00237 $rcid = 0;
00238 }
00239 }
00240
00241 if( $rcid ) {
00242 $patrol = ' <span class="patrollink">[' . $sk->link(
00243 $this->mTitle,
00244 wfMsgHtml( 'markaspatrolleddiff' ),
00245 array(),
00246 array(
00247 'action' => 'markpatrolled',
00248 'rcid' => $rcid
00249 ),
00250 array(
00251 'known',
00252 'noclasses'
00253 )
00254 ) . ']</span>';
00255 } else {
00256 $patrol = '';
00257 }
00258 } else {
00259 $patrol = '';
00260 }
00261
00262 # Carry over 'diffonly' param via navigation links
00263 if( $diffOnly != $wgUser->getBoolOption('diffonly') ) {
00264 $query['diffonly'] = $diffOnly;
00265 }
00266
00267 # Make "previous revision link"
00268 $query['diff'] = 'prev';
00269 $query['oldid'] = $this->mOldid;
00270 # Cascade unhide param in links for easy deletion browsing
00271 if( $this->unhide ) {
00272 $query['unhide'] = 1;
00273 }
00274 $prevlink = $sk->link(
00275 $this->mTitle,
00276 wfMsgHtml( 'previousdiff' ),
00277 array(
00278 'id' => 'differences-prevlink'
00279 ),
00280 $query,
00281 array(
00282 'known',
00283 'noclasses'
00284 )
00285 );
00286
00287 # Make "next revision link"
00288 $query['diff'] = 'next';
00289 $query['oldid'] = $this->mNewid;
00290 # Skip next link on the top revision
00291 if( $this->mNewRev->isCurrent() ) {
00292 $nextlink = ' ';
00293 } else {
00294 $nextlink = $sk->link(
00295 $this->mTitle,
00296 wfMsgHtml( 'nextdiff' ),
00297 array(
00298 'id' => 'differences-nextlink'
00299 ),
00300 $query,
00301 array(
00302 'known',
00303 'noclasses'
00304 )
00305 );
00306 }
00307
00308 $oldminor = '';
00309 $newminor = '';
00310
00311 if( $this->mOldRev->isMinor() ) {
00312 $oldminor = ChangesList::flag( 'minor' );
00313 }
00314 if( $this->mNewRev->isMinor() ) {
00315 $newminor = ChangesList::flag( 'minor' );
00316 }
00317
00318 # Handle RevisionDelete links...
00319 $ldel = $this->revisionDeleteLink( $this->mOldRev );
00320 $rdel = $this->revisionDeleteLink( $this->mNewRev );
00321
00322 $oldHeader = '<div id="mw-diff-otitle1"><strong>'.$this->mOldtitle.'</strong></div>' .
00323 '<div id="mw-diff-otitle2">' .
00324 $sk->revUserTools( $this->mOldRev, !$this->unhide ).'</div>' .
00325 '<div id="mw-diff-otitle3">' . $oldminor .
00326 $sk->revComment( $this->mOldRev, !$diffOnly, !$this->unhide ).$ldel.'</div>' .
00327 '<div id="mw-diff-otitle4">' . $prevlink .'</div>';
00328 $newHeader = '<div id="mw-diff-ntitle1"><strong>'.$this->mNewtitle.'</strong></div>' .
00329 '<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev, !$this->unhide ) .
00330 " $rollback</div>" .
00331 '<div id="mw-diff-ntitle3">' . $newminor .
00332 $sk->revComment( $this->mNewRev, !$diffOnly, !$this->unhide ).$rdel.'</div>' .
00333 '<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>';
00334
00335 # Check if this user can see the revisions
00336 $allowed = $this->mOldRev->userCan(Revision::DELETED_TEXT)
00337 && $this->mNewRev->userCan(Revision::DELETED_TEXT);
00338 # Check if one of the revisions is deleted/suppressed
00339 $deleted = $suppressed = false;
00340 if( $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) {
00341 $deleted = true;
00342 if( $this->mOldRev->isDeleted(Revision::DELETED_RESTRICTED) )
00343 $suppressed = true;
00344 }
00345 if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
00346 $deleted = true;
00347 if( $this->mNewRev->isDeleted(Revision::DELETED_RESTRICTED) )
00348 $suppressed = true;
00349 }
00350 # If the diff cannot be shown due to a deleted revision, then output
00351 # the diff header and links to unhide (if available)...
00352 if( $deleted && (!$this->unhide || !$allowed) ) {
00353 $this->showDiffStyle();
00354 $multi = $this->getMultiNotice();
00355 $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
00356 if( !$allowed ) {
00357 $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
00358 # Give explanation for why revision is not visible
00359 $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n",
00360 array( $msg ) );
00361 } else {
00362 # Give explanation and add a link to view the diff...
00363 $link = $this->mTitle->getFullUrl( array(
00364 'diff' => $this->mNewid,
00365 'oldid' => $this->mOldid,
00366 'unhide' => 1
00367 ) );
00368 $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
00369 $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", array( $msg, $link ) );
00370 }
00371 # Otherwise, output a regular diff...
00372 } else {
00373 # Add deletion notice if the user is viewing deleted content
00374 $notice = '';
00375 if( $deleted ) {
00376 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
00377 $notice = "<div class='mw-warning plainlinks'>\n".wfMsgExt($msg,'parseinline')."</div>\n";
00378 }
00379 $this->showDiff( $oldHeader, $newHeader, $notice );
00380 if( !$diffOnly ) {
00381 $this->renderNewRevision();
00382 }
00383 }
00384 wfProfileOut( __METHOD__ );
00385 }
00386
00387 protected function revisionDeleteLink( $rev ) {
00388 global $wgUser;
00389 $link = '';
00390 $canHide = $wgUser->isAllowed( 'deleterevision' );
00391
00392
00393
00394 if( $canHide || ($rev->getVisibility() && $wgUser->isAllowed( 'deletedhistory' )) ) {
00395 $sk = $wgUser->getSkin();
00396 if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
00397 $link = $sk->revDeleteLinkDisabled( $canHide );
00398 } else {
00399 $query = array(
00400 'type' => 'revision',
00401 'target' => $rev->mTitle->getPrefixedDbkey(),
00402 'ids' => $rev->getId()
00403 );
00404 $link = $sk->revDeleteLink( $query,
00405 $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide );
00406 }
00407 $link = ' ' . $link . ' ';
00408 }
00409 return $link;
00410 }
00411
00415 function renderNewRevision() {
00416 global $wgOut, $wgUser;
00417 wfProfileIn( __METHOD__ );
00418
00419 $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
00420 # Add deleted rev tag if needed
00421 if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
00422 $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-permission' );
00423 } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
00424 $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-view' );
00425 }
00426
00427 if( !$this->mNewRev->isCurrent() ) {
00428 $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
00429 }
00430
00431 $this->loadNewText();
00432 if( is_object( $this->mNewRev ) ) {
00433 $wgOut->setRevisionId( $this->mNewRev->getId() );
00434 }
00435
00436 if( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
00437
00438
00439 if( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mTitle, $wgOut ) ) ) {
00440
00441 $m = array();
00442 preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
00443 $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
00444 $wgOut->addHTML( htmlspecialchars( $this->mNewtext ) );
00445 $wgOut->addHTML( "\n</pre>\n" );
00446 }
00447 } else {
00448 $wgOut->addWikiTextTidy( $this->mNewtext );
00449 }
00450
00451 if( is_object( $this->mNewRev ) && !$this->mNewRev->isCurrent() ) {
00452 $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
00453 }
00454 # Add redundant patrol link on bottom...
00455 if( $this->mRcidMarkPatrolled && $this->mTitle->quickUserCan('patrol') ) {
00456 $sk = $wgUser->getSkin();
00457 $wgOut->addHTML(
00458 "<div class='patrollink'>[" . $sk->link(
00459 $this->mTitle,
00460 wfMsgHtml( 'markaspatrolleddiff' ),
00461 array(),
00462 array(
00463 'action' => 'markpatrolled',
00464 'rcid' => $this->mRcidMarkPatrolled
00465 )
00466 ) . ']</div>'
00467 );
00468 }
00469
00470 wfProfileOut( __METHOD__ );
00471 }
00472
00477 function showFirstRevision() {
00478 global $wgOut, $wgUser;
00479 wfProfileIn( __METHOD__ );
00480
00481 # Get article text from the DB
00482 #
00483 if ( ! $this->loadNewText() ) {
00484 $t = $this->mTitle->getPrefixedText();
00485 $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
00486 $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
00487 $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
00488 wfProfileOut( __METHOD__ );
00489 return;
00490 }
00491 if ( $this->mNewRev->isCurrent() ) {
00492 $wgOut->setArticleFlag( true );
00493 }
00494
00495 # Check if user is allowed to look at this page. If not, bail out.
00496 #
00497 if ( !$this->mTitle->userCanRead() ) {
00498 $wgOut->loginToUse();
00499 $wgOut->output();
00500 wfProfileOut( __METHOD__ );
00501 throw new MWException("Permission Error: you do not have access to view this page");
00502 }
00503
00504 # Prepare the header box
00505 #
00506 $sk = $wgUser->getSkin();
00507
00508 $next = $this->mTitle->getNextRevisionID( $this->mNewid );
00509 if( !$next ) {
00510 $nextlink = '';
00511 } else {
00512 $nextlink = '<br />' . $sk->link(
00513 $this->mTitle,
00514 wfMsgHtml( 'nextdiff' ),
00515 array(
00516 'id' => 'differences-nextlink'
00517 ),
00518 array(
00519 'diff' => 'next',
00520 'oldid' => $this->mNewid,
00521 ),
00522 array(
00523 'known',
00524 'noclasses'
00525 )
00526 );
00527 }
00528 $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\">" .
00529 $sk->revUserTools( $this->mNewRev ) . "<br />" . $sk->revComment( $this->mNewRev ) . $nextlink . "</div>\n";
00530
00531 $wgOut->addHTML( $header );
00532
00533 $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
00534 $wgOut->setRobotPolicy( 'noindex,nofollow' );
00535
00536 wfProfileOut( __METHOD__ );
00537 }
00538
00543 function showDiff( $otitle, $ntitle, $notice = '' ) {
00544 global $wgOut;
00545 $diff = $this->getDiff( $otitle, $ntitle, $notice );
00546 if ( $diff === false ) {
00547 $wgOut->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' );
00548 return false;
00549 } else {
00550 $this->showDiffStyle();
00551 $wgOut->addHTML( $diff );
00552 return true;
00553 }
00554 }
00555
00559 function showDiffStyle() {
00560 global $wgStylePath, $wgStyleVersion, $wgOut;
00561 $wgOut->addStyle( 'common/diff.css' );
00562
00563
00564 $wgOut->addScript( "<script type=\"text/javascript\" src=\"$wgStylePath/common/diff.js?$wgStyleVersion\"></script>" );
00565 }
00566
00575 function getDiff( $otitle, $ntitle, $notice = '' ) {
00576 $body = $this->getDiffBody();
00577 if ( $body === false ) {
00578 return false;
00579 } else {
00580 $multi = $this->getMultiNotice();
00581 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
00582 }
00583 }
00584
00590 function getDiffBody() {
00591 global $wgMemc;
00592 wfProfileIn( __METHOD__ );
00593 $this->mCacheHit = true;
00594
00595 if ( !$this->loadRevisionData() )
00596 return '';
00597 if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
00598 return '';
00599 } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
00600 return '';
00601 } else if ( $this->mOldRev && $this->mNewRev && $this->mOldRev->getID() == $this->mNewRev->getID() ) {
00602 return '';
00603 }
00604
00605 $key = false;
00606 if ( $this->mOldid && $this->mNewid ) {
00607 $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid );
00608
00609 if ( !$this->mRefreshCache ) {
00610 $difftext = $wgMemc->get( $key );
00611 if ( $difftext ) {
00612 wfIncrStats( 'diff_cache_hit' );
00613 $difftext = $this->localiseLineNumbers( $difftext );
00614 $difftext .= "\n<!-- diff cache key $key -->\n";
00615 wfProfileOut( __METHOD__ );
00616 return $difftext;
00617 }
00618 }
00619 }
00620 $this->mCacheHit = false;
00621
00622
00623 if ( !$this->loadText() ) {
00624 wfProfileOut( __METHOD__ );
00625 return false;
00626 }
00627
00628 $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
00629
00630
00631 if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
00632 wfIncrStats( 'diff_uncacheable' );
00633 } else if ( $key !== false && $difftext !== false ) {
00634 wfIncrStats( 'diff_cache_miss' );
00635 $wgMemc->set( $key, $difftext, 7*86400 );
00636 } else {
00637 wfIncrStats( 'diff_uncacheable' );
00638 }
00639
00640 if ( $difftext !== false ) {
00641 $difftext = $this->localiseLineNumbers( $difftext );
00642 }
00643 wfProfileOut( __METHOD__ );
00644 return $difftext;
00645 }
00646
00651 private function initDiffEngines() {
00652 global $wgExternalDiffEngine;
00653 if ( $wgExternalDiffEngine == 'wikidiff' && !function_exists( 'wikidiff_do_diff' ) ) {
00654 wfProfileIn( __METHOD__ . '-php_wikidiff.so' );
00655 wfSuppressWarnings();
00656 dl( 'php_wikidiff.so' );
00657 wfRestoreWarnings();
00658 wfProfileOut( __METHOD__ . '-php_wikidiff.so' );
00659 }
00660 else if ( $wgExternalDiffEngine == 'wikidiff2' && !function_exists( 'wikidiff2_do_diff' ) ) {
00661 wfProfileIn( __METHOD__ . '-php_wikidiff2.so' );
00662 wfSuppressWarnings();
00663 dl( 'php_wikidiff2.so' );
00664 wfRestoreWarnings();
00665 wfProfileOut( __METHOD__ . '-php_wikidiff2.so' );
00666 }
00667 }
00668
00673 function generateDiffBody( $otext, $ntext ) {
00674 global $wgExternalDiffEngine, $wgContLang;
00675
00676 $otext = str_replace( "\r\n", "\n", $otext );
00677 $ntext = str_replace( "\r\n", "\n", $ntext );
00678
00679 $this->initDiffEngines();
00680
00681 if ( $wgExternalDiffEngine == 'wikidiff' && function_exists( 'wikidiff_do_diff' ) ) {
00682 # For historical reasons, external diff engine expects
00683 # input text to be HTML-escaped already
00684 $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) );
00685 $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) );
00686 return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) .
00687 $this->debug( 'wikidiff1' );
00688 }
00689
00690 if ( $wgExternalDiffEngine == 'wikidiff2' && function_exists( 'wikidiff2_do_diff' ) ) {
00691 # Better external diff engine, the 2 may some day be dropped
00692 # This one does the escaping and segmenting itself
00693 wfProfileIn( 'wikidiff2_do_diff' );
00694 $text = wikidiff2_do_diff( $otext, $ntext, 2 );
00695 $text .= $this->debug( 'wikidiff2' );
00696 wfProfileOut( 'wikidiff2_do_diff' );
00697 return $text;
00698 }
00699 if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) {
00700 # Diff via the shell
00701 global $wgTmpDirectory;
00702 $tempName1 = tempnam( $wgTmpDirectory, 'diff_' );
00703 $tempName2 = tempnam( $wgTmpDirectory, 'diff_' );
00704
00705 $tempFile1 = fopen( $tempName1, "w" );
00706 if ( !$tempFile1 ) {
00707 wfProfileOut( __METHOD__ );
00708 return false;
00709 }
00710 $tempFile2 = fopen( $tempName2, "w" );
00711 if ( !$tempFile2 ) {
00712 wfProfileOut( __METHOD__ );
00713 return false;
00714 }
00715 fwrite( $tempFile1, $otext );
00716 fwrite( $tempFile2, $ntext );
00717 fclose( $tempFile1 );
00718 fclose( $tempFile2 );
00719 $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
00720 wfProfileIn( __METHOD__ . "-shellexec" );
00721 $difftext = wfShellExec( $cmd );
00722 $difftext .= $this->debug( "external $wgExternalDiffEngine" );
00723 wfProfileOut( __METHOD__ . "-shellexec" );
00724 unlink( $tempName1 );
00725 unlink( $tempName2 );
00726 return $difftext;
00727 }
00728
00729 # Native PHP diff
00730 $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
00731 $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
00732 $diffs = new Diff( $ota, $nta );
00733 $formatter = new TableDiffFormatter();
00734 return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) .
00735 $this->debug();
00736 }
00737
00742 protected function debug( $generator="internal" ) {
00743 global $wgShowHostnames;
00744 if ( !$this->enableDebugComment ) {
00745 return '';
00746 }
00747 $data = array( $generator );
00748 if( $wgShowHostnames ) {
00749 $data[] = wfHostname();
00750 }
00751 $data[] = wfTimestamp( TS_DB );
00752 return "<!-- diff generator: " .
00753 implode( " ",
00754 array_map(
00755 "htmlspecialchars",
00756 $data ) ) .
00757 " -->\n";
00758 }
00759
00763 function localiseLineNumbers( $text ) {
00764 return preg_replace_callback( '/<!--LINE (\d+)-->/',
00765 array( &$this, 'localiseLineNumbersCb' ), $text );
00766 }
00767
00768 function localiseLineNumbersCb( $matches ) {
00769 global $wgLang;
00770 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) return '';
00771 return wfMsgExt( 'lineno', 'escape', $wgLang->formatNum( $matches[1] ) );
00772 }
00773
00774
00778 function getMultiNotice() {
00779 if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) )
00780 return '';
00781
00782 if( !$this->mOldPage->equals( $this->mNewPage ) ) {
00783
00784 return '';
00785 }
00786
00787 $oldid = $this->mOldRev->getId();
00788 $newid = $this->mNewRev->getId();
00789 if ( $oldid > $newid ) {
00790 $tmp = $oldid; $oldid = $newid; $newid = $tmp;
00791 }
00792
00793 $n = $this->mTitle->countRevisionsBetween( $oldid, $newid );
00794 if ( !$n )
00795 return '';
00796
00797 return wfMsgExt( 'diff-multi', array( 'parseinline' ), $n );
00798 }
00799
00800
00804 static function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
00805 $header = "<table class='diff'>";
00806 if( $diff ) {
00807 $header .= "
00808 <col class='diff-marker' />
00809 <col class='diff-content' />
00810 <col class='diff-marker' />
00811 <col class='diff-content' />";
00812 $colspan = 2;
00813 $multiColspan = 4;
00814 } else {
00815 $colspan = 1;
00816 $multiColspan = 2;
00817 }
00818 $header .= "
00819 <tr valign='top'>
00820 <td colspan='$colspan' class='diff-otitle'>{$otitle}</td>
00821 <td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td>
00822 </tr>";
00823
00824 if ( $multi != '' ) {
00825 $header .= "<tr><td colspan='{$multiColspan}' align='center' class='diff-multi'>{$multi}</td></tr>";
00826 }
00827 if ( $notice != '' ) {
00828 $header .= "<tr><td colspan='{$multiColspan}' align='center'>{$notice}</td></tr>";
00829 }
00830
00831 return $header . $diff . "</table>";
00832 }
00833
00837 function setText( $oldText, $newText ) {
00838 $this->mOldtext = $oldText;
00839 $this->mNewtext = $newText;
00840 $this->mTextLoaded = 2;
00841 $this->mRevisionsLoaded = true;
00842 }
00843
00854 function loadRevisionData() {
00855 global $wgLang, $wgUser;
00856 if ( $this->mRevisionsLoaded ) {
00857 return true;
00858 } else {
00859
00860 $this->mRevisionsLoaded = true;
00861 }
00862
00863
00864 $this->mNewRev = $this->mNewid
00865 ? Revision::newFromId( $this->mNewid )
00866 : Revision::newFromTitle( $this->mTitle );
00867 if( !$this->mNewRev instanceof Revision )
00868 return false;
00869
00870
00871 $this->mNewid = $this->mNewRev->getId();
00872
00873
00874 $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
00875
00876
00877 $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true );
00878 $dateofrev = $wgLang->date( $this->mNewRev->getTimestamp(), true );
00879 $timeofrev = $wgLang->time( $this->mNewRev->getTimestamp(), true );
00880 $this->mNewPage = $this->mNewRev->getTitle();
00881 if( $this->mNewRev->isCurrent() ) {
00882 $newLink = $this->mNewPage->escapeLocalUrl( array(
00883 'oldid' => $this->mNewid
00884 ) );
00885 $this->mPagetitle = htmlspecialchars( wfMsg(
00886 'currentrev-asof',
00887 $timestamp,
00888 $dateofrev,
00889 $timeofrev
00890 ) );
00891 $newEdit = $this->mNewPage->escapeLocalUrl( array(
00892 'action' => 'edit'
00893 ) );
00894
00895 $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
00896 $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
00897 } else {
00898 $newLink = $this->mNewPage->escapeLocalUrl( array(
00899 'oldid' => $this->mNewid
00900 ) );
00901 $newEdit = $this->mNewPage->escapeLocalUrl( array(
00902 'action' => 'edit',
00903 'oldid' => $this->mNewid
00904 ) );
00905 $this->mPagetitle = htmlspecialchars( wfMsg(
00906 'revisionasof',
00907 $timestamp,
00908 $dateofrev,
00909 $timeofrev
00910 ) );
00911
00912 $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
00913 $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
00914 }
00915 if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
00916 $this->mNewtitle = "<span class='history-deleted'>{$this->mPagetitle}</span>";
00917 } else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
00918 $this->mNewtitle = "<span class='history-deleted'>{$this->mNewtitle}</span>";
00919 }
00920
00921
00922 $this->mOldRev = false;
00923 if( $this->mOldid ) {
00924 $this->mOldRev = Revision::newFromId( $this->mOldid );
00925 } elseif ( $this->mOldid === 0 ) {
00926 $rev = $this->mNewRev->getPrevious();
00927 if( $rev ) {
00928 $this->mOldid = $rev->getId();
00929 $this->mOldRev = $rev;
00930 } else {
00931
00932 $this->mOldid = false;
00933 $this->mOldRev = false;
00934 }
00935 }
00936
00937 if( is_null( $this->mOldRev ) ) {
00938 return false;
00939 }
00940
00941 if ( $this->mOldRev ) {
00942 $this->mOldPage = $this->mOldRev->getTitle();
00943
00944 $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true );
00945 $dateofrev = $wgLang->date( $this->mOldRev->getTimestamp(), true );
00946 $timeofrev = $wgLang->time( $this->mOldRev->getTimestamp(), true );
00947 $oldLink = $this->mOldPage->escapeLocalUrl( array(
00948 'oldid' => $this->mOldid
00949 ) );
00950 $oldEdit = $this->mOldPage->escapeLocalUrl( array(
00951 'action' => 'edit',
00952 'oldid' => $this->mOldid
00953 ) );
00954 $this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t, $dateofrev, $timeofrev ) );
00955
00956 $this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>"
00957 . " (<a href='$oldEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
00958
00959 $newUndo = $this->mNewPage->escapeLocalUrl( array(
00960 'action' => 'edit',
00961 'undoafter' => $this->mOldid,
00962 'undo' => $this->mNewid
00963 ) );
00964 $htmlLink = htmlspecialchars( wfMsg( 'editundo' ) );
00965 $htmlTitle = $wgUser->getSkin()->tooltip( 'undo' );
00966 if( $editable && !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
00967 $this->mNewtitle .= " (<a href='$newUndo' $htmlTitle>" . $htmlLink . "</a>)";
00968 }
00969
00970 if( !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) {
00971 $this->mOldtitle = '<span class="history-deleted">' . $this->mOldPagetitle . '</span>';
00972 } else if( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
00973 $this->mOldtitle = '<span class="history-deleted">' . $this->mOldtitle . '</span>';
00974 }
00975 }
00976
00977 return true;
00978 }
00979
00983 function loadText() {
00984 if ( $this->mTextLoaded == 2 ) {
00985 return true;
00986 } else {
00987
00988 $this->mTextLoaded = 2;
00989 }
00990
00991 if ( !$this->loadRevisionData() ) {
00992 return false;
00993 }
00994 if ( $this->mOldRev ) {
00995 $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER );
00996 if ( $this->mOldtext === false ) {
00997 return false;
00998 }
00999 }
01000 if ( $this->mNewRev ) {
01001 $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
01002 if ( $this->mNewtext === false ) {
01003 return false;
01004 }
01005 }
01006 return true;
01007 }
01008
01012 function loadNewText() {
01013 if ( $this->mTextLoaded >= 1 ) {
01014 return true;
01015 } else {
01016 $this->mTextLoaded = 1;
01017 }
01018 if ( !$this->loadRevisionData() ) {
01019 return false;
01020 }
01021 $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
01022 return true;
01023 }
01024 }