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