MediaWiki  REL1_19
DifferenceEngine.php
Go to the documentation of this file.
00001 <?php
00015 define( 'MW_DIFF_VERSION', '1.11a' );
00016 
00021 class DifferenceEngine extends ContextSource {
00025         var $mOldid, $mNewid;
00026         var $mOldtext, $mNewtext;
00027         protected $mDiffLang;
00028 
00032         var $mOldPage, $mNewPage;
00033         var $mRcidMarkPatrolled;
00034 
00038         var $mOldRev, $mNewRev;
00039         private $mRevisionsIdsLoaded = false; // Have the revisions IDs been loaded
00040         var $mRevisionsLoaded = false; // Have the revisions been loaded
00041         var $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2?
00042         var $mCacheHit = false; // Was the diff fetched from cache?
00043 
00049         var $enableDebugComment = false;
00050 
00051         // If true, line X is not displayed when X is 1, for example to increase
00052         // readability and conserve space with many small diffs.
00053         protected $mReducedLineNumbers = false;
00054 
00055         // Link to action=markpatrolled
00056         protected $mMarkPatrolledLink = null;
00057 
00058         protected $unhide = false; # show rev_deleted content if allowed
00070         function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
00071                 $refreshCache = false, $unhide = false )
00072         {
00073                 if ( $context instanceof IContextSource ) {
00074                         $this->setContext( $context );
00075                 }
00076 
00077                 wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'\n" );
00078 
00079                 $this->mOldid = $old;
00080                 $this->mNewid = $new;
00081                 $this->mRcidMarkPatrolled = intval( $rcid );  # force it to be an integer
00082                 $this->mRefreshCache = $refreshCache;
00083                 $this->unhide = $unhide;
00084         }
00085 
00089         function setReducedLineNumbers( $value = true ) {
00090                 $this->mReducedLineNumbers = $value;
00091         }
00092 
00096         function getDiffLang() {
00097                 if ( $this->mDiffLang === null ) {
00098                         # Default language in which the diff text is written.
00099                         $this->mDiffLang = $this->getTitle()->getPageLanguage();
00100                 }
00101                 return $this->mDiffLang;
00102         }
00103 
00107         function wasCacheHit() {
00108                 return $this->mCacheHit;
00109         }
00110 
00114         function getOldid() {
00115                 $this->loadRevisionIds();
00116                 return $this->mOldid;
00117         }
00118 
00122         function getNewid() {
00123                 $this->loadRevisionIds();
00124                 return $this->mNewid;
00125         }
00126 
00134         function deletedLink( $id ) {
00135                 if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
00136                         $dbr = wfGetDB( DB_SLAVE );
00137                         $row = $dbr->selectRow('archive', '*',
00138                                 array( 'ar_rev_id' => $id ),
00139                                 __METHOD__ );
00140                         if ( $row ) {
00141                                 $rev = Revision::newFromArchiveRow( $row );
00142                                 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
00143                                 return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( array(
00144                                         'target' => $title->getPrefixedText(),
00145                                         'timestamp' => $rev->getTimestamp()
00146                                 ));
00147                         }
00148                 }
00149                 return false;
00150         }
00151 
00158         function deletedIdMarker( $id ) {
00159                 $link = $this->deletedLink( $id );
00160                 if ( $link ) {
00161                         return "[$link $id]";
00162                 } else {
00163                         return $id;
00164                 }
00165         }
00166 
00167         function showDiffPage( $diffOnly = false ) {
00168                 wfProfileIn( __METHOD__ );
00169 
00170                 # Allow frames except in certain special cases
00171                 $out = $this->getOutput();
00172                 $out->allowClickjacking();
00173                 $out->setRobotPolicy( 'noindex,nofollow' );
00174 
00175                 if ( !$this->loadRevisionData() ) {
00176                         // Sounds like a deleted revision... Let's see what we can do.
00177                         $t = $this->getTitle()->getPrefixedText();
00178                         $d = $this->msg( 'missingarticle-diff',
00179                                 $this->deletedIdMarker( $this->mOldid ),
00180                                 $this->deletedIdMarker( $this->mNewid ) )->escaped();
00181                         $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
00182                         $out->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", "<span class='plainlinks'>$d</span>" );
00183                         wfProfileOut( __METHOD__ );
00184                         return;
00185                 }
00186 
00187                 $user = $this->getUser();
00188                 $permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user );
00189                 if ( $this->mOldPage ) { # mOldPage might not be set, see below.
00190                         $permErrors = wfMergeErrorArrays( $permErrors,
00191                                 $this->mOldPage->getUserPermissionsErrors( 'read', $user ) );
00192                 }
00193                 if ( count( $permErrors ) ) {
00194                         wfProfileOut( __METHOD__ );
00195                         throw new PermissionsError( 'read', $permErrors );
00196                 }
00197 
00198                 # If external diffs are enabled both globally and for the user,
00199                 # we'll use the application/x-external-editor interface to call
00200                 # an external diff tool like kompare, kdiff3, etc.
00201                 if ( ExternalEdit::useExternalEngine( $this->getContext(), 'diff' ) ) {
00202                         $urls = array(
00203                                 'File' => array( 'Extension' => 'wiki', 'URL' =>
00204                                         # This should be mOldPage, but it may not be set, see below.
00205                                         $this->mNewPage->getCanonicalURL( array(
00206                                                 'action' => 'raw', 'oldid' => $this->mOldid ) )
00207                                 ),
00208                                 'File2' => array( 'Extension' => 'wiki', 'URL' =>
00209                                         $this->mNewPage->getCanonicalURL( array(
00210                                                 'action' => 'raw', 'oldid' => $this->mNewid ) )
00211                                 ),
00212                         );
00213 
00214                         $externalEditor = new ExternalEdit( $this->getContext(), $urls );
00215                         $externalEditor->execute();
00216 
00217                         wfProfileOut( __METHOD__ );
00218                         return;
00219                 }
00220 
00221                 $rollback = '';
00222                 $undoLink = '';
00223 
00224                 $query = array();
00225                 # Carry over 'diffonly' param via navigation links
00226                 if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
00227                         $query['diffonly'] = $diffOnly;
00228                 }
00229                 # Cascade unhide param in links for easy deletion browsing
00230                 if ( $this->unhide ) {
00231                         $query['unhide'] = 1;
00232                 }
00233 
00234                 # Check if one of the revisions is deleted/suppressed
00235                 $deleted = $suppressed = false;
00236                 $allowed = $this->mNewRev->userCan( Revision::DELETED_TEXT, $user );
00237 
00238                 # mOldRev is false if the difference engine is called with a "vague" query for
00239                 # a diff between a version V and its previous version V' AND the version V
00240                 # is the first version of that article. In that case, V' does not exist.
00241                 if ( $this->mOldRev === false ) {
00242                         $out->setPageTitle( $this->mNewPage->getPrefixedText() );
00243                         $out->addSubtitle( $this->msg( 'difference' ) );
00244                         $samePage = true;
00245                         $oldHeader = '';
00246                 } else {
00247                         wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
00248 
00249                         $sk = $this->getSkin();
00250                         if ( method_exists( $sk, 'suppressQuickbar' ) ) {
00251                                 $sk->suppressQuickbar();
00252                         }
00253 
00254                         if ( $this->mNewPage->equals( $this->mOldPage ) ) {
00255                                 $out->setPageTitle( $this->mNewPage->getPrefixedText() );
00256                                 $out->addSubtitle( $this->msg( 'difference' ) );
00257                                 $samePage = true;
00258                         } else {
00259                                 $out->setPageTitle( $this->mOldPage->getPrefixedText() . ', ' . $this->mNewPage->getPrefixedText() );
00260                                 $out->addSubtitle( $this->msg( 'difference-multipage' ) );
00261                                 $samePage = false;
00262                         }
00263 
00264                         if ( $samePage && $this->mNewPage->userCan( 'edit', $user ) ) {
00265                                 if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) {
00266                                         $out->preventClickjacking();
00267                                         $rollback = '&#160;&#160;&#160;' . Linker::generateRollback( $this->mNewRev );
00268                                 }
00269                                 if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
00270                                         $undoLink = ' ' . $this->msg( 'parentheses' )->rawParams(
00271                                                 Html::element( 'a', array(
00272                                                         'href' => $this->mNewPage->getLocalUrl( array(
00273                                                                 'action' => 'edit',
00274                                                                 'undoafter' => $this->mOldid,
00275                                                                 'undo' => $this->mNewid ) ),
00276                                                         'title' => Linker::titleAttrib( 'undo' )
00277                                                 ),
00278                                                 $this->msg( 'editundo' )->text()
00279                                         ) )->escaped();
00280                                 }
00281                         }
00282 
00283                         # Make "previous revision link"
00284                         if ( $samePage && $this->mOldRev->getPrevious() ) {
00285                                 $prevlink = Linker::linkKnown(
00286                                         $this->mOldPage,
00287                                         $this->msg( 'previousdiff' )->escaped(),
00288                                         array( 'id' => 'differences-prevlink' ),
00289                                         array( 'diff' => 'prev', 'oldid' => $this->mOldid ) + $query
00290                                 );
00291                         } else {
00292                                 $prevlink = '&#160;';
00293                         }
00294 
00295                         if ( $this->mOldRev->isMinor() ) {
00296                                 $oldminor = ChangesList::flag( 'minor' );
00297                         } else {
00298                                 $oldminor = '';
00299                         }
00300 
00301                         $ldel = $this->revisionDeleteLink( $this->mOldRev );
00302                         $oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' );
00303 
00304                         $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
00305                                 '<div id="mw-diff-otitle2">' .
00306                                         Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' .
00307                                 '<div id="mw-diff-otitle3">' . $oldminor .
00308                                         Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
00309                                 '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
00310 
00311                         if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
00312                                 $deleted = true; // old revisions text is hidden
00313                                 if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
00314                                         $suppressed = true; // also suppressed
00315                                 }
00316                         }
00317 
00318                         # Check if this user can see the revisions
00319                         if ( !$this->mOldRev->userCan( Revision::DELETED_TEXT, $user ) ) {
00320                                 $allowed = false;
00321                         }
00322                 }
00323 
00324                 # Make "next revision link"
00325                 # Skip next link on the top revision
00326                 if ( $samePage && !$this->mNewRev->isCurrent() ) {
00327                         $nextlink = Linker::linkKnown(
00328                                 $this->mNewPage,
00329                                 $this->msg( 'nextdiff' )->escaped(),
00330                                 array( 'id' => 'differences-nextlink' ),
00331                                 array( 'diff' => 'next', 'oldid' => $this->mNewid ) + $query
00332                         );
00333                 } else {
00334                         $nextlink = '&#160;';
00335                 }
00336 
00337                 if ( $this->mNewRev->isMinor() ) {
00338                         $newminor = ChangesList::flag( 'minor' );
00339                 } else {
00340                         $newminor = '';
00341                 }
00342 
00343                 # Handle RevisionDelete links...
00344                 $rdel = $this->revisionDeleteLink( $this->mNewRev );
00345                 $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) . $undoLink;
00346 
00347                 $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
00348                         '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) .
00349                                 " $rollback</div>" .
00350                         '<div id="mw-diff-ntitle3">' . $newminor .
00351                                 Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
00352                         '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
00353 
00354                 if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
00355                         $deleted = true; // new revisions text is hidden
00356                         if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) )
00357                                 $suppressed = true; // also suppressed
00358                 }
00359 
00360                 # If the diff cannot be shown due to a deleted revision, then output
00361                 # the diff header and links to unhide (if available)...
00362                 if ( $deleted && ( !$this->unhide || !$allowed ) ) {
00363                         $this->showDiffStyle();
00364                         $multi = $this->getMultiNotice();
00365                         $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
00366                         if ( !$allowed ) {
00367                                 $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
00368                                 # Give explanation for why revision is not visible
00369                                 $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
00370                                         array( $msg ) );
00371                         } else {
00372                                 # Give explanation and add a link to view the diff...
00373                                 $link = $this->getTitle()->getFullUrl( $this->getRequest()->appendQueryValue( 'unhide', '1', true ) );
00374                                 $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
00375                                 $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n", array( $msg, $link ) );
00376                         }
00377                 # Otherwise, output a regular diff...
00378                 } else {
00379                         # Add deletion notice if the user is viewing deleted content
00380                         $notice = '';
00381                         if ( $deleted ) {
00382                                 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
00383                                 $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" . $this->msg( $msg )->parse() . "</div>\n";
00384                         }
00385                         $this->showDiff( $oldHeader, $newHeader, $notice );
00386                         if ( !$diffOnly ) {
00387                                 $this->renderNewRevision();
00388                         }
00389                 }
00390                 wfProfileOut( __METHOD__ );
00391         }
00392 
00401         protected function markPatrolledLink() {
00402                 global $wgUseRCPatrol;
00403 
00404                 if ( $this->mMarkPatrolledLink === null ) {
00405                         // Prepare a change patrol link, if applicable
00406                         if ( $wgUseRCPatrol && $this->mNewPage->userCan( 'patrol', $this->getUser() ) ) {
00407                                 // If we've been given an explicit change identifier, use it; saves time
00408                                 if ( $this->mRcidMarkPatrolled ) {
00409                                         $rcid = $this->mRcidMarkPatrolled;
00410                                         $rc = RecentChange::newFromId( $rcid );
00411                                         // Already patrolled?
00412                                         $rcid = is_object( $rc ) && !$rc->getAttribute( 'rc_patrolled' ) ? $rcid : 0;
00413                                 } else {
00414                                         // Look for an unpatrolled change corresponding to this diff
00415                                         $db = wfGetDB( DB_SLAVE );
00416                                         $change = RecentChange::newFromConds(
00417                                                 array(
00418                                                 // Redundant user,timestamp condition so we can use the existing index
00419                                                         'rc_user_text'  => $this->mNewRev->getRawUserText(),
00420                                                         'rc_timestamp'  => $db->timestamp( $this->mNewRev->getTimestamp() ),
00421                                                         'rc_this_oldid' => $this->mNewid,
00422                                                         'rc_last_oldid' => $this->mOldid,
00423                                                         'rc_patrolled'  => 0
00424                                                 ),
00425                                                 __METHOD__
00426                                         );
00427                                         if ( $change instanceof RecentChange ) {
00428                                                 $rcid = $change->mAttribs['rc_id'];
00429                                                 $this->mRcidMarkPatrolled = $rcid;
00430                                         } else {
00431                                                 // None found
00432                                                 $rcid = 0;
00433                                         }
00434                                 }
00435                                 // Build the link
00436                                 if ( $rcid ) {
00437                                         $this->getOutput()->preventClickjacking();
00438                                         $token = $this->getUser()->getEditToken( $rcid );
00439                                         $this->mMarkPatrolledLink = ' <span class="patrollink">[' . Linker::linkKnown(
00440                                                 $this->mNewPage,
00441                                                 $this->msg( 'markaspatrolleddiff' )->escaped(),
00442                                                 array(),
00443                                                 array(
00444                                                         'action' => 'markpatrolled',
00445                                                         'rcid' => $rcid,
00446                                                         'token' => $token,
00447                                                 )
00448                                         ) . ']</span>';
00449                                 } else {
00450                                         $this->mMarkPatrolledLink = '';
00451                                 }
00452                         } else {
00453                                 $this->mMarkPatrolledLink = '';
00454                         }
00455                 }
00456 
00457                 return $this->mMarkPatrolledLink;
00458         }
00459 
00464         protected function revisionDeleteLink( $rev ) {
00465                 $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() );
00466                 if ( $link !== '' ) {
00467                         $link = '&#160;&#160;&#160;' . $link . ' ';
00468                 }
00469                 return $link;
00470         }
00471 
00475         function renderNewRevision() {
00476                 wfProfileIn( __METHOD__ );
00477                 $out = $this->getOutput();
00478                 $revHeader = $this->getRevisionHeader( $this->mNewRev );
00479                 # Add "current version as of X" title
00480                 $out->addHTML( "<hr class='diff-hr' />
00481                 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
00482                 # Page content may be handled by a hooked call instead...
00483                 if ( wfRunHooks( 'ArticleContentOnDiff', array( $this, $out ) ) ) {
00484                         $this->loadNewText();
00485                         $out->setRevisionId( $this->mNewid );
00486                         $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
00487                         $out->setArticleFlag( true );
00488 
00489                         if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) {
00490                                 // Stolen from Article::view --AG 2007-10-11
00491                                 // Give hooks a chance to customise the output
00492                                 // @TODO: standardize this crap into one function
00493                                 if ( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mNewPage, $out ) ) ) {
00494                                         // Wrap the whole lot in a <pre> and don't parse
00495                                         $m = array();
00496                                         preg_match( '!\.(css|js)$!u', $this->mNewPage->getText(), $m );
00497                                         $out->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
00498                                         $out->addHTML( htmlspecialchars( $this->mNewtext ) );
00499                                         $out->addHTML( "\n</pre>\n" );
00500                                 }
00501                         } elseif ( !wfRunHooks( 'ArticleViewCustom', array( $this->mNewtext, $this->mNewPage, $out ) ) ) {
00502                                 // Handled by extension
00503                         } else {
00504                                 // Normal page
00505                                 if ( $this->getTitle()->equals( $this->mNewPage ) ) {
00506                                         // If the Title stored in the context is the same as the one
00507                                         // of the new revision, we can use its associated WikiPage
00508                                         // object.
00509                                         $wikiPage = $this->getWikiPage();
00510                                 } else {
00511                                         // Otherwise we need to create our own WikiPage object
00512                                         $wikiPage = WikiPage::factory( $this->mNewPage );
00513                                 }
00514 
00515                                 $parserOptions = ParserOptions::newFromContext( $this->getContext() );
00516                                 $parserOptions->enableLimitReport();
00517                                 $parserOptions->setTidy( true );
00518 
00519                                 if ( !$this->mNewRev->isCurrent() ) {
00520                                         $parserOptions->setEditSection( false );
00521                                 }
00522 
00523                                 $parserOutput = $wikiPage->getParserOutput( $parserOptions, $this->mNewid );
00524 
00525                                 # WikiPage::getParserOutput() should not return false, but just in case
00526                                 if( $parserOutput ) {
00527                                         $out->addParserOutput( $parserOutput );
00528                                 }
00529                         }
00530                 }
00531                 # Add redundant patrol link on bottom...
00532                 $out->addHTML( $this->markPatrolledLink() );
00533 
00534                 wfProfileOut( __METHOD__ );
00535         }
00536 
00543         function showDiff( $otitle, $ntitle, $notice = '' ) {
00544                 $diff = $this->getDiff( $otitle, $ntitle, $notice );
00545                 if ( $diff === false ) {
00546                         $this->getOutput()->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' );
00547                         return false;
00548                 } else {
00549                         $this->showDiffStyle();
00550                         $this->getOutput()->addHTML( $diff );
00551                         return true;
00552                 }
00553         }
00554 
00558         function showDiffStyle() {
00559                 $this->getOutput()->addModuleStyles( 'mediawiki.action.history.diff' );
00560         }
00561 
00570         function getDiff( $otitle, $ntitle, $notice = '' ) {
00571                 $body = $this->getDiffBody();
00572                 if ( $body === false ) {
00573                         return false;
00574                 } else {
00575                         $multi = $this->getMultiNotice();
00576                         return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
00577                 }
00578         }
00579 
00585         public function getDiffBody() {
00586                 global $wgMemc;
00587                 wfProfileIn( __METHOD__ );
00588                 $this->mCacheHit = true;
00589                 // Check if the diff should be hidden from this user
00590                 if ( !$this->loadRevisionData() ) {
00591                         wfProfileOut( __METHOD__ );
00592                         return false;
00593                 } elseif ( $this->mOldRev && !$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
00594                         wfProfileOut( __METHOD__ );
00595                         return false;
00596                 } elseif ( $this->mNewRev && !$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
00597                         wfProfileOut( __METHOD__ );
00598                         return false;
00599                 }
00600                 // Short-circuit
00601                 // If mOldRev is false, it means that the 
00602                 if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev
00603                         && $this->mOldRev->getID() == $this->mNewRev->getID() ) )
00604                 {
00605                         wfProfileOut( __METHOD__ );
00606                         return '';
00607                 }
00608                 // Cacheable?
00609                 $key = false;
00610                 if ( $this->mOldid && $this->mNewid ) {
00611                         $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION,
00612                                 'oldid', $this->mOldid, 'newid', $this->mNewid );
00613                         // Try cache
00614                         if ( !$this->mRefreshCache ) {
00615                                 $difftext = $wgMemc->get( $key );
00616                                 if ( $difftext ) {
00617                                         wfIncrStats( 'diff_cache_hit' );
00618                                         $difftext = $this->localiseLineNumbers( $difftext );
00619                                         $difftext .= "\n<!-- diff cache key $key -->\n";
00620                                         wfProfileOut( __METHOD__ );
00621                                         return $difftext;
00622                                 }
00623                         } // don't try to load but save the result
00624                 }
00625                 $this->mCacheHit = false;
00626 
00627                 // Loadtext is permission safe, this just clears out the diff
00628                 if ( !$this->loadText() ) {
00629                         wfProfileOut( __METHOD__ );
00630                         return false;
00631                 }
00632 
00633                 $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
00634 
00635                 // Save to cache for 7 days
00636                 if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
00637                         wfIncrStats( 'diff_uncacheable' );
00638                 } elseif ( $key !== false && $difftext !== false ) {
00639                         wfIncrStats( 'diff_cache_miss' );
00640                         $wgMemc->set( $key, $difftext, 7 * 86400 );
00641                 } else {
00642                         wfIncrStats( 'diff_uncacheable' );
00643                 }
00644                 // Replace line numbers with the text in the user's language
00645                 if ( $difftext !== false ) {
00646                         $difftext = $this->localiseLineNumbers( $difftext );
00647                 }
00648                 wfProfileOut( __METHOD__ );
00649                 return $difftext;
00650         }
00651 
00656         private function initDiffEngines() {
00657                 global $wgExternalDiffEngine;
00658                 if ( $wgExternalDiffEngine == 'wikidiff' && !function_exists( 'wikidiff_do_diff' ) ) {
00659                         wfProfileIn( __METHOD__ . '-php_wikidiff.so' );
00660                         wfDl( 'php_wikidiff' );
00661                         wfProfileOut( __METHOD__ . '-php_wikidiff.so' );
00662                 }
00663                 elseif ( $wgExternalDiffEngine == 'wikidiff2' && !function_exists( 'wikidiff2_do_diff' ) ) {
00664                         wfProfileIn( __METHOD__ . '-php_wikidiff2.so' );
00665                         wfDl( 'wikidiff2' );
00666                         wfProfileOut( __METHOD__ . '-php_wikidiff2.so' );
00667                 }
00668         }
00669 
00676         function generateDiffBody( $otext, $ntext ) {
00677                 global $wgExternalDiffEngine, $wgContLang;
00678 
00679                 wfProfileIn( __METHOD__ );
00680 
00681                 $otext = str_replace( "\r\n", "\n", $otext );
00682                 $ntext = str_replace( "\r\n", "\n", $ntext );
00683 
00684                 $this->initDiffEngines();
00685 
00686                 if ( $wgExternalDiffEngine == 'wikidiff' && function_exists( 'wikidiff_do_diff' ) ) {
00687                         # For historical reasons, external diff engine expects
00688                         # input text to be HTML-escaped already
00689                         $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) );
00690                         $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) );
00691                         wfProfileOut( __METHOD__ );
00692                         return $wgContLang->unsegmentForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) .
00693                         $this->debug( 'wikidiff1' );
00694                 }
00695 
00696                 if ( $wgExternalDiffEngine == 'wikidiff2' && function_exists( 'wikidiff2_do_diff' ) ) {
00697                         # Better external diff engine, the 2 may some day be dropped
00698                         # This one does the escaping and segmenting itself
00699                         wfProfileIn( 'wikidiff2_do_diff' );
00700                         $text = wikidiff2_do_diff( $otext, $ntext, 2 );
00701                         $text .= $this->debug( 'wikidiff2' );
00702                         wfProfileOut( 'wikidiff2_do_diff' );
00703                         wfProfileOut( __METHOD__ );
00704                         return $text;
00705                 }
00706                 if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) {
00707                         # Diff via the shell
00708                         global $wgTmpDirectory;
00709                         $tempName1 = tempnam( $wgTmpDirectory, 'diff_' );
00710                         $tempName2 = tempnam( $wgTmpDirectory, 'diff_' );
00711 
00712                         $tempFile1 = fopen( $tempName1, "w" );
00713                         if ( !$tempFile1 ) {
00714                                 wfProfileOut( __METHOD__ );
00715                                 return false;
00716                         }
00717                         $tempFile2 = fopen( $tempName2, "w" );
00718                         if ( !$tempFile2 ) {
00719                                 wfProfileOut( __METHOD__ );
00720                                 return false;
00721                         }
00722                         fwrite( $tempFile1, $otext );
00723                         fwrite( $tempFile2, $ntext );
00724                         fclose( $tempFile1 );
00725                         fclose( $tempFile2 );
00726                         $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
00727                         wfProfileIn( __METHOD__ . "-shellexec" );
00728                         $difftext = wfShellExec( $cmd );
00729                         $difftext .= $this->debug( "external $wgExternalDiffEngine" );
00730                         wfProfileOut( __METHOD__ . "-shellexec" );
00731                         unlink( $tempName1 );
00732                         unlink( $tempName2 );
00733                         wfProfileOut( __METHOD__ );
00734                         return $difftext;
00735                 }
00736 
00737                 # Native PHP diff
00738                 $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
00739                 $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
00740                 $diffs = new Diff( $ota, $nta );
00741                 $formatter = new TableDiffFormatter();
00742                 $difftext = $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) .
00743                 wfProfileOut( __METHOD__ );
00744                 return $difftext;
00745         }
00746 
00751         protected function debug( $generator = "internal" ) {
00752                 global $wgShowHostnames;
00753                 if ( !$this->enableDebugComment ) {
00754                         return '';
00755                 }
00756                 $data = array( $generator );
00757                 if ( $wgShowHostnames ) {
00758                         $data[] = wfHostname();
00759                 }
00760                 $data[] = wfTimestamp( TS_DB );
00761                 return "<!-- diff generator: " .
00762                 implode( " ",
00763                 array_map(
00764                                         "htmlspecialchars",
00765                 $data ) ) .
00766                         " -->\n";
00767         }
00768 
00772         function localiseLineNumbers( $text ) {
00773                 return preg_replace_callback( '/<!--LINE (\d+)-->/',
00774                 array( &$this, 'localiseLineNumbersCb' ), $text );
00775         }
00776 
00777         function localiseLineNumbersCb( $matches ) {
00778                 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) return '';
00779                 return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
00780         }
00781 
00782 
00787         function getMultiNotice() {
00788                 if ( !is_object( $this->mOldRev ) || !is_object( $this->mNewRev ) ) {
00789                         return '';
00790                 } elseif ( !$this->mOldPage->equals( $this->mNewPage ) ) {
00791                         // Comparing two different pages? Count would be meaningless.
00792                         return '';
00793                 }
00794 
00795                 if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) {
00796                         $oldRev = $this->mNewRev; // flip
00797                         $newRev = $this->mOldRev; // flip
00798                 } else { // normal case
00799                         $oldRev = $this->mOldRev;
00800                         $newRev = $this->mNewRev;
00801                 }
00802 
00803                 $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev );
00804                 if ( $nEdits > 0 ) {
00805                         $limit = 100; // use diff-multi-manyusers if too many users
00806                         $numUsers = $this->mNewPage->countAuthorsBetween( $oldRev, $newRev, $limit );
00807                         return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
00808                 }
00809                 return ''; // nothing
00810         }
00811 
00819         public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
00820                 if ( $numUsers > $limit ) {
00821                         $msg = 'diff-multi-manyusers';
00822                         $numUsers = $limit;
00823                 } else {
00824                         $msg = 'diff-multi';
00825                 }
00826                 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
00827         }
00828 
00837         private function getRevisionHeader( Revision $rev, $complete = '' ) {
00838                 $lang = $this->getLanguage();
00839                 $user = $this->getUser();
00840                 $revtimestamp = $rev->getTimestamp();
00841                 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
00842                 $dateofrev = $lang->userDate( $revtimestamp, $user );
00843                 $timeofrev = $lang->userTime( $revtimestamp, $user );
00844 
00845                 $header = $this->msg(
00846                         $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
00847                         $timestamp,
00848                         $dateofrev,
00849                         $timeofrev
00850                 )->escaped();
00851 
00852                 if ( $complete !== 'complete' ) {
00853                         return $header;
00854                 }
00855 
00856                 $title = $rev->getTitle();
00857 
00858                 $header = Linker::linkKnown( $title, $header, array(),
00859                         array( 'oldid' => $rev->getID() ) );
00860 
00861                 if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) {
00862                         $editQuery = array( 'action' => 'edit' );
00863                         if ( !$rev->isCurrent() ) {
00864                                 $editQuery['oldid'] = $rev->getID();
00865                         }
00866 
00867                         $msg = $this->msg( $title->userCan( 'edit', $user ) ? 'editold' : 'viewsourceold' )->escaped();
00868                         $header .= ' (' . Linker::linkKnown( $title, $msg, array(), $editQuery ) . ')';
00869                         if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
00870                                 $header = Html::rawElement( 'span', array( 'class' => 'history-deleted' ), $header );
00871                         }
00872                 } else {
00873                         $header = Html::rawElement( 'span', array( 'class' => 'history-deleted' ), $header );
00874                 }
00875 
00876                 return $header;
00877         }
00878 
00884         function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
00885                 // shared.css sets diff in interface language/dir, but the actual content
00886                 // is often in a different language, mostly the page content language/dir
00887                 $tableClass = 'diff diff-contentalign-' . htmlspecialchars( $this->getDiffLang()->alignStart() );
00888                 $header = "<table class='$tableClass'>";
00889 
00890                 if ( !$diff && !$otitle ) {
00891                         $header .= "
00892                         <tr valign='top'>
00893                         <td class='diff-ntitle'>{$ntitle}</td>
00894                         </tr>";
00895                         $multiColspan = 1;
00896                 } else {
00897                         if ( $diff ) { // Safari/Chrome show broken output if cols not used
00898                                 $header .= "
00899                                 <col class='diff-marker' />
00900                                 <col class='diff-content' />
00901                                 <col class='diff-marker' />
00902                                 <col class='diff-content' />";
00903                                 $colspan = 2;
00904                                 $multiColspan = 4;
00905                         } else {
00906                                 $colspan = 1;
00907                                 $multiColspan = 2;
00908                         }
00909                         $header .= "
00910                         <tr valign='top'>
00911                         <td colspan='$colspan' class='diff-otitle'>{$otitle}</td>
00912                         <td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td>
00913                         </tr>";
00914                 }
00915 
00916                 if ( $multi != '' ) {
00917                         $header .= "<tr><td colspan='{$multiColspan}' align='center' class='diff-multi'>{$multi}</td></tr>";
00918                 }
00919                 if ( $notice != '' ) {
00920                         $header .= "<tr><td colspan='{$multiColspan}' align='center'>{$notice}</td></tr>";
00921                 }
00922 
00923                 return $header . $diff . "</table>";
00924         }
00925 
00929         function setText( $oldText, $newText ) {
00930                 $this->mOldtext = $oldText;
00931                 $this->mNewtext = $newText;
00932                 $this->mTextLoaded = 2;
00933                 $this->mRevisionsLoaded = true;
00934         }
00935 
00941         function setTextLanguage( $lang ) {
00942                 $this->mDiffLang = wfGetLangObj( $lang );
00943         }
00944 
00948         private function loadRevisionIds() {
00949                 if ( $this->mRevisionsIdsLoaded ) {
00950                         return;
00951                 }
00952 
00953                 $this->mRevisionsIdsLoaded = true;
00954 
00955                 $old = $this->mOldid;
00956                 $new = $this->mNewid;
00957 
00958                 if ( $new === 'prev' ) {
00959                         # Show diff between revision $old and the previous one.
00960                         # Get previous one from DB.
00961                         $this->mNewid = intval( $old );
00962                         $this->mOldid = $this->getTitle()->getPreviousRevisionID( $this->mNewid );
00963                 } elseif ( $new === 'next' ) {
00964                         # Show diff between revision $old and the next one.
00965                         # Get next one from DB.
00966                         $this->mOldid = intval( $old );
00967                         $this->mNewid = $this->getTitle()->getNextRevisionID( $this->mOldid );
00968                         if ( $this->mNewid === false ) {
00969                                 # if no result, NewId points to the newest old revision. The only newer
00970                                 # revision is cur, which is "0".
00971                                 $this->mNewid = 0;
00972                         }
00973                 } else {
00974                         $this->mOldid = intval( $old );
00975                         $this->mNewid = intval( $new );
00976                         wfRunHooks( 'NewDifferenceEngine', array( $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ) );
00977                 }
00978         }
00979 
00992         function loadRevisionData() {
00993                 if ( $this->mRevisionsLoaded ) {
00994                         return true;
00995                 }
00996 
00997                 // Whether it succeeds or fails, we don't want to try again
00998                 $this->mRevisionsLoaded = true;
00999 
01000                 $this->loadRevisionIds();
01001 
01002                 // Load the new revision object
01003                 $this->mNewRev = $this->mNewid
01004                         ? Revision::newFromId( $this->mNewid )
01005                         : Revision::newFromTitle( $this->getTitle() );
01006 
01007                 if ( !$this->mNewRev instanceof Revision ) {
01008                         return false;
01009                 }
01010 
01011                 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
01012                 $this->mNewid = $this->mNewRev->getId();
01013                 $this->mNewPage = $this->mNewRev->getTitle();
01014 
01015                 // Load the old revision object
01016                 $this->mOldRev = false;
01017                 if ( $this->mOldid ) {
01018                         $this->mOldRev = Revision::newFromId( $this->mOldid );
01019                 } elseif ( $this->mOldid === 0 ) {
01020                         $rev = $this->mNewRev->getPrevious();
01021                         if ( $rev ) {
01022                                 $this->mOldid = $rev->getId();
01023                                 $this->mOldRev = $rev;
01024                         } else {
01025                                 // No previous revision; mark to show as first-version only.
01026                                 $this->mOldid = false;
01027                                 $this->mOldRev = false;
01028                         }
01029                 } /* elseif ( $this->mOldid === false ) leave mOldRev false; */
01030 
01031                 if ( is_null( $this->mOldRev ) ) {
01032                         return false;
01033                 }
01034 
01035                 if ( $this->mOldRev ) {
01036                         $this->mOldPage = $this->mOldRev->getTitle();
01037                 }
01038 
01039                 return true;
01040         }
01041 
01047         function loadText() {
01048                 if ( $this->mTextLoaded == 2 ) {
01049                         return true;
01050                 } else {
01051                         // Whether it succeeds or fails, we don't want to try again
01052                         $this->mTextLoaded = 2;
01053                 }
01054 
01055                 if ( !$this->loadRevisionData() ) {
01056                         return false;
01057                 }
01058                 if ( $this->mOldRev ) {
01059                         $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER );
01060                         if ( $this->mOldtext === false ) {
01061                                 return false;
01062                         }
01063                 }
01064                 if ( $this->mNewRev ) {
01065                         $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
01066                         if ( $this->mNewtext === false ) {
01067                                 return false;
01068                         }
01069                 }
01070                 return true;
01071         }
01072 
01078         function loadNewText() {
01079                 if ( $this->mTextLoaded >= 1 ) {
01080                         return true;
01081                 } else {
01082                         $this->mTextLoaded = 1;
01083                 }
01084                 if ( !$this->loadRevisionData() ) {
01085                         return false;
01086                 }
01087                 $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
01088                 return true;
01089         }
01090 }