MediaWiki
REL1_22
|
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 = '   ' . $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 = ' '; 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 = ' '; 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 = '   ' . $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 }