MediaWiki  REL1_22
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 $mOldTags, $mNewTags;
00045     var $mOldContent, $mNewContent;
00046     protected $mDiffLang;
00047 
00051     var $mOldPage, $mNewPage;
00052 
00056     var $mOldRev, $mNewRev;
00057     private $mRevisionsIdsLoaded = false; // Have the revisions IDs been loaded
00058     var $mRevisionsLoaded = false; // Have the revisions been loaded
00059     var $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2?
00060     var $mCacheHit = false; // Was the diff fetched from cache?
00061 
00067     var $enableDebugComment = false;
00068 
00069     // If true, line X is not displayed when X is 1, for example to increase
00070     // readability and conserve space with many small diffs.
00071     protected $mReducedLineNumbers = false;
00072 
00073     // Link to action=markpatrolled
00074     protected $mMarkPatrolledLink = null;
00075 
00076     protected $unhide = false; # show rev_deleted content if allowed
00088     function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
00089         $refreshCache = false, $unhide = false )
00090     {
00091         if ( $context instanceof IContextSource ) {
00092             $this->setContext( $context );
00093         }
00094 
00095         wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'\n" );
00096 
00097         $this->mOldid = $old;
00098         $this->mNewid = $new;
00099         $this->mRefreshCache = $refreshCache;
00100         $this->unhide = $unhide;
00101     }
00102 
00106     function setReducedLineNumbers( $value = true ) {
00107         $this->mReducedLineNumbers = $value;
00108     }
00109 
00113     function getDiffLang() {
00114         if ( $this->mDiffLang === null ) {
00115             # Default language in which the diff text is written.
00116             $this->mDiffLang = $this->getTitle()->getPageLanguage();
00117         }
00118         return $this->mDiffLang;
00119     }
00120 
00124     function wasCacheHit() {
00125         return $this->mCacheHit;
00126     }
00127 
00131     function getOldid() {
00132         $this->loadRevisionIds();
00133         return $this->mOldid;
00134     }
00135 
00139     function getNewid() {
00140         $this->loadRevisionIds();
00141         return $this->mNewid;
00142     }
00143 
00151     function deletedLink( $id ) {
00152         if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
00153             $dbr = wfGetDB( DB_SLAVE );
00154             $row = $dbr->selectRow( 'archive', '*',
00155                 array( 'ar_rev_id' => $id ),
00156                 __METHOD__ );
00157             if ( $row ) {
00158                 $rev = Revision::newFromArchiveRow( $row );
00159                 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
00160                 return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( array(
00161                     'target' => $title->getPrefixedText(),
00162                     'timestamp' => $rev->getTimestamp()
00163                 ));
00164             }
00165         }
00166         return false;
00167     }
00168 
00175     function deletedIdMarker( $id ) {
00176         $link = $this->deletedLink( $id );
00177         if ( $link ) {
00178             return "[$link $id]";
00179         } else {
00180             return $id;
00181         }
00182     }
00183 
00184     private function showMissingRevision() {
00185         $out = $this->getOutput();
00186 
00187         $missing = array();
00188         if ( $this->mOldRev === null ||
00189             ( $this->mOldRev && $this->mOldContent === null )
00190         ) {
00191             $missing[] = $this->deletedIdMarker( $this->mOldid );
00192         }
00193         if ( $this->mNewRev === null ||
00194             ( $this->mNewRev && $this->mNewContent === null )
00195         ) {
00196             $missing[] = $this->deletedIdMarker( $this->mNewid );
00197         }
00198 
00199         $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
00200         $out->addWikiMsg( 'difference-missing-revision',
00201             $this->getLanguage()->listToText( $missing ), count( $missing ) );
00202     }
00203 
00204     function showDiffPage( $diffOnly = false ) {
00205         wfProfileIn( __METHOD__ );
00206 
00207         # Allow frames except in certain special cases
00208         $out = $this->getOutput();
00209         $out->allowClickjacking();
00210         $out->setRobotPolicy( 'noindex,nofollow' );
00211 
00212         if ( !$this->loadRevisionData() ) {
00213             $this->showMissingRevision();
00214             wfProfileOut( __METHOD__ );
00215             return;
00216         }
00217 
00218         $user = $this->getUser();
00219         $permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user );
00220         if ( $this->mOldPage ) { # mOldPage might not be set, see below.
00221             $permErrors = wfMergeErrorArrays( $permErrors,
00222                 $this->mOldPage->getUserPermissionsErrors( 'read', $user ) );
00223         }
00224         if ( count( $permErrors ) ) {
00225             wfProfileOut( __METHOD__ );
00226             throw new PermissionsError( 'read', $permErrors );
00227         }
00228 
00229         $rollback = '';
00230         $undoLink = '';
00231 
00232         $query = array();
00233         # Carry over 'diffonly' param via navigation links
00234         if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
00235             $query['diffonly'] = $diffOnly;
00236         }
00237         # Cascade unhide param in links for easy deletion browsing
00238         if ( $this->unhide ) {
00239             $query['unhide'] = 1;
00240         }
00241 
00242         # Check if one of the revisions is deleted/suppressed
00243         $deleted = $suppressed = false;
00244         $allowed = $this->mNewRev->userCan( Revision::DELETED_TEXT, $user );
00245 
00246         $revisionTools = array();
00247 
00248         # mOldRev is false if the difference engine is called with a "vague" query for
00249         # a diff between a version V and its previous version V' AND the version V
00250         # is the first version of that article. In that case, V' does not exist.
00251         if ( $this->mOldRev === false ) {
00252             $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
00253             $samePage = true;
00254             $oldHeader = '';
00255         } else {
00256             wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
00257 
00258             if ( $this->mNewPage->equals( $this->mOldPage ) ) {
00259                 $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
00260                 $samePage = true;
00261             } else {
00262                 $out->setPageTitle( $this->msg( 'difference-title-multipage', $this->mOldPage->getPrefixedText(),
00263                     $this->mNewPage->getPrefixedText() ) );
00264                 $out->addSubtitle( $this->msg( 'difference-multipage' ) );
00265                 $samePage = false;
00266             }
00267 
00268             if ( $samePage && $this->mNewPage->quickUserCan( 'edit', $user ) ) {
00269                 if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) {
00270                     $rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext() );
00271                     if ( $rollbackLink ) {
00272                         $out->preventClickjacking();
00273                         $rollback = '&#160;&#160;&#160;' . $rollbackLink;
00274                     }
00275                 }
00276                 if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
00277                     $undoLink = Html::element( 'a', array(
00278                             'href' => $this->mNewPage->getLocalURL( array(
00279                                 'action' => 'edit',
00280                                 'undoafter' => $this->mOldid,
00281                                 'undo' => $this->mNewid ) ),
00282                             'title' => Linker::titleAttrib( 'undo' )
00283                         ),
00284                         $this->msg( 'editundo' )->text()
00285                     );
00286                     $revisionTools[] = $undoLink;
00287                 }
00288             }
00289 
00290             # Make "previous revision link"
00291             if ( $samePage && $this->mOldRev->getPrevious() ) {
00292                 $prevlink = Linker::linkKnown(
00293                     $this->mOldPage,
00294                     $this->msg( 'previousdiff' )->escaped(),
00295                     array( 'id' => 'differences-prevlink' ),
00296                     array( 'diff' => 'prev', 'oldid' => $this->mOldid ) + $query
00297                 );
00298             } else {
00299                 $prevlink = '&#160;';
00300             }
00301 
00302             if ( $this->mOldRev->isMinor() ) {
00303                 $oldminor = ChangesList::flag( 'minor' );
00304             } else {
00305                 $oldminor = '';
00306             }
00307 
00308             $ldel = $this->revisionDeleteLink( $this->mOldRev );
00309             $oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' );
00310             $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff' );
00311 
00312             $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
00313                 '<div id="mw-diff-otitle2">' .
00314                     Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' .
00315                 '<div id="mw-diff-otitle3">' . $oldminor .
00316                     Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
00317                 '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
00318                 '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
00319 
00320             if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
00321                 $deleted = true; // old revisions text is hidden
00322                 if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
00323                     $suppressed = true; // also suppressed
00324                 }
00325             }
00326 
00327             # Check if this user can see the revisions
00328             if ( !$this->mOldRev->userCan( Revision::DELETED_TEXT, $user ) ) {
00329                 $allowed = false;
00330             }
00331         }
00332 
00333         # Make "next revision link"
00334         # Skip next link on the top revision
00335         if ( $samePage && !$this->mNewRev->isCurrent() ) {
00336             $nextlink = Linker::linkKnown(
00337                 $this->mNewPage,
00338                 $this->msg( 'nextdiff' )->escaped(),
00339                 array( 'id' => 'differences-nextlink' ),
00340                 array( 'diff' => 'next', 'oldid' => $this->mNewid ) + $query
00341             );
00342         } else {
00343             $nextlink = '&#160;';
00344         }
00345 
00346         if ( $this->mNewRev->isMinor() ) {
00347             $newminor = ChangesList::flag( 'minor' );
00348         } else {
00349             $newminor = '';
00350         }
00351 
00352         # Handle RevisionDelete links...
00353         $rdel = $this->revisionDeleteLink( $this->mNewRev );
00354 
00355         # Allow extensions to define their own revision tools
00356         wfRunHooks( 'DiffRevisionTools', array( $this->mNewRev, &$revisionTools ) );
00357         $formattedRevisionTools = array();
00358         // Put each one in parentheses (poor man's button)
00359         foreach ( $revisionTools as $tool ) {
00360             $formattedRevisionTools[] = $this->msg( 'parentheses' )->rawParams( $tool )->escaped();
00361         }
00362         $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) . ' ' . implode( ' ', $formattedRevisionTools );
00363         $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff' );
00364 
00365         $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
00366             '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) .
00367                 " $rollback</div>" .
00368             '<div id="mw-diff-ntitle3">' . $newminor .
00369                 Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
00370             '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
00371             '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
00372 
00373         if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
00374             $deleted = true; // new revisions text is hidden
00375             if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
00376                 $suppressed = true; // also suppressed
00377             }
00378         }
00379 
00380         # If the diff cannot be shown due to a deleted revision, then output
00381         # the diff header and links to unhide (if available)...
00382         if ( $deleted && ( !$this->unhide || !$allowed ) ) {
00383             $this->showDiffStyle();
00384             $multi = $this->getMultiNotice();
00385             $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
00386             if ( !$allowed ) {
00387                 $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
00388                 # Give explanation for why revision is not visible
00389                 $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
00390                     array( $msg ) );
00391             } else {
00392                 # Give explanation and add a link to view the diff...
00393                 $link = $this->getTitle()->getFullURL( $this->getRequest()->appendQueryValue( 'unhide', '1', true ) );
00394                 $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
00395                 $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n", array( $msg, $link ) );
00396             }
00397         # Otherwise, output a regular diff...
00398         } else {
00399             # Add deletion notice if the user is viewing deleted content
00400             $notice = '';
00401             if ( $deleted ) {
00402                 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
00403                 $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" . $this->msg( $msg )->parse() . "</div>\n";
00404             }
00405             $this->showDiff( $oldHeader, $newHeader, $notice );
00406             if ( !$diffOnly ) {
00407                 $this->renderNewRevision();
00408             }
00409         }
00410         wfProfileOut( __METHOD__ );
00411     }
00412 
00421     protected function markPatrolledLink() {
00422         global $wgUseRCPatrol, $wgEnableAPI, $wgEnableWriteAPI;
00423         $user = $this->getUser();
00424 
00425         if ( $this->mMarkPatrolledLink === null ) {
00426             // Prepare a change patrol link, if applicable
00427             if (
00428                 // Is patrolling enabled and the user allowed to?
00429                 $wgUseRCPatrol && $this->mNewPage->quickUserCan( 'patrol', $user ) &&
00430                 // Only do this if the revision isn't more than 6 hours older
00431                 // than the Max RC age (6h because the RC might not be cleaned out regularly)
00432                 RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 )
00433             ) {
00434                 // Look for an unpatrolled change corresponding to this diff
00435 
00436                 $db = wfGetDB( DB_SLAVE );
00437                 $change = RecentChange::newFromConds(
00438                     array(
00439                         'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
00440                         'rc_this_oldid' => $this->mNewid,
00441                         'rc_patrolled' => 0
00442                     ),
00443                     __METHOD__,
00444                     array( 'USE INDEX' => 'rc_timestamp' )
00445                 );
00446 
00447                 if ( $change && $change->getPerformer()->getName() !== $user->getName() ) {
00448                     $rcid = $change->getAttribute( 'rc_id' );
00449                 } else {
00450                     // None found or the page has been created by the current user.
00451                     // If the user could patrol this it already would be patrolled
00452                     $rcid = 0;
00453                 }
00454                 // Build the link
00455                 if ( $rcid ) {
00456                     $this->getOutput()->preventClickjacking();
00457                     if ( $wgEnableAPI && $wgEnableWriteAPI
00458                         && $user->isAllowed( 'writeapi' )
00459                     ) {
00460                         $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' );
00461                     }
00462 
00463                     $token = $user->getEditToken( $rcid );
00464                     $this->mMarkPatrolledLink = ' <span class="patrollink">[' . Linker::linkKnown(
00465                         $this->mNewPage,
00466                         $this->msg( 'markaspatrolleddiff' )->escaped(),
00467                         array(),
00468                         array(
00469                             'action' => 'markpatrolled',
00470                             'rcid' => $rcid,
00471                             'token' => $token,
00472                         )
00473                     ) . ']</span>';
00474                 } else {
00475                     $this->mMarkPatrolledLink = '';
00476                 }
00477             } else {
00478                 $this->mMarkPatrolledLink = '';
00479             }
00480         }
00481 
00482         return $this->mMarkPatrolledLink;
00483     }
00484 
00489     protected function revisionDeleteLink( $rev ) {
00490         $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() );
00491         if ( $link !== '' ) {
00492             $link = '&#160;&#160;&#160;' . $link . ' ';
00493         }
00494         return $link;
00495     }
00496 
00500     function renderNewRevision() {
00501         wfProfileIn( __METHOD__ );
00502         $out = $this->getOutput();
00503         $revHeader = $this->getRevisionHeader( $this->mNewRev );
00504         # Add "current version as of X" title
00505         $out->addHTML( "<hr class='diff-hr' />
00506         <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
00507         # Page content may be handled by a hooked call instead...
00508         if ( wfRunHooks( 'ArticleContentOnDiff', array( $this, $out ) ) ) {
00509             $this->loadNewText();
00510             $out->setRevisionId( $this->mNewid );
00511             $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
00512             $out->setArticleFlag( true );
00513 
00514             // NOTE: only needed for B/C: custom rendering of JS/CSS via hook
00515             if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) {
00516                 // Stolen from Article::view --AG 2007-10-11
00517                 // Give hooks a chance to customise the output
00518                 // @todo standardize this crap into one function
00519                 if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
00520                     // NOTE: deprecated hook, B/C only
00521                     // use the content object's own rendering
00522                     $cnt = $this->mNewRev->getContent();
00523                     $po = $cnt ? $cnt->getParserOutput( $this->mNewRev->getTitle(), $this->mNewRev->getId() ) : null;
00524                     $txt = $po ? $po->getText() : '';
00525                     $out->addHTML( $txt );
00526                 }
00527             } elseif ( !wfRunHooks( 'ArticleContentViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
00528                 // Handled by extension
00529             } elseif ( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
00530                 // NOTE: deprecated hook, B/C only
00531                 // Handled by extension
00532             } else {
00533                 // Normal page
00534                 if ( $this->getTitle()->equals( $this->mNewPage ) ) {
00535                     // If the Title stored in the context is the same as the one
00536                     // of the new revision, we can use its associated WikiPage
00537                     // object.
00538                     $wikiPage = $this->getWikiPage();
00539                 } else {
00540                     // Otherwise we need to create our own WikiPage object
00541                     $wikiPage = WikiPage::factory( $this->mNewPage );
00542                 }
00543 
00544                 $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
00545 
00546                 # Also try to load it as a redirect
00547                 $rt = $this->mNewContent ? $this->mNewContent->getRedirectTarget() : null;
00548 
00549                 if ( $rt ) {
00550                     $article = Article::newFromTitle( $this->mNewPage, $this->getContext() );
00551                     $out->addHTML( $article->viewRedirect( $rt ) );
00552 
00553                     # WikiPage::getParserOutput() should not return false, but just in case
00554                     if ( $parserOutput ) {
00555                         # Show categories etc.
00556                         $out->addParserOutputNoText( $parserOutput );
00557                     }
00558                 } elseif ( $parserOutput ) {
00559                     $out->addParserOutput( $parserOutput );
00560                 }
00561             }
00562         }
00563         # Add redundant patrol link on bottom...
00564         $out->addHTML( $this->markPatrolledLink() );
00565 
00566         wfProfileOut( __METHOD__ );
00567     }
00568 
00569     protected function getParserOutput( WikiPage $page, Revision $rev ) {
00570         $parserOptions = $page->makeParserOptions( $this->getContext() );
00571 
00572         if ( !$rev->isCurrent() || !$rev->getTitle()->quickUserCan( "edit" ) ) {
00573             $parserOptions->setEditSection( false );
00574         }
00575 
00576         $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
00577         return $parserOutput;
00578     }
00579 
00586     function showDiff( $otitle, $ntitle, $notice = '' ) {
00587         $diff = $this->getDiff( $otitle, $ntitle, $notice );
00588         if ( $diff === false ) {
00589             $this->showMissingRevision();
00590             return false;
00591         } else {
00592             $this->showDiffStyle();
00593             $this->getOutput()->addHTML( $diff );
00594             return true;
00595         }
00596     }
00597 
00601     function showDiffStyle() {
00602         $this->getOutput()->addModuleStyles( 'mediawiki.action.history.diff' );
00603     }
00604 
00613     function getDiff( $otitle, $ntitle, $notice = '' ) {
00614         $body = $this->getDiffBody();
00615         if ( $body === false ) {
00616             return false;
00617         } else {
00618             $multi = $this->getMultiNotice();
00619             // Display a message when the diff is empty
00620             if ( $body === '' ) {
00621                 $notice .= '<div class="mw-diff-empty">' . $this->msg( 'diff-empty' )->parse() . "</div>\n";
00622             }
00623             return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
00624         }
00625     }
00626 
00632     public function getDiffBody() {
00633         global $wgMemc;
00634         wfProfileIn( __METHOD__ );
00635         $this->mCacheHit = true;
00636         // Check if the diff should be hidden from this user
00637         if ( !$this->loadRevisionData() ) {
00638             wfProfileOut( __METHOD__ );
00639             return false;
00640         } elseif ( $this->mOldRev && !$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
00641             wfProfileOut( __METHOD__ );
00642             return false;
00643         } elseif ( $this->mNewRev && !$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
00644             wfProfileOut( __METHOD__ );
00645             return false;
00646         }
00647         // Short-circuit
00648         if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev
00649             && $this->mOldRev->getID() == $this->mNewRev->getID() ) )
00650         {
00651             wfProfileOut( __METHOD__ );
00652             return '';
00653         }
00654         // Cacheable?
00655         $key = false;
00656         if ( $this->mOldid && $this->mNewid ) {
00657             $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION,
00658                 'oldid', $this->mOldid, 'newid', $this->mNewid );
00659             // Try cache
00660             if ( !$this->mRefreshCache ) {
00661                 $difftext = $wgMemc->get( $key );
00662                 if ( $difftext ) {
00663                     wfIncrStats( 'diff_cache_hit' );
00664                     $difftext = $this->localiseLineNumbers( $difftext );
00665                     $difftext .= "\n<!-- diff cache key $key -->\n";
00666                     wfProfileOut( __METHOD__ );
00667                     return $difftext;
00668                 }
00669             } // don't try to load but save the result
00670         }
00671         $this->mCacheHit = false;
00672 
00673         // Loadtext is permission safe, this just clears out the diff
00674         if ( !$this->loadText() ) {
00675             wfProfileOut( __METHOD__ );
00676             return false;
00677         }
00678 
00679         $difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent );
00680 
00681         // Save to cache for 7 days
00682         if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
00683             wfIncrStats( 'diff_uncacheable' );
00684         } elseif ( $key !== false && $difftext !== false ) {
00685             wfIncrStats( 'diff_cache_miss' );
00686             $wgMemc->set( $key, $difftext, 7 * 86400 );
00687         } else {
00688             wfIncrStats( 'diff_uncacheable' );
00689         }
00690         // Replace line numbers with the text in the user's language
00691         if ( $difftext !== false ) {
00692             $difftext = $this->localiseLineNumbers( $difftext );
00693         }
00694         wfProfileOut( __METHOD__ );
00695         return $difftext;
00696     }
00697 
00716     function generateContentDiffBody( Content $old, Content $new ) {
00717         if ( !( $old instanceof TextContent ) ) {
00718             throw new MWException( "Diff not implemented for " . get_class( $old ) . "; "
00719                     . "override generateContentDiffBody to fix this." );
00720         }
00721 
00722         if ( !( $new instanceof TextContent ) ) {
00723             throw new MWException( "Diff not implemented for " . get_class( $new ) . "; "
00724                 . "override generateContentDiffBody to fix this." );
00725         }
00726 
00727         $otext = $old->serialize();
00728         $ntext = $new->serialize();
00729 
00730         return $this->generateTextDiffBody( $otext, $ntext );
00731     }
00732 
00741     function generateDiffBody( $otext, $ntext ) {
00742         ContentHandler::deprecated( __METHOD__, "1.21" );
00743 
00744         return $this->generateTextDiffBody( $otext, $ntext );
00745     }
00746 
00756     function generateTextDiffBody( $otext, $ntext ) {
00757         global $wgExternalDiffEngine, $wgContLang;
00758 
00759         wfProfileIn( __METHOD__ );
00760 
00761         $otext = str_replace( "\r\n", "\n", $otext );
00762         $ntext = str_replace( "\r\n", "\n", $ntext );
00763 
00764         if ( $wgExternalDiffEngine == 'wikidiff' && function_exists( 'wikidiff_do_diff' ) ) {
00765             # For historical reasons, external diff engine expects
00766             # input text to be HTML-escaped already
00767             $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) );
00768             $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) );
00769             wfProfileOut( __METHOD__ );
00770             return $wgContLang->unsegmentForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) .
00771             $this->debug( 'wikidiff1' );
00772         }
00773 
00774         if ( $wgExternalDiffEngine == 'wikidiff2' && function_exists( 'wikidiff2_do_diff' ) ) {
00775             # Better external diff engine, the 2 may some day be dropped
00776             # This one does the escaping and segmenting itself
00777             wfProfileIn( 'wikidiff2_do_diff' );
00778             $text = wikidiff2_do_diff( $otext, $ntext, 2 );
00779             $text .= $this->debug( 'wikidiff2' );
00780             wfProfileOut( 'wikidiff2_do_diff' );
00781             wfProfileOut( __METHOD__ );
00782             return $text;
00783         }
00784         if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) {
00785             # Diff via the shell
00786             $tmpDir = wfTempDir();
00787             $tempName1 = tempnam( $tmpDir, 'diff_' );
00788             $tempName2 = tempnam( $tmpDir, 'diff_' );
00789 
00790             $tempFile1 = fopen( $tempName1, "w" );
00791             if ( !$tempFile1 ) {
00792                 wfProfileOut( __METHOD__ );
00793                 return false;
00794             }
00795             $tempFile2 = fopen( $tempName2, "w" );
00796             if ( !$tempFile2 ) {
00797                 wfProfileOut( __METHOD__ );
00798                 return false;
00799             }
00800             fwrite( $tempFile1, $otext );
00801             fwrite( $tempFile2, $ntext );
00802             fclose( $tempFile1 );
00803             fclose( $tempFile2 );
00804             $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
00805             wfProfileIn( __METHOD__ . "-shellexec" );
00806             $difftext = wfShellExec( $cmd );
00807             $difftext .= $this->debug( "external $wgExternalDiffEngine" );
00808             wfProfileOut( __METHOD__ . "-shellexec" );
00809             unlink( $tempName1 );
00810             unlink( $tempName2 );
00811             wfProfileOut( __METHOD__ );
00812             return $difftext;
00813         }
00814 
00815         # Native PHP diff
00816         $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
00817         $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
00818         $diffs = new Diff( $ota, $nta );
00819         $formatter = new TableDiffFormatter();
00820         $difftext = $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) .
00821         wfProfileOut( __METHOD__ );
00822         return $difftext;
00823     }
00824 
00830     protected function debug( $generator = "internal" ) {
00831         global $wgShowHostnames;
00832         if ( !$this->enableDebugComment ) {
00833             return '';
00834         }
00835         $data = array( $generator );
00836         if ( $wgShowHostnames ) {
00837             $data[] = wfHostname();
00838         }
00839         $data[] = wfTimestamp( TS_DB );
00840         return "<!-- diff generator: " .
00841         implode( " ",
00842         array_map(
00843                     "htmlspecialchars",
00844         $data ) ) .
00845             " -->\n";
00846     }
00847 
00852     function localiseLineNumbers( $text ) {
00853         return preg_replace_callback( '/<!--LINE (\d+)-->/',
00854         array( &$this, 'localiseLineNumbersCb' ), $text );
00855     }
00856 
00857     function localiseLineNumbersCb( $matches ) {
00858         if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
00859             return '';
00860         }
00861         return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
00862     }
00863 
00868     function getMultiNotice() {
00869         if ( !is_object( $this->mOldRev ) || !is_object( $this->mNewRev ) ) {
00870             return '';
00871         } elseif ( !$this->mOldPage->equals( $this->mNewPage ) ) {
00872             // Comparing two different pages? Count would be meaningless.
00873             return '';
00874         }
00875 
00876         if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) {
00877             $oldRev = $this->mNewRev; // flip
00878             $newRev = $this->mOldRev; // flip
00879         } else { // normal case
00880             $oldRev = $this->mOldRev;
00881             $newRev = $this->mNewRev;
00882         }
00883 
00884         $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev );
00885         if ( $nEdits > 0 ) {
00886             $limit = 100; // use diff-multi-manyusers if too many users
00887             $numUsers = $this->mNewPage->countAuthorsBetween( $oldRev, $newRev, $limit );
00888             return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
00889         }
00890         return ''; // nothing
00891     }
00892 
00900     public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
00901         if ( $numUsers > $limit ) {
00902             $msg = 'diff-multi-manyusers';
00903             $numUsers = $limit;
00904         } else {
00905             $msg = 'diff-multi';
00906         }
00907         return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
00908     }
00909 
00918     protected function getRevisionHeader( Revision $rev, $complete = '' ) {
00919         $lang = $this->getLanguage();
00920         $user = $this->getUser();
00921         $revtimestamp = $rev->getTimestamp();
00922         $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
00923         $dateofrev = $lang->userDate( $revtimestamp, $user );
00924         $timeofrev = $lang->userTime( $revtimestamp, $user );
00925 
00926         $header = $this->msg(
00927             $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
00928             $timestamp,
00929             $dateofrev,
00930             $timeofrev
00931         )->escaped();
00932 
00933         if ( $complete !== 'complete' ) {
00934             return $header;
00935         }
00936 
00937         $title = $rev->getTitle();
00938 
00939         $header = Linker::linkKnown( $title, $header, array(),
00940             array( 'oldid' => $rev->getID() ) );
00941 
00942         if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) {
00943             $editQuery = array( 'action' => 'edit' );
00944             if ( !$rev->isCurrent() ) {
00945                 $editQuery['oldid'] = $rev->getID();
00946             }
00947 
00948             $msg = $this->msg( $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold' )->escaped();
00949             $header .= ' ' . $this->msg( 'parentheses' )->rawParams(
00950                 Linker::linkKnown( $title, $msg, array(), $editQuery ) )->plain();
00951             if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
00952                 $header = Html::rawElement( 'span', array( 'class' => 'history-deleted' ), $header );
00953             }
00954         } else {
00955             $header = Html::rawElement( 'span', array( 'class' => 'history-deleted' ), $header );
00956         }
00957 
00958         return $header;
00959     }
00960 
00966     function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
00967         // shared.css sets diff in interface language/dir, but the actual content
00968         // is often in a different language, mostly the page content language/dir
00969         $tableClass = 'diff diff-contentalign-' . htmlspecialchars( $this->getDiffLang()->alignStart() );
00970         $header = "<table class='$tableClass'>";
00971 
00972         if ( !$diff && !$otitle ) {
00973             $header .= "
00974             <tr style='vertical-align: top;'>
00975             <td class='diff-ntitle'>{$ntitle}</td>
00976             </tr>";
00977             $multiColspan = 1;
00978         } else {
00979             if ( $diff ) { // Safari/Chrome show broken output if cols not used
00980                 $header .= "
00981                 <col class='diff-marker' />
00982                 <col class='diff-content' />
00983                 <col class='diff-marker' />
00984                 <col class='diff-content' />";
00985                 $colspan = 2;
00986                 $multiColspan = 4;
00987             } else {
00988                 $colspan = 1;
00989                 $multiColspan = 2;
00990             }
00991             if ( $otitle || $ntitle ) {
00992                 $header .= "
00993                 <tr style='vertical-align: top;'>
00994                 <td colspan='$colspan' class='diff-otitle'>{$otitle}</td>
00995                 <td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td>
00996                 </tr>";
00997             }
00998         }
00999 
01000         if ( $multi != '' ) {
01001             $header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;' class='diff-multi'>{$multi}</td></tr>";
01002         }
01003         if ( $notice != '' ) {
01004             $header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;'>{$notice}</td></tr>";
01005         }
01006 
01007         return $header . $diff . "</table>";
01008     }
01009 
01014     function setText( $oldText, $newText ) {
01015         ContentHandler::deprecated( __METHOD__, "1.21" );
01016 
01017         $oldContent = ContentHandler::makeContent( $oldText, $this->getTitle() );
01018         $newContent = ContentHandler::makeContent( $newText, $this->getTitle() );
01019 
01020         $this->setContent( $oldContent, $newContent );
01021     }
01022 
01027     function setContent( Content $oldContent, Content $newContent ) {
01028         $this->mOldContent = $oldContent;
01029         $this->mNewContent = $newContent;
01030 
01031         $this->mTextLoaded = 2;
01032         $this->mRevisionsLoaded = true;
01033     }
01034 
01040     function setTextLanguage( $lang ) {
01041         $this->mDiffLang = wfGetLangObj( $lang );
01042     }
01043 
01047     private function loadRevisionIds() {
01048         if ( $this->mRevisionsIdsLoaded ) {
01049             return;
01050         }
01051 
01052         $this->mRevisionsIdsLoaded = true;
01053 
01054         $old = $this->mOldid;
01055         $new = $this->mNewid;
01056 
01057         if ( $new === 'prev' ) {
01058             # Show diff between revision $old and the previous one.
01059             # Get previous one from DB.
01060             $this->mNewid = intval( $old );
01061             $this->mOldid = $this->getTitle()->getPreviousRevisionID( $this->mNewid );
01062         } elseif ( $new === 'next' ) {
01063             # Show diff between revision $old and the next one.
01064             # Get next one from DB.
01065             $this->mOldid = intval( $old );
01066             $this->mNewid = $this->getTitle()->getNextRevisionID( $this->mOldid );
01067             if ( $this->mNewid === false ) {
01068                 # if no result, NewId points to the newest old revision. The only newer
01069                 # revision is cur, which is "0".
01070                 $this->mNewid = 0;
01071             }
01072         } else {
01073             $this->mOldid = intval( $old );
01074             $this->mNewid = intval( $new );
01075             wfRunHooks( 'NewDifferenceEngine', array( $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ) );
01076         }
01077     }
01078 
01091     function loadRevisionData() {
01092         if ( $this->mRevisionsLoaded ) {
01093             return true;
01094         }
01095 
01096         // Whether it succeeds or fails, we don't want to try again
01097         $this->mRevisionsLoaded = true;
01098 
01099         $this->loadRevisionIds();
01100 
01101         // Load the new revision object
01102         $this->mNewRev = $this->mNewid
01103             ? Revision::newFromId( $this->mNewid )
01104             : Revision::newFromTitle( $this->getTitle(), false, Revision::READ_NORMAL );
01105 
01106         if ( !$this->mNewRev instanceof Revision ) {
01107             return false;
01108         }
01109 
01110         // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
01111         $this->mNewid = $this->mNewRev->getId();
01112         $this->mNewPage = $this->mNewRev->getTitle();
01113 
01114         // Load the old revision object
01115         $this->mOldRev = false;
01116         if ( $this->mOldid ) {
01117             $this->mOldRev = Revision::newFromId( $this->mOldid );
01118         } elseif ( $this->mOldid === 0 ) {
01119             $rev = $this->mNewRev->getPrevious();
01120             if ( $rev ) {
01121                 $this->mOldid = $rev->getId();
01122                 $this->mOldRev = $rev;
01123             } else {
01124                 // No previous revision; mark to show as first-version only.
01125                 $this->mOldid = false;
01126                 $this->mOldRev = false;
01127             }
01128         } /* elseif ( $this->mOldid === false ) leave mOldRev false; */
01129 
01130         if ( is_null( $this->mOldRev ) ) {
01131             return false;
01132         }
01133 
01134         if ( $this->mOldRev ) {
01135             $this->mOldPage = $this->mOldRev->getTitle();
01136         }
01137 
01138         // Load tags information for both revisions
01139         $dbr = wfGetDB( DB_SLAVE );
01140         if ( $this->mOldid !== false ) {
01141             $this->mOldTags = $dbr->selectField(
01142                 'tag_summary',
01143                 'ts_tags',
01144                 array( 'ts_rev_id' => $this->mOldid ),
01145                 __METHOD__
01146             );
01147         } else {
01148             $this->mOldTags = false;
01149         }
01150         $this->mNewTags = $dbr->selectField(
01151             'tag_summary',
01152             'ts_tags',
01153             array( 'ts_rev_id' => $this->mNewid ),
01154             __METHOD__
01155         );
01156 
01157         return true;
01158     }
01159 
01165     function loadText() {
01166         if ( $this->mTextLoaded == 2 ) {
01167             return true;
01168         }
01169 
01170         // Whether it succeeds or fails, we don't want to try again
01171         $this->mTextLoaded = 2;
01172 
01173         if ( !$this->loadRevisionData() ) {
01174             return false;
01175         }
01176 
01177         if ( $this->mOldRev ) {
01178             $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
01179             if ( $this->mOldContent === null ) {
01180                 return false;
01181             }
01182         }
01183 
01184         if ( $this->mNewRev ) {
01185             $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
01186             if ( $this->mNewContent === null ) {
01187                 return false;
01188             }
01189         }
01190 
01191         return true;
01192     }
01193 
01199     function loadNewText() {
01200         if ( $this->mTextLoaded >= 1 ) {
01201             return true;
01202         }
01203 
01204         $this->mTextLoaded = 1;
01205 
01206         if ( !$this->loadRevisionData() ) {
01207             return false;
01208         }
01209 
01210         $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
01211 
01212         return true;
01213     }
01214 }