MediaWiki
REL1_24
|
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['mw-diff-undo'] = $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 $key => $tool ) { 00391 $toolClass = is_string( $key ) ? $key : 'mw-diff-tool'; 00392 $element = Html::rawElement( 00393 'span', 00394 array( 'class' => $toolClass ), 00395 $this->msg( 'parentheses' )->rawParams( $tool )->escaped() 00396 ); 00397 $formattedRevisionTools[] = $element; 00398 } 00399 $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) . 00400 ' ' . implode( ' ', $formattedRevisionTools ); 00401 $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff' ); 00402 00403 $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' . 00404 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) . 00405 " $rollback</div>" . 00406 '<div id="mw-diff-ntitle3">' . $newminor . 00407 Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' . 00408 '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' . 00409 '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>'; 00410 00411 if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) { 00412 $deleted = true; // new revisions text is hidden 00413 if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) { 00414 $suppressed = true; // also suppressed 00415 } 00416 } 00417 00418 # If the diff cannot be shown due to a deleted revision, then output 00419 # the diff header and links to unhide (if available)... 00420 if ( $deleted && ( !$this->unhide || !$allowed ) ) { 00421 $this->showDiffStyle(); 00422 $multi = $this->getMultiNotice(); 00423 $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) ); 00424 if ( !$allowed ) { 00425 $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff'; 00426 # Give explanation for why revision is not visible 00427 $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n", 00428 array( $msg ) ); 00429 } else { 00430 # Give explanation and add a link to view the diff... 00431 $query = $this->getRequest()->appendQueryValue( 'unhide', '1', true ); 00432 $link = $this->getTitle()->getFullURL( $query ); 00433 $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff'; 00434 $out->wrapWikiMsg( 00435 "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n", 00436 array( $msg, $link ) 00437 ); 00438 } 00439 # Otherwise, output a regular diff... 00440 } else { 00441 # Add deletion notice if the user is viewing deleted content 00442 $notice = ''; 00443 if ( $deleted ) { 00444 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view'; 00445 $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" . 00446 $this->msg( $msg )->parse() . 00447 "</div>\n"; 00448 } 00449 $this->showDiff( $oldHeader, $newHeader, $notice ); 00450 if ( !$diffOnly ) { 00451 $this->renderNewRevision(); 00452 } 00453 } 00454 wfProfileOut( __METHOD__ ); 00455 } 00456 00465 protected function markPatrolledLink() { 00466 global $wgUseRCPatrol, $wgEnableAPI, $wgEnableWriteAPI; 00467 $user = $this->getUser(); 00468 00469 if ( $this->mMarkPatrolledLink === null ) { 00470 // Prepare a change patrol link, if applicable 00471 if ( 00472 // Is patrolling enabled and the user allowed to? 00473 $wgUseRCPatrol && $this->mNewPage->quickUserCan( 'patrol', $user ) && 00474 // Only do this if the revision isn't more than 6 hours older 00475 // than the Max RC age (6h because the RC might not be cleaned out regularly) 00476 RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 ) 00477 ) { 00478 // Look for an unpatrolled change corresponding to this diff 00479 00480 $db = wfGetDB( DB_SLAVE ); 00481 $change = RecentChange::newFromConds( 00482 array( 00483 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ), 00484 'rc_this_oldid' => $this->mNewid, 00485 'rc_patrolled' => 0 00486 ), 00487 __METHOD__, 00488 array( 'USE INDEX' => 'rc_timestamp' ) 00489 ); 00490 00491 if ( $change && $change->getPerformer()->getName() !== $user->getName() ) { 00492 $rcid = $change->getAttribute( 'rc_id' ); 00493 } else { 00494 // None found or the page has been created by the current user. 00495 // If the user could patrol this it already would be patrolled 00496 $rcid = 0; 00497 } 00498 // Build the link 00499 if ( $rcid ) { 00500 $this->getOutput()->preventClickjacking(); 00501 if ( $wgEnableAPI && $wgEnableWriteAPI 00502 && $user->isAllowed( 'writeapi' ) 00503 ) { 00504 $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' ); 00505 } 00506 00507 $token = $user->getEditToken( $rcid ); 00508 $this->mMarkPatrolledLink = ' <span class="patrollink">[' . Linker::linkKnown( 00509 $this->mNewPage, 00510 $this->msg( 'markaspatrolleddiff' )->escaped(), 00511 array(), 00512 array( 00513 'action' => 'markpatrolled', 00514 'rcid' => $rcid, 00515 'token' => $token, 00516 ) 00517 ) . ']</span>'; 00518 } else { 00519 $this->mMarkPatrolledLink = ''; 00520 } 00521 } else { 00522 $this->mMarkPatrolledLink = ''; 00523 } 00524 } 00525 00526 return $this->mMarkPatrolledLink; 00527 } 00528 00534 protected function revisionDeleteLink( $rev ) { 00535 $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() ); 00536 if ( $link !== '' ) { 00537 $link = '   ' . $link . ' '; 00538 } 00539 00540 return $link; 00541 } 00542 00546 public function renderNewRevision() { 00547 wfProfileIn( __METHOD__ ); 00548 $out = $this->getOutput(); 00549 $revHeader = $this->getRevisionHeader( $this->mNewRev ); 00550 # Add "current version as of X" title 00551 $out->addHTML( "<hr class='diff-hr' /> 00552 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" ); 00553 # Page content may be handled by a hooked call instead... 00554 # @codingStandardsIgnoreStart Ignoring long lines. 00555 if ( wfRunHooks( 'ArticleContentOnDiff', array( $this, $out ) ) ) { 00556 $this->loadNewText(); 00557 $out->setRevisionId( $this->mNewid ); 00558 $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() ); 00559 $out->setArticleFlag( true ); 00560 00561 // NOTE: only needed for B/C: custom rendering of JS/CSS via hook 00562 if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) { 00563 // This needs to be synchronised with Article::showCssOrJsPage(), which sucks 00564 // Give hooks a chance to customise the output 00565 // @todo standardize this crap into one function 00566 if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->mNewContent, $this->mNewPage, $out ) ) ) { 00567 // NOTE: deprecated hook, B/C only 00568 // use the content object's own rendering 00569 $cnt = $this->mNewRev->getContent(); 00570 $po = $cnt ? $cnt->getParserOutput( $this->mNewRev->getTitle(), $this->mNewRev->getId() ) : null; 00571 if ( $po ) { 00572 $out->addParserOutputContent( $po ); 00573 } 00574 } 00575 } elseif ( !wfRunHooks( 'ArticleContentViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) { 00576 // Handled by extension 00577 } elseif ( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) { 00578 // NOTE: deprecated hook, B/C only 00579 // Handled by extension 00580 } else { 00581 // Normal page 00582 if ( $this->getTitle()->equals( $this->mNewPage ) ) { 00583 // If the Title stored in the context is the same as the one 00584 // of the new revision, we can use its associated WikiPage 00585 // object. 00586 $wikiPage = $this->getWikiPage(); 00587 } else { 00588 // Otherwise we need to create our own WikiPage object 00589 $wikiPage = WikiPage::factory( $this->mNewPage ); 00590 } 00591 00592 $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev ); 00593 00594 # WikiPage::getParserOutput() should not return false, but just in case 00595 if ( $parserOutput ) { 00596 $out->addParserOutput( $parserOutput ); 00597 } 00598 } 00599 } 00600 # @codingStandardsIgnoreEnd 00601 00602 # Add redundant patrol link on bottom... 00603 $out->addHTML( $this->markPatrolledLink() ); 00604 00605 wfProfileOut( __METHOD__ ); 00606 } 00607 00608 protected function getParserOutput( WikiPage $page, Revision $rev ) { 00609 $parserOptions = $page->makeParserOptions( $this->getContext() ); 00610 00611 if ( !$rev->isCurrent() || !$rev->getTitle()->quickUserCan( "edit" ) ) { 00612 $parserOptions->setEditSection( false ); 00613 } 00614 00615 $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() ); 00616 00617 return $parserOutput; 00618 } 00619 00630 public function showDiff( $otitle, $ntitle, $notice = '' ) { 00631 $diff = $this->getDiff( $otitle, $ntitle, $notice ); 00632 if ( $diff === false ) { 00633 $this->showMissingRevision(); 00634 00635 return false; 00636 } else { 00637 $this->showDiffStyle(); 00638 $this->getOutput()->addHTML( $diff ); 00639 00640 return true; 00641 } 00642 } 00643 00647 public function showDiffStyle() { 00648 $this->getOutput()->addModuleStyles( 'mediawiki.action.history.diff' ); 00649 } 00650 00660 public function getDiff( $otitle, $ntitle, $notice = '' ) { 00661 $body = $this->getDiffBody(); 00662 if ( $body === false ) { 00663 return false; 00664 } 00665 00666 $multi = $this->getMultiNotice(); 00667 // Display a message when the diff is empty 00668 if ( $body === '' ) { 00669 $notice .= '<div class="mw-diff-empty">' . 00670 $this->msg( 'diff-empty' )->parse() . 00671 "</div>\n"; 00672 } 00673 00674 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice ); 00675 } 00676 00682 public function getDiffBody() { 00683 global $wgMemc; 00684 wfProfileIn( __METHOD__ ); 00685 $this->mCacheHit = true; 00686 // Check if the diff should be hidden from this user 00687 if ( !$this->loadRevisionData() ) { 00688 wfProfileOut( __METHOD__ ); 00689 00690 return false; 00691 } elseif ( $this->mOldRev && 00692 !$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) 00693 ) { 00694 wfProfileOut( __METHOD__ ); 00695 00696 return false; 00697 } elseif ( $this->mNewRev && 00698 !$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) 00699 ) { 00700 wfProfileOut( __METHOD__ ); 00701 00702 return false; 00703 } 00704 // Short-circuit 00705 if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev 00706 && $this->mOldRev->getID() == $this->mNewRev->getID() ) 00707 ) { 00708 wfProfileOut( __METHOD__ ); 00709 00710 return ''; 00711 } 00712 // Cacheable? 00713 $key = false; 00714 if ( $this->mOldid && $this->mNewid ) { 00715 $key = $this->getDiffBodyCacheKey(); 00716 00717 // Try cache 00718 if ( !$this->mRefreshCache ) { 00719 $difftext = $wgMemc->get( $key ); 00720 if ( $difftext ) { 00721 wfIncrStats( 'diff_cache_hit' ); 00722 $difftext = $this->localiseLineNumbers( $difftext ); 00723 $difftext .= "\n<!-- diff cache key $key -->\n"; 00724 wfProfileOut( __METHOD__ ); 00725 00726 return $difftext; 00727 } 00728 } // don't try to load but save the result 00729 } 00730 $this->mCacheHit = false; 00731 00732 // Loadtext is permission safe, this just clears out the diff 00733 if ( !$this->loadText() ) { 00734 wfProfileOut( __METHOD__ ); 00735 00736 return false; 00737 } 00738 00739 $difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent ); 00740 00741 // Save to cache for 7 days 00742 if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) { 00743 wfIncrStats( 'diff_uncacheable' ); 00744 } elseif ( $key !== false && $difftext !== false ) { 00745 wfIncrStats( 'diff_cache_miss' ); 00746 $wgMemc->set( $key, $difftext, 7 * 86400 ); 00747 } else { 00748 wfIncrStats( 'diff_uncacheable' ); 00749 } 00750 // Replace line numbers with the text in the user's language 00751 if ( $difftext !== false ) { 00752 $difftext = $this->localiseLineNumbers( $difftext ); 00753 } 00754 wfProfileOut( __METHOD__ ); 00755 00756 return $difftext; 00757 } 00758 00767 protected function getDiffBodyCacheKey() { 00768 if ( !$this->mOldid || !$this->mNewid ) { 00769 throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' ); 00770 } 00771 00772 return wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 00773 'oldid', $this->mOldid, 'newid', $this->mNewid ); 00774 } 00775 00795 public function generateContentDiffBody( Content $old, Content $new ) { 00796 if ( !( $old instanceof TextContent ) ) { 00797 throw new MWException( "Diff not implemented for " . get_class( $old ) . "; " . 00798 "override generateContentDiffBody to fix this." ); 00799 } 00800 00801 if ( !( $new instanceof TextContent ) ) { 00802 throw new MWException( "Diff not implemented for " . get_class( $new ) . "; " 00803 . "override generateContentDiffBody to fix this." ); 00804 } 00805 00806 $otext = $old->serialize(); 00807 $ntext = $new->serialize(); 00808 00809 return $this->generateTextDiffBody( $otext, $ntext ); 00810 } 00811 00821 public function generateDiffBody( $otext, $ntext ) { 00822 ContentHandler::deprecated( __METHOD__, "1.21" ); 00823 00824 return $this->generateTextDiffBody( $otext, $ntext ); 00825 } 00826 00837 public function generateTextDiffBody( $otext, $ntext ) { 00838 global $wgExternalDiffEngine, $wgContLang; 00839 00840 wfProfileIn( __METHOD__ ); 00841 00842 $otext = str_replace( "\r\n", "\n", $otext ); 00843 $ntext = str_replace( "\r\n", "\n", $ntext ); 00844 00845 if ( $wgExternalDiffEngine == 'wikidiff' && function_exists( 'wikidiff_do_diff' ) ) { 00846 # For historical reasons, external diff engine expects 00847 # input text to be HTML-escaped already 00848 $otext = htmlspecialchars( $wgContLang->segmentForDiff( $otext ) ); 00849 $ntext = htmlspecialchars( $wgContLang->segmentForDiff( $ntext ) ); 00850 wfProfileOut( __METHOD__ ); 00851 00852 return $wgContLang->unsegmentForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) . 00853 $this->debug( 'wikidiff1' ); 00854 } 00855 00856 if ( $wgExternalDiffEngine == 'wikidiff2' && function_exists( 'wikidiff2_do_diff' ) ) { 00857 # Better external diff engine, the 2 may some day be dropped 00858 # This one does the escaping and segmenting itself 00859 wfProfileIn( 'wikidiff2_do_diff' ); 00860 $text = wikidiff2_do_diff( $otext, $ntext, 2 ); 00861 $text .= $this->debug( 'wikidiff2' ); 00862 wfProfileOut( 'wikidiff2_do_diff' ); 00863 wfProfileOut( __METHOD__ ); 00864 00865 return $text; 00866 } 00867 if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) { 00868 # Diff via the shell 00869 $tmpDir = wfTempDir(); 00870 $tempName1 = tempnam( $tmpDir, 'diff_' ); 00871 $tempName2 = tempnam( $tmpDir, 'diff_' ); 00872 00873 $tempFile1 = fopen( $tempName1, "w" ); 00874 if ( !$tempFile1 ) { 00875 wfProfileOut( __METHOD__ ); 00876 00877 return false; 00878 } 00879 $tempFile2 = fopen( $tempName2, "w" ); 00880 if ( !$tempFile2 ) { 00881 wfProfileOut( __METHOD__ ); 00882 00883 return false; 00884 } 00885 fwrite( $tempFile1, $otext ); 00886 fwrite( $tempFile2, $ntext ); 00887 fclose( $tempFile1 ); 00888 fclose( $tempFile2 ); 00889 $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 ); 00890 wfProfileIn( __METHOD__ . "-shellexec" ); 00891 $difftext = wfShellExec( $cmd ); 00892 $difftext .= $this->debug( "external $wgExternalDiffEngine" ); 00893 wfProfileOut( __METHOD__ . "-shellexec" ); 00894 unlink( $tempName1 ); 00895 unlink( $tempName2 ); 00896 wfProfileOut( __METHOD__ ); 00897 00898 return $difftext; 00899 } 00900 00901 # Native PHP diff 00902 $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) ); 00903 $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) ); 00904 $diffs = new Diff( $ota, $nta ); 00905 $formatter = new TableDiffFormatter(); 00906 $difftext = $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) . 00907 wfProfileOut( __METHOD__ ); 00908 00909 return $difftext; 00910 } 00911 00920 protected function debug( $generator = "internal" ) { 00921 global $wgShowHostnames; 00922 if ( !$this->enableDebugComment ) { 00923 return ''; 00924 } 00925 $data = array( $generator ); 00926 if ( $wgShowHostnames ) { 00927 $data[] = wfHostname(); 00928 } 00929 $data[] = wfTimestamp( TS_DB ); 00930 00931 return "<!-- diff generator: " . 00932 implode( " ", array_map( "htmlspecialchars", $data ) ) . 00933 " -->\n"; 00934 } 00935 00943 public function localiseLineNumbers( $text ) { 00944 return preg_replace_callback( 00945 '/<!--LINE (\d+)-->/', 00946 array( &$this, 'localiseLineNumbersCb' ), 00947 $text 00948 ); 00949 } 00950 00951 public function localiseLineNumbersCb( $matches ) { 00952 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) { 00953 return ''; 00954 } 00955 00956 return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped(); 00957 } 00958 00964 public function getMultiNotice() { 00965 if ( !is_object( $this->mOldRev ) || !is_object( $this->mNewRev ) ) { 00966 return ''; 00967 } elseif ( !$this->mOldPage->equals( $this->mNewPage ) ) { 00968 // Comparing two different pages? Count would be meaningless. 00969 return ''; 00970 } 00971 00972 if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) { 00973 $oldRev = $this->mNewRev; // flip 00974 $newRev = $this->mOldRev; // flip 00975 } else { // normal case 00976 $oldRev = $this->mOldRev; 00977 $newRev = $this->mNewRev; 00978 } 00979 00980 // Sanity: don't show the notice if too many rows must be scanned 00981 // @todo show some special message for that case 00982 $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev, 1000 ); 00983 if ( $nEdits > 0 && $nEdits <= 1000 ) { 00984 $limit = 100; // use diff-multi-manyusers if too many users 00985 $users = $this->mNewPage->getAuthorsBetween( $oldRev, $newRev, $limit ); 00986 $numUsers = count( $users ); 00987 00988 if ( $numUsers == 1 && $users[0] == $newRev->getRawUserText() ) { 00989 $numUsers = 0; // special case to say "by the same user" instead of "by one other user" 00990 } 00991 00992 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit ); 00993 } 00994 00995 return ''; // nothing 00996 } 00997 01007 public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) { 01008 if ( $numUsers === 0 ) { 01009 $msg = 'diff-multi-sameuser'; 01010 } elseif ( $numUsers > $limit ) { 01011 $msg = 'diff-multi-manyusers'; 01012 $numUsers = $limit; 01013 } else { 01014 $msg = 'diff-multi-otherusers'; 01015 } 01016 01017 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse(); 01018 } 01019 01029 protected function getRevisionHeader( Revision $rev, $complete = '' ) { 01030 $lang = $this->getLanguage(); 01031 $user = $this->getUser(); 01032 $revtimestamp = $rev->getTimestamp(); 01033 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user ); 01034 $dateofrev = $lang->userDate( $revtimestamp, $user ); 01035 $timeofrev = $lang->userTime( $revtimestamp, $user ); 01036 01037 $header = $this->msg( 01038 $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof', 01039 $timestamp, 01040 $dateofrev, 01041 $timeofrev 01042 )->escaped(); 01043 01044 if ( $complete !== 'complete' ) { 01045 return $header; 01046 } 01047 01048 $title = $rev->getTitle(); 01049 01050 $header = Linker::linkKnown( $title, $header, array(), 01051 array( 'oldid' => $rev->getID() ) ); 01052 01053 if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) { 01054 $editQuery = array( 'action' => 'edit' ); 01055 if ( !$rev->isCurrent() ) { 01056 $editQuery['oldid'] = $rev->getID(); 01057 } 01058 01059 $key = $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold'; 01060 $msg = $this->msg( $key )->escaped(); 01061 $editLink = $this->msg( 'parentheses' )->rawParams( 01062 Linker::linkKnown( $title, $msg, array( ), $editQuery ) )->plain(); 01063 $header .= ' ' . Html::rawElement( 01064 'span', 01065 array( 'class' => 'mw-diff-edit' ), 01066 $editLink 01067 ); 01068 if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { 01069 $header = Html::rawElement( 01070 'span', 01071 array( 'class' => 'history-deleted' ), 01072 $header 01073 ); 01074 } 01075 } else { 01076 $header = Html::rawElement( 'span', array( 'class' => 'history-deleted' ), $header ); 01077 } 01078 01079 return $header; 01080 } 01081 01094 public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) { 01095 // shared.css sets diff in interface language/dir, but the actual content 01096 // is often in a different language, mostly the page content language/dir 01097 $tableClass = 'diff diff-contentalign-' . htmlspecialchars( $this->getDiffLang()->alignStart() ); 01098 $header = "<table class='$tableClass'>"; 01099 01100 if ( !$diff && !$otitle ) { 01101 $header .= " 01102 <tr style='vertical-align: top;'> 01103 <td class='diff-ntitle'>{$ntitle}</td> 01104 </tr>"; 01105 $multiColspan = 1; 01106 } else { 01107 if ( $diff ) { // Safari/Chrome show broken output if cols not used 01108 $header .= " 01109 <col class='diff-marker' /> 01110 <col class='diff-content' /> 01111 <col class='diff-marker' /> 01112 <col class='diff-content' />"; 01113 $colspan = 2; 01114 $multiColspan = 4; 01115 } else { 01116 $colspan = 1; 01117 $multiColspan = 2; 01118 } 01119 if ( $otitle || $ntitle ) { 01120 $header .= " 01121 <tr style='vertical-align: top;'> 01122 <td colspan='$colspan' class='diff-otitle'>{$otitle}</td> 01123 <td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td> 01124 </tr>"; 01125 } 01126 } 01127 01128 if ( $multi != '' ) { 01129 $header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;' " . 01130 "class='diff-multi'>{$multi}</td></tr>"; 01131 } 01132 if ( $notice != '' ) { 01133 $header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;'>{$notice}</td></tr>"; 01134 } 01135 01136 return $header . $diff . "</table>"; 01137 } 01138 01143 public function setText( $oldText, $newText ) { 01144 ContentHandler::deprecated( __METHOD__, "1.21" ); 01145 01146 $oldContent = ContentHandler::makeContent( $oldText, $this->getTitle() ); 01147 $newContent = ContentHandler::makeContent( $newText, $this->getTitle() ); 01148 01149 $this->setContent( $oldContent, $newContent ); 01150 } 01151 01158 public function setContent( Content $oldContent, Content $newContent ) { 01159 $this->mOldContent = $oldContent; 01160 $this->mNewContent = $newContent; 01161 01162 $this->mTextLoaded = 2; 01163 $this->mRevisionsLoaded = true; 01164 } 01165 01172 public function setTextLanguage( $lang ) { 01173 $this->mDiffLang = wfGetLangObj( $lang ); 01174 } 01175 01187 public function mapDiffPrevNext( $old, $new ) { 01188 if ( $new === 'prev' ) { 01189 // Show diff between revision $old and the previous one. Get previous one from DB. 01190 $newid = intval( $old ); 01191 $oldid = $this->getTitle()->getPreviousRevisionID( $newid ); 01192 } elseif ( $new === 'next' ) { 01193 // Show diff between revision $old and the next one. Get next one from DB. 01194 $oldid = intval( $old ); 01195 $newid = $this->getTitle()->getNextRevisionID( $oldid ); 01196 } else { 01197 $oldid = intval( $old ); 01198 $newid = intval( $new ); 01199 } 01200 01201 return array( $oldid, $newid ); 01202 } 01203 01207 private function loadRevisionIds() { 01208 if ( $this->mRevisionsIdsLoaded ) { 01209 return; 01210 } 01211 01212 $this->mRevisionsIdsLoaded = true; 01213 01214 $old = $this->mOldid; 01215 $new = $this->mNewid; 01216 01217 list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new ); 01218 if ( $new === 'next' && $this->mNewid === false ) { 01219 # if no result, NewId points to the newest old revision. The only newer 01220 # revision is cur, which is "0". 01221 $this->mNewid = 0; 01222 } 01223 01224 wfRunHooks( 01225 'NewDifferenceEngine', 01226 array( $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ) 01227 ); 01228 } 01229 01242 public function loadRevisionData() { 01243 if ( $this->mRevisionsLoaded ) { 01244 return true; 01245 } 01246 01247 // Whether it succeeds or fails, we don't want to try again 01248 $this->mRevisionsLoaded = true; 01249 01250 $this->loadRevisionIds(); 01251 01252 // Load the new revision object 01253 if ( $this->mNewid ) { 01254 $this->mNewRev = Revision::newFromId( $this->mNewid ); 01255 } else { 01256 $this->mNewRev = Revision::newFromTitle( 01257 $this->getTitle(), 01258 false, 01259 Revision::READ_NORMAL 01260 ); 01261 } 01262 01263 if ( !$this->mNewRev instanceof Revision ) { 01264 return false; 01265 } 01266 01267 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff) 01268 $this->mNewid = $this->mNewRev->getId(); 01269 $this->mNewPage = $this->mNewRev->getTitle(); 01270 01271 // Load the old revision object 01272 $this->mOldRev = false; 01273 if ( $this->mOldid ) { 01274 $this->mOldRev = Revision::newFromId( $this->mOldid ); 01275 } elseif ( $this->mOldid === 0 ) { 01276 $rev = $this->mNewRev->getPrevious(); 01277 if ( $rev ) { 01278 $this->mOldid = $rev->getId(); 01279 $this->mOldRev = $rev; 01280 } else { 01281 // No previous revision; mark to show as first-version only. 01282 $this->mOldid = false; 01283 $this->mOldRev = false; 01284 } 01285 } /* elseif ( $this->mOldid === false ) leave mOldRev false; */ 01286 01287 if ( is_null( $this->mOldRev ) ) { 01288 return false; 01289 } 01290 01291 if ( $this->mOldRev ) { 01292 $this->mOldPage = $this->mOldRev->getTitle(); 01293 } 01294 01295 // Load tags information for both revisions 01296 $dbr = wfGetDB( DB_SLAVE ); 01297 if ( $this->mOldid !== false ) { 01298 $this->mOldTags = $dbr->selectField( 01299 'tag_summary', 01300 'ts_tags', 01301 array( 'ts_rev_id' => $this->mOldid ), 01302 __METHOD__ 01303 ); 01304 } else { 01305 $this->mOldTags = false; 01306 } 01307 $this->mNewTags = $dbr->selectField( 01308 'tag_summary', 01309 'ts_tags', 01310 array( 'ts_rev_id' => $this->mNewid ), 01311 __METHOD__ 01312 ); 01313 01314 return true; 01315 } 01316 01322 public function loadText() { 01323 if ( $this->mTextLoaded == 2 ) { 01324 return true; 01325 } 01326 01327 // Whether it succeeds or fails, we don't want to try again 01328 $this->mTextLoaded = 2; 01329 01330 if ( !$this->loadRevisionData() ) { 01331 return false; 01332 } 01333 01334 if ( $this->mOldRev ) { 01335 $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); 01336 if ( $this->mOldContent === null ) { 01337 return false; 01338 } 01339 } 01340 01341 if ( $this->mNewRev ) { 01342 $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); 01343 if ( $this->mNewContent === null ) { 01344 return false; 01345 } 01346 } 01347 01348 return true; 01349 } 01350 01356 public function loadNewText() { 01357 if ( $this->mTextLoaded >= 1 ) { 01358 return true; 01359 } 01360 01361 $this->mTextLoaded = 1; 01362 01363 if ( !$this->loadRevisionData() ) { 01364 return false; 01365 } 01366 01367 $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); 01368 01369 return true; 01370 } 01371 01372 }