MediaWiki
REL1_23
|
00001 <?php 00030 define( 'MW_DIFF_VERSION', '1.11a' ); 00031 00036 class DifferenceEngine extends ContextSource { 00037 00039 public $mOldid; 00040 00042 public $mNewid; 00043 00044 private $mOldTags; 00045 private $mNewTags; 00046 00048 public $mOldContent; 00049 00051 public $mNewContent; 00052 00054 protected $mDiffLang; 00055 00057 public $mOldPage; 00058 00060 public $mNewPage; 00061 00063 public $mOldRev; 00064 00066 public $mNewRev; 00067 00069 private $mRevisionsIdsLoaded = false; 00070 00072 public $mRevisionsLoaded = false; 00073 00075 public $mTextLoaded = 0; 00076 00078 public $mCacheHit = false; 00079 00085 public $enableDebugComment = false; 00086 00090 protected $mReducedLineNumbers = false; 00091 00093 protected $mMarkPatrolledLink = null; 00094 00096 protected $unhide = false; 00108 public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0, 00109 $refreshCache = false, $unhide = false 00110 ) { 00111 if ( $context instanceof IContextSource ) { 00112 $this->setContext( $context ); 00113 } 00114 00115 wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'\n" ); 00116 00117 $this->mOldid = $old; 00118 $this->mNewid = $new; 00119 $this->mRefreshCache = $refreshCache; 00120 $this->unhide = $unhide; 00121 } 00122 00126 public function setReducedLineNumbers( $value = true ) { 00127 $this->mReducedLineNumbers = $value; 00128 } 00129 00133 public function getDiffLang() { 00134 if ( $this->mDiffLang === null ) { 00135 # Default language in which the diff text is written. 00136 $this->mDiffLang = $this->getTitle()->getPageLanguage(); 00137 } 00138 00139 return $this->mDiffLang; 00140 } 00141 00145 public function wasCacheHit() { 00146 return $this->mCacheHit; 00147 } 00148 00152 public function getOldid() { 00153 $this->loadRevisionIds(); 00154 00155 return $this->mOldid; 00156 } 00157 00161 public function getNewid() { 00162 $this->loadRevisionIds(); 00163 00164 return $this->mNewid; 00165 } 00166 00175 public function deletedLink( $id ) { 00176 if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) { 00177 $dbr = wfGetDB( DB_SLAVE ); 00178 $row = $dbr->selectRow( 'archive', '*', 00179 array( 'ar_rev_id' => $id ), 00180 __METHOD__ ); 00181 if ( $row ) { 00182 $rev = Revision::newFromArchiveRow( $row ); 00183 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title ); 00184 00185 return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( array( 00186 'target' => $title->getPrefixedText(), 00187 'timestamp' => $rev->getTimestamp() 00188 ) ); 00189 } 00190 } 00191 00192 return false; 00193 } 00194 00202 public function deletedIdMarker( $id ) { 00203 $link = $this->deletedLink( $id ); 00204 if ( $link ) { 00205 return "[$link $id]"; 00206 } else { 00207 return $id; 00208 } 00209 } 00210 00211 private function showMissingRevision() { 00212 $out = $this->getOutput(); 00213 00214 $missing = array(); 00215 if ( $this->mOldRev === null || 00216 ( $this->mOldRev && $this->mOldContent === null ) 00217 ) { 00218 $missing[] = $this->deletedIdMarker( $this->mOldid ); 00219 } 00220 if ( $this->mNewRev === null || 00221 ( $this->mNewRev && $this->mNewContent === null ) 00222 ) { 00223 $missing[] = $this->deletedIdMarker( $this->mNewid ); 00224 } 00225 00226 $out->setPageTitle( $this->msg( 'errorpagetitle' ) ); 00227 $out->addWikiMsg( 'difference-missing-revision', 00228 $this->getLanguage()->listToText( $missing ), count( $missing ) ); 00229 } 00230 00231 public function showDiffPage( $diffOnly = false ) { 00232 wfProfileIn( __METHOD__ ); 00233 00234 # Allow frames except in certain special cases 00235 $out = $this->getOutput(); 00236 $out->allowClickjacking(); 00237 $out->setRobotPolicy( 'noindex,nofollow' ); 00238 00239 if ( !$this->loadRevisionData() ) { 00240 $this->showMissingRevision(); 00241 wfProfileOut( __METHOD__ ); 00242 00243 return; 00244 } 00245 00246 $user = $this->getUser(); 00247 $permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user ); 00248 if ( $this->mOldPage ) { # mOldPage might not be set, see below. 00249 $permErrors = wfMergeErrorArrays( $permErrors, 00250 $this->mOldPage->getUserPermissionsErrors( 'read', $user ) ); 00251 } 00252 if ( count( $permErrors ) ) { 00253 wfProfileOut( __METHOD__ ); 00254 throw new PermissionsError( 'read', $permErrors ); 00255 } 00256 00257 $rollback = ''; 00258 00259 $query = array(); 00260 # Carry over 'diffonly' param via navigation links 00261 if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) { 00262 $query['diffonly'] = $diffOnly; 00263 } 00264 # Cascade unhide param in links for easy deletion browsing 00265 if ( $this->unhide ) { 00266 $query['unhide'] = 1; 00267 } 00268 00269 # Check if one of the revisions is deleted/suppressed 00270 $deleted = $suppressed = false; 00271 $allowed = $this->mNewRev->userCan( Revision::DELETED_TEXT, $user ); 00272 00273 $revisionTools = array(); 00274 00275 # mOldRev is false if the difference engine is called with a "vague" query for 00276 # a diff between a version V and its previous version V' AND the version V 00277 # is the first version of that article. In that case, V' does not exist. 00278 if ( $this->mOldRev === false ) { 00279 $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) ); 00280 $samePage = true; 00281 $oldHeader = ''; 00282 } else { 00283 wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) ); 00284 00285 if ( $this->mNewPage->equals( $this->mOldPage ) ) { 00286 $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) ); 00287 $samePage = true; 00288 } else { 00289 $out->setPageTitle( $this->msg( 'difference-title-multipage', 00290 $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) ); 00291 $out->addSubtitle( $this->msg( 'difference-multipage' ) ); 00292 $samePage = false; 00293 } 00294 00295 if ( $samePage && $this->mNewPage->quickUserCan( 'edit', $user ) ) { 00296 if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) { 00297 $rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext() ); 00298 if ( $rollbackLink ) { 00299 $out->preventClickjacking(); 00300 $rollback = '   ' . $rollbackLink; 00301 } 00302 } 00303 00304 if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && 00305 !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) 00306 ) { 00307 $undoLink = Html::element( 'a', array( 00308 'href' => $this->mNewPage->getLocalURL( array( 00309 'action' => 'edit', 00310 'undoafter' => $this->mOldid, 00311 'undo' => $this->mNewid 00312 ) ), 00313 'title' => Linker::titleAttrib( 'undo' ) 00314 ), 00315 $this->msg( 'editundo' )->text() 00316 ); 00317 $revisionTools[] = $undoLink; 00318 } 00319 } 00320 00321 # Make "previous revision link" 00322 if ( $samePage && $this->mOldRev->getPrevious() ) { 00323 $prevlink = Linker::linkKnown( 00324 $this->mOldPage, 00325 $this->msg( 'previousdiff' )->escaped(), 00326 array( 'id' => 'differences-prevlink' ), 00327 array( 'diff' => 'prev', 'oldid' => $this->mOldid ) + $query 00328 ); 00329 } else { 00330 $prevlink = ' '; 00331 } 00332 00333 if ( $this->mOldRev->isMinor() ) { 00334 $oldminor = ChangesList::flag( 'minor' ); 00335 } else { 00336 $oldminor = ''; 00337 } 00338 00339 $ldel = $this->revisionDeleteLink( $this->mOldRev ); 00340 $oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' ); 00341 $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff' ); 00342 00343 $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' . 00344 '<div id="mw-diff-otitle2">' . 00345 Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' . 00346 '<div id="mw-diff-otitle3">' . $oldminor . 00347 Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' . 00348 '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' . 00349 '<div id="mw-diff-otitle4">' . $prevlink . '</div>'; 00350 00351 if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) { 00352 $deleted = true; // old revisions text is hidden 00353 if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) { 00354 $suppressed = true; // also suppressed 00355 } 00356 } 00357 00358 # Check if this user can see the revisions 00359 if ( !$this->mOldRev->userCan( Revision::DELETED_TEXT, $user ) ) { 00360 $allowed = false; 00361 } 00362 } 00363 00364 # Make "next revision link" 00365 # Skip next link on the top revision 00366 if ( $samePage && !$this->mNewRev->isCurrent() ) { 00367 $nextlink = Linker::linkKnown( 00368 $this->mNewPage, 00369 $this->msg( 'nextdiff' )->escaped(), 00370 array( 'id' => 'differences-nextlink' ), 00371 array( 'diff' => 'next', 'oldid' => $this->mNewid ) + $query 00372 ); 00373 } else { 00374 $nextlink = ' '; 00375 } 00376 00377 if ( $this->mNewRev->isMinor() ) { 00378 $newminor = ChangesList::flag( 'minor' ); 00379 } else { 00380 $newminor = ''; 00381 } 00382 00383 # Handle RevisionDelete links... 00384 $rdel = $this->revisionDeleteLink( $this->mNewRev ); 00385 00386 # Allow extensions to define their own revision tools 00387 wfRunHooks( 'DiffRevisionTools', array( $this->mNewRev, &$revisionTools, $this->mOldRev ) ); 00388 $formattedRevisionTools = array(); 00389 // Put each one in parentheses (poor man's button) 00390 foreach ( $revisionTools as $tool ) { 00391 $formattedRevisionTools[] = $this->msg( 'parentheses' )->rawParams( $tool )->escaped(); 00392 } 00393 $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) . 00394 ' ' . implode( ' ', $formattedRevisionTools ); 00395 $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff' ); 00396 00397 $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' . 00398 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) . 00399 " $rollback</div>" . 00400 '<div id="mw-diff-ntitle3">' . $newminor . 00401 Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' . 00402 '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' . 00403 '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>'; 00404 00405 if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) { 00406 $deleted = true; // new revisions text is hidden 00407 if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) { 00408 $suppressed = true; // also suppressed 00409 } 00410 } 00411 00412 # If the diff cannot be shown due to a deleted revision, then output 00413 # the diff header and links to unhide (if available)... 00414 if ( $deleted && ( !$this->unhide || !$allowed ) ) { 00415 $this->showDiffStyle(); 00416 $multi = $this->getMultiNotice(); 00417 $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) ); 00418 if ( !$allowed ) { 00419 $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff'; 00420 # Give explanation for why revision is not visible 00421 $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n", 00422 array( $msg ) ); 00423 } else { 00424 # Give explanation and add a link to view the diff... 00425 $query = $this->getRequest()->appendQueryValue( 'unhide', '1', true ); 00426 $link = $this->getTitle()->getFullURL( $query ); 00427 $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff'; 00428 $out->wrapWikiMsg( 00429 "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n", 00430 array( $msg, $link ) 00431 ); 00432 } 00433 # Otherwise, output a regular diff... 00434 } else { 00435 # Add deletion notice if the user is viewing deleted content 00436 $notice = ''; 00437 if ( $deleted ) { 00438 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view'; 00439 $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" . 00440 $this->msg( $msg )->parse() . 00441 "</div>\n"; 00442 } 00443 $this->showDiff( $oldHeader, $newHeader, $notice ); 00444 if ( !$diffOnly ) { 00445 $this->renderNewRevision(); 00446 } 00447 } 00448 wfProfileOut( __METHOD__ ); 00449 } 00450 00459 protected function markPatrolledLink() { 00460 global $wgUseRCPatrol, $wgEnableAPI, $wgEnableWriteAPI; 00461 $user = $this->getUser(); 00462 00463 if ( $this->mMarkPatrolledLink === null ) { 00464 // Prepare a change patrol link, if applicable 00465 if ( 00466 // Is patrolling enabled and the user allowed to? 00467 $wgUseRCPatrol && $this->mNewPage->quickUserCan( 'patrol', $user ) && 00468 // Only do this if the revision isn't more than 6 hours older 00469 // than the Max RC age (6h because the RC might not be cleaned out regularly) 00470 RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 ) 00471 ) { 00472 // Look for an unpatrolled change corresponding to this diff 00473 00474 $db = wfGetDB( DB_SLAVE ); 00475 $change = RecentChange::newFromConds( 00476 array( 00477 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ), 00478 'rc_this_oldid' => $this->mNewid, 00479 'rc_patrolled' => 0 00480 ), 00481 __METHOD__, 00482 array( 'USE INDEX' => 'rc_timestamp' ) 00483 ); 00484 00485 if ( $change && $change->getPerformer()->getName() !== $user->getName() ) { 00486 $rcid = $change->getAttribute( 'rc_id' ); 00487 } else { 00488 // None found or the page has been created by the current user. 00489 // If the user could patrol this it already would be patrolled 00490 $rcid = 0; 00491 } 00492 // Build the link 00493 if ( $rcid ) { 00494 $this->getOutput()->preventClickjacking(); 00495 if ( $wgEnableAPI && $wgEnableWriteAPI 00496 && $user->isAllowed( 'writeapi' ) 00497 ) { 00498 $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' ); 00499 } 00500 00501 $token = $user->getEditToken( $rcid ); 00502 $this->mMarkPatrolledLink = ' <span class="patrollink">[' . Linker::linkKnown( 00503 $this->mNewPage, 00504 $this->msg( 'markaspatrolleddiff' )->escaped(), 00505 array(), 00506 array( 00507 'action' => 'markpatrolled', 00508 'rcid' => $rcid, 00509 'token' => $token, 00510 ) 00511 ) . ']</span>'; 00512 } else { 00513 $this->mMarkPatrolledLink = ''; 00514 } 00515 } else { 00516 $this->mMarkPatrolledLink = ''; 00517 } 00518 } 00519 00520 return $this->mMarkPatrolledLink; 00521 } 00522 00528 protected function revisionDeleteLink( $rev ) { 00529 $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() ); 00530 if ( $link !== '' ) { 00531 $link = '   ' . $link . ' '; 00532 } 00533 00534 return $link; 00535 } 00536 00540 public function renderNewRevision() { 00541 wfProfileIn( __METHOD__ ); 00542 $out = $this->getOutput(); 00543 $revHeader = $this->getRevisionHeader( $this->mNewRev ); 00544 # Add "current version as of X" title 00545 $out->addHTML( "<hr class='diff-hr' /> 00546 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" ); 00547 # Page content may be handled by a hooked call instead... 00548 # @codingStandardsIgnoreStart Ignoring long lines. 00549 if ( wfRunHooks( 'ArticleContentOnDiff', array( $this, $out ) ) ) { 00550 $this->loadNewText(); 00551 $out->setRevisionId( $this->mNewid ); 00552 $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() ); 00553 $out->setArticleFlag( true ); 00554 00555 // NOTE: only needed for B/C: custom rendering of JS/CSS via hook 00556 if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) { 00557 // Stolen from Article::view --AG 2007-10-11 00558 // Give hooks a chance to customise the output 00559 // @todo standardize this crap into one function 00560 if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->mNewContent, $this->mNewPage, $out ) ) ) { 00561 // NOTE: deprecated hook, B/C only 00562 // use the content object's own rendering 00563 $cnt = $this->mNewRev->getContent(); 00564 $po = $cnt ? $cnt->getParserOutput( $this->mNewRev->getTitle(), $this->mNewRev->getId() ) : null; 00565 $txt = $po ? $po->getText() : ''; 00566 $out->addHTML( $txt ); 00567 } 00568 } elseif ( !wfRunHooks( 'ArticleContentViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) { 00569 // Handled by extension 00570 } elseif ( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) { 00571 // NOTE: deprecated hook, B/C only 00572 // Handled by extension 00573 } else { 00574 // Normal page 00575 if ( $this->getTitle()->equals( $this->mNewPage ) ) { 00576 // If the Title stored in the context is the same as the one 00577 // of the new revision, we can use its associated WikiPage 00578 // object. 00579 $wikiPage = $this->getWikiPage(); 00580 } else { 00581 // Otherwise we need to create our own WikiPage object 00582 $wikiPage = WikiPage::factory( $this->mNewPage ); 00583 } 00584 00585 $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev ); 00586 00587 # Also try to load it as a redirect 00588 $rt = $this->mNewContent ? $this->mNewContent->getRedirectTarget() : null; 00589 00590 if ( $rt ) { 00591 $article = Article::newFromTitle( $this->mNewPage, $this->getContext() ); 00592 $out->addHTML( $article->viewRedirect( $rt ) ); 00593 00594 # WikiPage::getParserOutput() should not return false, but just in case 00595 if ( $parserOutput ) { 00596 # Show categories etc. 00597 $out->addParserOutputNoText( $parserOutput ); 00598 } 00599 } elseif ( $parserOutput ) { 00600 $out->addParserOutput( $parserOutput ); 00601 } 00602 } 00603 } 00604 # @codingStandardsIgnoreEnd 00605 00606 # Add redundant patrol link on bottom... 00607 $out->addHTML( $this->markPatrolledLink() ); 00608 00609 wfProfileOut( __METHOD__ ); 00610 } 00611 00612 protected function getParserOutput( WikiPage $page, Revision $rev ) { 00613 $parserOptions = $page->makeParserOptions( $this->getContext() ); 00614 00615 if ( !$rev->isCurrent() || !$rev->getTitle()->quickUserCan( "edit" ) ) { 00616 $parserOptions->setEditSection( false ); 00617 } 00618 00619 $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() ); 00620 00621 return $parserOutput; 00622 } 00623 00634 public function showDiff( $otitle, $ntitle, $notice = '' ) { 00635 $diff = $this->getDiff( $otitle, $ntitle, $notice ); 00636 if ( $diff === false ) { 00637 $this->showMissingRevision(); 00638 00639 return false; 00640 } else { 00641 $this->showDiffStyle(); 00642 $this->getOutput()->addHTML( $diff ); 00643 00644 return true; 00645 } 00646 } 00647 00651 public function showDiffStyle() { 00652 $this->getOutput()->addModuleStyles( 'mediawiki.action.history.diff' ); 00653 } 00654 00664 public function getDiff( $otitle, $ntitle, $notice = '' ) { 00665 $body = $this->getDiffBody(); 00666 if ( $body === false ) { 00667 return false; 00668 } 00669 00670 $multi = $this->getMultiNotice(); 00671 // Display a message when the diff is empty 00672 if ( $body === '' ) { 00673 $notice .= '<div class="mw-diff-empty">' . 00674 $this->msg( 'diff-empty' )->parse() . 00675 "</div>\n"; 00676 } 00677 00678 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice ); 00679 } 00680 00686 public function getDiffBody() { 00687 global $wgMemc; 00688 wfProfileIn( __METHOD__ ); 00689 $this->mCacheHit = true; 00690 // Check if the diff should be hidden from this user 00691 if ( !$this->loadRevisionData() ) { 00692 wfProfileOut( __METHOD__ ); 00693 00694 return false; 00695 } elseif ( $this->mOldRev && 00696 !$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) 00697 ) { 00698 wfProfileOut( __METHOD__ ); 00699 00700 return false; 00701 } elseif ( $this->mNewRev && 00702 !$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) 00703 ) { 00704 wfProfileOut( __METHOD__ ); 00705 00706 return false; 00707 } 00708 // Short-circuit 00709 if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev 00710 && $this->mOldRev->getID() == $this->mNewRev->getID() ) 00711 ) { 00712 wfProfileOut( __METHOD__ ); 00713 00714 return ''; 00715 } 00716 // Cacheable? 00717 $key = false; 00718 if ( $this->mOldid && $this->mNewid ) { 00719 $key = $this->getDiffBodyCacheKey(); 00720 00721 // Try cache 00722 if ( !$this->mRefreshCache ) { 00723 $difftext = $wgMemc->get( $key ); 00724 if ( $difftext ) { 00725 wfIncrStats( 'diff_cache_hit' ); 00726 $difftext = $this->localiseLineNumbers( $difftext ); 00727 $difftext .= "\n<!-- diff cache key $key -->\n"; 00728 wfProfileOut( __METHOD__ ); 00729 00730 return $difftext; 00731 } 00732 } // don't try to load but save the result 00733 } 00734 $this->mCacheHit = false; 00735 00736 // Loadtext is permission safe, this just clears out the diff 00737 if ( !$this->loadText() ) { 00738 wfProfileOut( __METHOD__ ); 00739 00740 return false; 00741 } 00742 00743 $difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent ); 00744 00745 // Save to cache for 7 days 00746 if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) { 00747 wfIncrStats( 'diff_uncacheable' ); 00748 } elseif ( $key !== false && $difftext !== false ) { 00749 wfIncrStats( 'diff_cache_miss' ); 00750 $wgMemc->set( $key, $difftext, 7 * 86400 ); 00751 } else { 00752 wfIncrStats( 'diff_uncacheable' ); 00753 } 00754 // Replace line numbers with the text in the user's language 00755 if ( $difftext !== false ) { 00756 $difftext = $this->localiseLineNumbers( $difftext ); 00757 } 00758 wfProfileOut( __METHOD__ ); 00759 00760 return $difftext; 00761 } 00762 00771 protected function getDiffBodyCacheKey() { 00772 if ( !$this->mOldid || !$this->mNewid ) { 00773 throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' ); 00774 } 00775 00776 return wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 00777 'oldid', $this->mOldid, 'newid', $this->mNewid ); 00778 } 00779 00799 public function generateContentDiffBody( Content $old, Content $new ) { 00800 if ( !( $old instanceof TextContent ) ) { 00801 throw new MWException( "Diff not implemented for " . get_class( $old ) . "; " . 00802 "override generateContentDiffBody to fix this." ); 00803 } 00804 00805 if ( !( $new instanceof TextContent ) ) { 00806 throw new MWException( "Diff not implemented for " . get_class( $new ) . "; " 00807 . "override generateContentDiffBody to fix this." ); 00808 } 00809 00810 $otext = $old->serialize(); 00811 $ntext = $new->serialize(); 00812 00813 return $this->generateTextDiffBody( $otext, $ntext ); 00814 } 00815 00825 public function generateDiffBody( $otext, $ntext ) { 00826 ContentHandler::deprecated( __METHOD__, "1.21" ); 00827 00828 return $this->generateTextDiffBody( $otext, $ntext ); 00829 } 00830 00841 public function generateTextDiffBody( $otext, $ntext ) { 00842 global $wgExternalDiffEngine, $wgContLang; 00843 00844 wfProfileIn( __METHOD__ ); 00845 00846 $otext = str_replace( "\r\n", "\n", $otext ); 00847 $ntext = str_replace( "\r\n", "\n", $ntext ); 00848 00849 if ( $wgExternalDiffEngine == 'wikidiff' && function_exists( 'wikidiff_do_diff' ) ) { 00850 # For historical reasons, external diff engine expects 00851 # input text to be HTML-escaped already 00852 $otext = htmlspecialchars( $wgContLang->segmentForDiff( $otext ) ); 00853 $ntext = htmlspecialchars( $wgContLang->segmentForDiff( $ntext ) ); 00854 wfProfileOut( __METHOD__ ); 00855 00856 return $wgContLang->unsegmentForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) . 00857 $this->debug( 'wikidiff1' ); 00858 } 00859 00860 if ( $wgExternalDiffEngine == 'wikidiff2' && function_exists( 'wikidiff2_do_diff' ) ) { 00861 # Better external diff engine, the 2 may some day be dropped 00862 # This one does the escaping and segmenting itself 00863 wfProfileIn( 'wikidiff2_do_diff' ); 00864 $text = wikidiff2_do_diff( $otext, $ntext, 2 ); 00865 $text .= $this->debug( 'wikidiff2' ); 00866 wfProfileOut( 'wikidiff2_do_diff' ); 00867 wfProfileOut( __METHOD__ ); 00868 00869 return $text; 00870 } 00871 if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) { 00872 # Diff via the shell 00873 $tmpDir = wfTempDir(); 00874 $tempName1 = tempnam( $tmpDir, 'diff_' ); 00875 $tempName2 = tempnam( $tmpDir, 'diff_' ); 00876 00877 $tempFile1 = fopen( $tempName1, "w" ); 00878 if ( !$tempFile1 ) { 00879 wfProfileOut( __METHOD__ ); 00880 00881 return false; 00882 } 00883 $tempFile2 = fopen( $tempName2, "w" ); 00884 if ( !$tempFile2 ) { 00885 wfProfileOut( __METHOD__ ); 00886 00887 return false; 00888 } 00889 fwrite( $tempFile1, $otext ); 00890 fwrite( $tempFile2, $ntext ); 00891 fclose( $tempFile1 ); 00892 fclose( $tempFile2 ); 00893 $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 ); 00894 wfProfileIn( __METHOD__ . "-shellexec" ); 00895 $difftext = wfShellExec( $cmd ); 00896 $difftext .= $this->debug( "external $wgExternalDiffEngine" ); 00897 wfProfileOut( __METHOD__ . "-shellexec" ); 00898 unlink( $tempName1 ); 00899 unlink( $tempName2 ); 00900 wfProfileOut( __METHOD__ ); 00901 00902 return $difftext; 00903 } 00904 00905 # Native PHP diff 00906 $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) ); 00907 $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) ); 00908 $diffs = new Diff( $ota, $nta ); 00909 $formatter = new TableDiffFormatter(); 00910 $difftext = $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) . 00911 wfProfileOut( __METHOD__ ); 00912 00913 return $difftext; 00914 } 00915 00924 protected function debug( $generator = "internal" ) { 00925 global $wgShowHostnames; 00926 if ( !$this->enableDebugComment ) { 00927 return ''; 00928 } 00929 $data = array( $generator ); 00930 if ( $wgShowHostnames ) { 00931 $data[] = wfHostname(); 00932 } 00933 $data[] = wfTimestamp( TS_DB ); 00934 00935 return "<!-- diff generator: " . 00936 implode( " ", array_map( "htmlspecialchars", $data ) ) . 00937 " -->\n"; 00938 } 00939 00947 public function localiseLineNumbers( $text ) { 00948 return preg_replace_callback( 00949 '/<!--LINE (\d+)-->/', 00950 array( &$this, 'localiseLineNumbersCb' ), 00951 $text 00952 ); 00953 } 00954 00955 public function localiseLineNumbersCb( $matches ) { 00956 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) { 00957 return ''; 00958 } 00959 00960 return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped(); 00961 } 00962 00968 public function getMultiNotice() { 00969 if ( !is_object( $this->mOldRev ) || !is_object( $this->mNewRev ) ) { 00970 return ''; 00971 } elseif ( !$this->mOldPage->equals( $this->mNewPage ) ) { 00972 // Comparing two different pages? Count would be meaningless. 00973 return ''; 00974 } 00975 00976 if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) { 00977 $oldRev = $this->mNewRev; // flip 00978 $newRev = $this->mOldRev; // flip 00979 } else { // normal case 00980 $oldRev = $this->mOldRev; 00981 $newRev = $this->mNewRev; 00982 } 00983 00984 // Sanity: don't show the notice if too many rows must be scanned 00985 // @TODO: show some special message for that case 00986 $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev, 1000 ); 00987 if ( $nEdits > 0 && $nEdits <= 1000 ) { 00988 $limit = 100; // use diff-multi-manyusers if too many users 00989 $users = $this->mNewPage->getAuthorsBetween( $oldRev, $newRev, $limit ); 00990 $numUsers = count( $users ); 00991 00992 if ( $numUsers == 1 && $users[0] == $newRev->getRawUserText() ) { 00993 $numUsers = 0; // special case to say "by the same user" instead of "by one other user" 00994 } 00995 00996 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit ); 00997 } 00998 00999 return ''; // nothing 01000 } 01001 01011 public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) { 01012 if ( $numUsers === 0 ) { 01013 $msg = 'diff-multi-sameuser'; 01014 } elseif ( $numUsers > $limit ) { 01015 $msg = 'diff-multi-manyusers'; 01016 $numUsers = $limit; 01017 } else { 01018 $msg = 'diff-multi-otherusers'; 01019 } 01020 01021 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse(); 01022 } 01023 01033 protected function getRevisionHeader( Revision $rev, $complete = '' ) { 01034 $lang = $this->getLanguage(); 01035 $user = $this->getUser(); 01036 $revtimestamp = $rev->getTimestamp(); 01037 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user ); 01038 $dateofrev = $lang->userDate( $revtimestamp, $user ); 01039 $timeofrev = $lang->userTime( $revtimestamp, $user ); 01040 01041 $header = $this->msg( 01042 $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof', 01043 $timestamp, 01044 $dateofrev, 01045 $timeofrev 01046 )->escaped(); 01047 01048 if ( $complete !== 'complete' ) { 01049 return $header; 01050 } 01051 01052 $title = $rev->getTitle(); 01053 01054 $header = Linker::linkKnown( $title, $header, array(), 01055 array( 'oldid' => $rev->getID() ) ); 01056 01057 if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) { 01058 $editQuery = array( 'action' => 'edit' ); 01059 if ( !$rev->isCurrent() ) { 01060 $editQuery['oldid'] = $rev->getID(); 01061 } 01062 01063 $key = $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold'; 01064 $msg = $this->msg( $key )->escaped(); 01065 $header .= ' ' . $this->msg( 'parentheses' )->rawParams( 01066 Linker::linkKnown( $title, $msg, array(), $editQuery ) )->plain(); 01067 if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { 01068 $header = Html::rawElement( 01069 'span', 01070 array( 'class' => 'history-deleted' ), 01071 $header 01072 ); 01073 } 01074 } else { 01075 $header = Html::rawElement( 'span', array( 'class' => 'history-deleted' ), $header ); 01076 } 01077 01078 return $header; 01079 } 01080 01093 public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) { 01094 // shared.css sets diff in interface language/dir, but the actual content 01095 // is often in a different language, mostly the page content language/dir 01096 $tableClass = 'diff diff-contentalign-' . htmlspecialchars( $this->getDiffLang()->alignStart() ); 01097 $header = "<table class='$tableClass'>"; 01098 01099 if ( !$diff && !$otitle ) { 01100 $header .= " 01101 <tr style='vertical-align: top;'> 01102 <td class='diff-ntitle'>{$ntitle}</td> 01103 </tr>"; 01104 $multiColspan = 1; 01105 } else { 01106 if ( $diff ) { // Safari/Chrome show broken output if cols not used 01107 $header .= " 01108 <col class='diff-marker' /> 01109 <col class='diff-content' /> 01110 <col class='diff-marker' /> 01111 <col class='diff-content' />"; 01112 $colspan = 2; 01113 $multiColspan = 4; 01114 } else { 01115 $colspan = 1; 01116 $multiColspan = 2; 01117 } 01118 if ( $otitle || $ntitle ) { 01119 $header .= " 01120 <tr style='vertical-align: top;'> 01121 <td colspan='$colspan' class='diff-otitle'>{$otitle}</td> 01122 <td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td> 01123 </tr>"; 01124 } 01125 } 01126 01127 if ( $multi != '' ) { 01128 $header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;' " . 01129 "class='diff-multi'>{$multi}</td></tr>"; 01130 } 01131 if ( $notice != '' ) { 01132 $header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;'>{$notice}</td></tr>"; 01133 } 01134 01135 return $header . $diff . "</table>"; 01136 } 01137 01142 public function setText( $oldText, $newText ) { 01143 ContentHandler::deprecated( __METHOD__, "1.21" ); 01144 01145 $oldContent = ContentHandler::makeContent( $oldText, $this->getTitle() ); 01146 $newContent = ContentHandler::makeContent( $newText, $this->getTitle() ); 01147 01148 $this->setContent( $oldContent, $newContent ); 01149 } 01150 01155 public function setContent( Content $oldContent, Content $newContent ) { 01156 $this->mOldContent = $oldContent; 01157 $this->mNewContent = $newContent; 01158 01159 $this->mTextLoaded = 2; 01160 $this->mRevisionsLoaded = true; 01161 } 01162 01168 public function setTextLanguage( $lang ) { 01169 $this->mDiffLang = wfGetLangObj( $lang ); 01170 } 01171 01183 public function mapDiffPrevNext( $old, $new ) { 01184 if ( $new === 'prev' ) { 01185 // Show diff between revision $old and the previous one. Get previous one from DB. 01186 $newid = intval( $old ); 01187 $oldid = $this->getTitle()->getPreviousRevisionID( $newid ); 01188 } elseif ( $new === 'next' ) { 01189 // Show diff between revision $old and the next one. Get next one from DB. 01190 $oldid = intval( $old ); 01191 $newid = $this->getTitle()->getNextRevisionID( $oldid ); 01192 } else { 01193 $oldid = intval( $old ); 01194 $newid = intval( $new ); 01195 } 01196 01197 return array( $oldid, $newid ); 01198 } 01199 01203 private function loadRevisionIds() { 01204 if ( $this->mRevisionsIdsLoaded ) { 01205 return; 01206 } 01207 01208 $this->mRevisionsIdsLoaded = true; 01209 01210 $old = $this->mOldid; 01211 $new = $this->mNewid; 01212 01213 list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new ); 01214 if ( $new === 'next' && $this->mNewid === false ) { 01215 # if no result, NewId points to the newest old revision. The only newer 01216 # revision is cur, which is "0". 01217 $this->mNewid = 0; 01218 } 01219 01220 wfRunHooks( 01221 'NewDifferenceEngine', 01222 array( $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ) 01223 ); 01224 } 01225 01238 public function loadRevisionData() { 01239 if ( $this->mRevisionsLoaded ) { 01240 return true; 01241 } 01242 01243 // Whether it succeeds or fails, we don't want to try again 01244 $this->mRevisionsLoaded = true; 01245 01246 $this->loadRevisionIds(); 01247 01248 // Load the new revision object 01249 if ( $this->mNewid ) { 01250 $this->mNewRev = Revision::newFromId( $this->mNewid ); 01251 } else { 01252 $this->mNewRev = Revision::newFromTitle( 01253 $this->getTitle(), 01254 false, 01255 Revision::READ_NORMAL 01256 ); 01257 } 01258 01259 if ( !$this->mNewRev instanceof Revision ) { 01260 return false; 01261 } 01262 01263 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff) 01264 $this->mNewid = $this->mNewRev->getId(); 01265 $this->mNewPage = $this->mNewRev->getTitle(); 01266 01267 // Load the old revision object 01268 $this->mOldRev = false; 01269 if ( $this->mOldid ) { 01270 $this->mOldRev = Revision::newFromId( $this->mOldid ); 01271 } elseif ( $this->mOldid === 0 ) { 01272 $rev = $this->mNewRev->getPrevious(); 01273 if ( $rev ) { 01274 $this->mOldid = $rev->getId(); 01275 $this->mOldRev = $rev; 01276 } else { 01277 // No previous revision; mark to show as first-version only. 01278 $this->mOldid = false; 01279 $this->mOldRev = false; 01280 } 01281 } /* elseif ( $this->mOldid === false ) leave mOldRev false; */ 01282 01283 if ( is_null( $this->mOldRev ) ) { 01284 return false; 01285 } 01286 01287 if ( $this->mOldRev ) { 01288 $this->mOldPage = $this->mOldRev->getTitle(); 01289 } 01290 01291 // Load tags information for both revisions 01292 $dbr = wfGetDB( DB_SLAVE ); 01293 if ( $this->mOldid !== false ) { 01294 $this->mOldTags = $dbr->selectField( 01295 'tag_summary', 01296 'ts_tags', 01297 array( 'ts_rev_id' => $this->mOldid ), 01298 __METHOD__ 01299 ); 01300 } else { 01301 $this->mOldTags = false; 01302 } 01303 $this->mNewTags = $dbr->selectField( 01304 'tag_summary', 01305 'ts_tags', 01306 array( 'ts_rev_id' => $this->mNewid ), 01307 __METHOD__ 01308 ); 01309 01310 return true; 01311 } 01312 01318 public function loadText() { 01319 if ( $this->mTextLoaded == 2 ) { 01320 return true; 01321 } 01322 01323 // Whether it succeeds or fails, we don't want to try again 01324 $this->mTextLoaded = 2; 01325 01326 if ( !$this->loadRevisionData() ) { 01327 return false; 01328 } 01329 01330 if ( $this->mOldRev ) { 01331 $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); 01332 if ( $this->mOldContent === null ) { 01333 return false; 01334 } 01335 } 01336 01337 if ( $this->mNewRev ) { 01338 $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); 01339 if ( $this->mNewContent === null ) { 01340 return false; 01341 } 01342 } 01343 01344 return true; 01345 } 01346 01352 public function loadNewText() { 01353 if ( $this->mTextLoaded >= 1 ) { 01354 return true; 01355 } 01356 01357 $this->mTextLoaded = 1; 01358 01359 if ( !$this->loadRevisionData() ) { 01360 return false; 01361 } 01362 01363 $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); 01364 01365 return true; 01366 } 01367 01368 }