MediaWiki
REL1_21
|
00001 <?php 00030 define( 'MW_DIFF_VERSION', '1.11a' ); 00031 00036 class DifferenceEngine extends ContextSource { 00040 var $mOldid, $mNewid; 00044 var $mOldContent, $mNewContent; 00045 protected $mDiffLang; 00046 00050 var $mOldPage, $mNewPage; 00051 var $mRcidMarkPatrolled; 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->mRcidMarkPatrolled = intval( $rcid ); # force it to be an integer 00100 $this->mRefreshCache = $refreshCache; 00101 $this->unhide = $unhide; 00102 } 00103 00107 function setReducedLineNumbers( $value = true ) { 00108 $this->mReducedLineNumbers = $value; 00109 } 00110 00114 function getDiffLang() { 00115 if ( $this->mDiffLang === null ) { 00116 # Default language in which the diff text is written. 00117 $this->mDiffLang = $this->getTitle()->getPageLanguage(); 00118 } 00119 return $this->mDiffLang; 00120 } 00121 00125 function wasCacheHit() { 00126 return $this->mCacheHit; 00127 } 00128 00132 function getOldid() { 00133 $this->loadRevisionIds(); 00134 return $this->mOldid; 00135 } 00136 00140 function getNewid() { 00141 $this->loadRevisionIds(); 00142 return $this->mNewid; 00143 } 00144 00152 function deletedLink( $id ) { 00153 if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) { 00154 $dbr = wfGetDB( DB_SLAVE ); 00155 $row = $dbr->selectRow( 'archive', '*', 00156 array( 'ar_rev_id' => $id ), 00157 __METHOD__ ); 00158 if ( $row ) { 00159 $rev = Revision::newFromArchiveRow( $row ); 00160 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title ); 00161 return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( array( 00162 'target' => $title->getPrefixedText(), 00163 'timestamp' => $rev->getTimestamp() 00164 )); 00165 } 00166 } 00167 return false; 00168 } 00169 00176 function deletedIdMarker( $id ) { 00177 $link = $this->deletedLink( $id ); 00178 if ( $link ) { 00179 return "[$link $id]"; 00180 } else { 00181 return $id; 00182 } 00183 } 00184 00185 private function showMissingRevision() { 00186 $out = $this->getOutput(); 00187 00188 $missing = array(); 00189 if ( $this->mOldRev === null ) { 00190 $missing[] = $this->deletedIdMarker( $this->mOldid ); 00191 } 00192 if ( $this->mNewRev === null ) { 00193 $missing[] = $this->deletedIdMarker( $this->mNewid ); 00194 } 00195 00196 $out->setPageTitle( $this->msg( 'errorpagetitle' ) ); 00197 $out->addWikiMsg( 'difference-missing-revision', 00198 $this->getLanguage()->listToText( $missing ), count( $missing ) ); 00199 } 00200 00201 function showDiffPage( $diffOnly = false ) { 00202 wfProfileIn( __METHOD__ ); 00203 00204 # Allow frames except in certain special cases 00205 $out = $this->getOutput(); 00206 $out->allowClickjacking(); 00207 $out->setRobotPolicy( 'noindex,nofollow' ); 00208 00209 if ( !$this->loadRevisionData() ) { 00210 $this->showMissingRevision(); 00211 wfProfileOut( __METHOD__ ); 00212 return; 00213 } 00214 00215 $user = $this->getUser(); 00216 $permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user ); 00217 if ( $this->mOldPage ) { # mOldPage might not be set, see below. 00218 $permErrors = wfMergeErrorArrays( $permErrors, 00219 $this->mOldPage->getUserPermissionsErrors( 'read', $user ) ); 00220 } 00221 if ( count( $permErrors ) ) { 00222 wfProfileOut( __METHOD__ ); 00223 throw new PermissionsError( 'read', $permErrors ); 00224 } 00225 00226 # If external diffs are enabled both globally and for the user, 00227 # we'll use the application/x-external-editor interface to call 00228 # an external diff tool like kompare, kdiff3, etc. 00229 if ( ExternalEdit::useExternalEngine( $this->getContext(), 'diff' ) ) { 00230 //TODO: come up with a good solution for non-text content here. 00231 // at least, the content format needs to be passed to the client somehow. 00232 // Currently, action=raw will just fail for non-text content. 00233 00234 $urls = array( 00235 'File' => array( 'Extension' => 'wiki', 'URL' => 00236 # This should be mOldPage, but it may not be set, see below. 00237 $this->mNewPage->getCanonicalURL( array( 00238 'action' => 'raw', 'oldid' => $this->mOldid ) ) 00239 ), 00240 'File2' => array( 'Extension' => 'wiki', 'URL' => 00241 $this->mNewPage->getCanonicalURL( array( 00242 'action' => 'raw', 'oldid' => $this->mNewid ) ) 00243 ), 00244 ); 00245 00246 $externalEditor = new ExternalEdit( $this->getContext(), $urls ); 00247 $externalEditor->execute(); 00248 00249 wfProfileOut( __METHOD__ ); 00250 return; 00251 } 00252 00253 $rollback = ''; 00254 $undoLink = ''; 00255 00256 $query = array(); 00257 # Carry over 'diffonly' param via navigation links 00258 if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) { 00259 $query['diffonly'] = $diffOnly; 00260 } 00261 # Cascade unhide param in links for easy deletion browsing 00262 if ( $this->unhide ) { 00263 $query['unhide'] = 1; 00264 } 00265 00266 # Check if one of the revisions is deleted/suppressed 00267 $deleted = $suppressed = false; 00268 $allowed = $this->mNewRev->userCan( Revision::DELETED_TEXT, $user ); 00269 00270 $revisionTools = array(); 00271 00272 # mOldRev is false if the difference engine is called with a "vague" query for 00273 # a diff between a version V and its previous version V' AND the version V 00274 # is the first version of that article. In that case, V' does not exist. 00275 if ( $this->mOldRev === false ) { 00276 $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) ); 00277 $samePage = true; 00278 $oldHeader = ''; 00279 } else { 00280 wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) ); 00281 00282 $sk = $this->getSkin(); 00283 if ( method_exists( $sk, 'suppressQuickbar' ) ) { 00284 $sk->suppressQuickbar(); 00285 } 00286 00287 if ( $this->mNewPage->equals( $this->mOldPage ) ) { 00288 $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) ); 00289 $samePage = true; 00290 } else { 00291 $out->setPageTitle( $this->msg( 'difference-title-multipage', $this->mOldPage->getPrefixedText(), 00292 $this->mNewPage->getPrefixedText() ) ); 00293 $out->addSubtitle( $this->msg( 'difference-multipage' ) ); 00294 $samePage = false; 00295 } 00296 00297 if ( $samePage && $this->mNewPage->quickUserCan( 'edit', $user ) ) { 00298 if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) { 00299 $rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext() ); 00300 if ( $rollbackLink ) { 00301 $out->preventClickjacking(); 00302 $rollback = '   ' . $rollbackLink; 00303 } 00304 } 00305 if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) { 00306 $undoLink = Html::element( 'a', array( 00307 'href' => $this->mNewPage->getLocalUrl( array( 00308 'action' => 'edit', 00309 'undoafter' => $this->mOldid, 00310 'undo' => $this->mNewid ) ), 00311 'title' => Linker::titleAttrib( 'undo' ) 00312 ), 00313 $this->msg( 'editundo' )->text() 00314 ); 00315 $revisionTools[] = $undoLink; 00316 } 00317 } 00318 00319 # Make "previous revision link" 00320 if ( $samePage && $this->mOldRev->getPrevious() ) { 00321 $prevlink = Linker::linkKnown( 00322 $this->mOldPage, 00323 $this->msg( 'previousdiff' )->escaped(), 00324 array( 'id' => 'differences-prevlink' ), 00325 array( 'diff' => 'prev', 'oldid' => $this->mOldid ) + $query 00326 ); 00327 } else { 00328 $prevlink = ' '; 00329 } 00330 00331 if ( $this->mOldRev->isMinor() ) { 00332 $oldminor = ChangesList::flag( 'minor' ); 00333 } else { 00334 $oldminor = ''; 00335 } 00336 00337 $ldel = $this->revisionDeleteLink( $this->mOldRev ); 00338 $oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' ); 00339 00340 $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' . 00341 '<div id="mw-diff-otitle2">' . 00342 Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' . 00343 '<div id="mw-diff-otitle3">' . $oldminor . 00344 Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' . 00345 '<div id="mw-diff-otitle4">' . $prevlink . '</div>'; 00346 00347 if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) { 00348 $deleted = true; // old revisions text is hidden 00349 if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) { 00350 $suppressed = true; // also suppressed 00351 } 00352 } 00353 00354 # Check if this user can see the revisions 00355 if ( !$this->mOldRev->userCan( Revision::DELETED_TEXT, $user ) ) { 00356 $allowed = false; 00357 } 00358 } 00359 00360 # Make "next revision link" 00361 # Skip next link on the top revision 00362 if ( $samePage && !$this->mNewRev->isCurrent() ) { 00363 $nextlink = Linker::linkKnown( 00364 $this->mNewPage, 00365 $this->msg( 'nextdiff' )->escaped(), 00366 array( 'id' => 'differences-nextlink' ), 00367 array( 'diff' => 'next', 'oldid' => $this->mNewid ) + $query 00368 ); 00369 } else { 00370 $nextlink = ' '; 00371 } 00372 00373 if ( $this->mNewRev->isMinor() ) { 00374 $newminor = ChangesList::flag( 'minor' ); 00375 } else { 00376 $newminor = ''; 00377 } 00378 00379 # Handle RevisionDelete links... 00380 $rdel = $this->revisionDeleteLink( $this->mNewRev ); 00381 00382 # Allow extensions to define their own revision tools 00383 wfRunHooks( 'DiffRevisionTools', array( $this->mNewRev, &$revisionTools ) ); 00384 $formattedRevisionTools = array(); 00385 // Put each one in parentheses (poor man's button) 00386 foreach ( $revisionTools as $tool ) { 00387 $formattedRevisionTools[] = $this->msg( 'parentheses' )->rawParams( $tool )->escaped(); 00388 } 00389 $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) . ' ' . implode( ' ', $formattedRevisionTools ); 00390 00391 $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' . 00392 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) . 00393 " $rollback</div>" . 00394 '<div id="mw-diff-ntitle3">' . $newminor . 00395 Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' . 00396 '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>'; 00397 00398 if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) { 00399 $deleted = true; // new revisions text is hidden 00400 if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) 00401 $suppressed = true; // also suppressed 00402 } 00403 00404 # If the diff cannot be shown due to a deleted revision, then output 00405 # the diff header and links to unhide (if available)... 00406 if ( $deleted && ( !$this->unhide || !$allowed ) ) { 00407 $this->showDiffStyle(); 00408 $multi = $this->getMultiNotice(); 00409 $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) ); 00410 if ( !$allowed ) { 00411 $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff'; 00412 # Give explanation for why revision is not visible 00413 $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n", 00414 array( $msg ) ); 00415 } else { 00416 # Give explanation and add a link to view the diff... 00417 $link = $this->getTitle()->getFullUrl( $this->getRequest()->appendQueryValue( 'unhide', '1', true ) ); 00418 $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff'; 00419 $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n", array( $msg, $link ) ); 00420 } 00421 # Otherwise, output a regular diff... 00422 } else { 00423 # Add deletion notice if the user is viewing deleted content 00424 $notice = ''; 00425 if ( $deleted ) { 00426 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view'; 00427 $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" . $this->msg( $msg )->parse() . "</div>\n"; 00428 } 00429 $this->showDiff( $oldHeader, $newHeader, $notice ); 00430 if ( !$diffOnly ) { 00431 $this->renderNewRevision(); 00432 } 00433 } 00434 wfProfileOut( __METHOD__ ); 00435 } 00436 00445 protected function markPatrolledLink() { 00446 global $wgUseRCPatrol; 00447 00448 if ( $this->mMarkPatrolledLink === null ) { 00449 // Prepare a change patrol link, if applicable 00450 if ( $wgUseRCPatrol && $this->mNewPage->quickUserCan( 'patrol', $this->getUser() ) ) { 00451 // If we've been given an explicit change identifier, use it; saves time 00452 if ( $this->mRcidMarkPatrolled ) { 00453 $rcid = $this->mRcidMarkPatrolled; 00454 $rc = RecentChange::newFromId( $rcid ); 00455 // Already patrolled? 00456 $rcid = is_object( $rc ) && !$rc->getAttribute( 'rc_patrolled' ) ? $rcid : 0; 00457 } else { 00458 // Look for an unpatrolled change corresponding to this diff 00459 $db = wfGetDB( DB_SLAVE ); 00460 $change = RecentChange::newFromConds( 00461 array( 00462 // Redundant user,timestamp condition so we can use the existing index 00463 'rc_user_text' => $this->mNewRev->getRawUserText(), 00464 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ), 00465 'rc_this_oldid' => $this->mNewid, 00466 'rc_last_oldid' => $this->mOldid, 00467 'rc_patrolled' => 0 00468 ), 00469 __METHOD__ 00470 ); 00471 if ( $change instanceof RecentChange ) { 00472 $rcid = $change->mAttribs['rc_id']; 00473 $this->mRcidMarkPatrolled = $rcid; 00474 } else { 00475 // None found 00476 $rcid = 0; 00477 } 00478 } 00479 // Build the link 00480 if ( $rcid ) { 00481 $this->getOutput()->preventClickjacking(); 00482 $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' ); 00483 00484 $token = $this->getUser()->getEditToken( $rcid ); 00485 $this->mMarkPatrolledLink = ' <span class="patrollink">[' . Linker::linkKnown( 00486 $this->mNewPage, 00487 $this->msg( 'markaspatrolleddiff' )->escaped(), 00488 array(), 00489 array( 00490 'action' => 'markpatrolled', 00491 'rcid' => $rcid, 00492 'token' => $token, 00493 ) 00494 ) . ']</span>'; 00495 } else { 00496 $this->mMarkPatrolledLink = ''; 00497 } 00498 } else { 00499 $this->mMarkPatrolledLink = ''; 00500 } 00501 } 00502 00503 return $this->mMarkPatrolledLink; 00504 } 00505 00510 protected function revisionDeleteLink( $rev ) { 00511 $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() ); 00512 if ( $link !== '' ) { 00513 $link = '   ' . $link . ' '; 00514 } 00515 return $link; 00516 } 00517 00521 function renderNewRevision() { 00522 wfProfileIn( __METHOD__ ); 00523 $out = $this->getOutput(); 00524 $revHeader = $this->getRevisionHeader( $this->mNewRev ); 00525 # Add "current version as of X" title 00526 $out->addHTML( "<hr class='diff-hr' /> 00527 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" ); 00528 # Page content may be handled by a hooked call instead... 00529 if ( wfRunHooks( 'ArticleContentOnDiff', array( $this, $out ) ) ) { 00530 $this->loadNewText(); 00531 $out->setRevisionId( $this->mNewid ); 00532 $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() ); 00533 $out->setArticleFlag( true ); 00534 00535 // NOTE: only needed for B/C: custom rendering of JS/CSS via hook 00536 if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) { 00537 // Stolen from Article::view --AG 2007-10-11 00538 // Give hooks a chance to customise the output 00539 // @TODO: standardize this crap into one function 00540 if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->mNewContent, $this->mNewPage, $out ) ) ) { 00541 // NOTE: deprecated hook, B/C only 00542 // use the content object's own rendering 00543 $cnt = $this->mNewRev->getContent(); 00544 $po = $cnt ? $cnt->getParserOutput( $this->mNewRev->getTitle(), $this->mNewRev->getId() ) : null; 00545 $txt = $po ? $po->getText() : ''; 00546 $out->addHTML( $txt ); 00547 } 00548 } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) { 00549 // Handled by extension 00550 } elseif( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) { 00551 // NOTE: deprecated hook, B/C only 00552 // Handled by extension 00553 } else { 00554 // Normal page 00555 if ( $this->getTitle()->equals( $this->mNewPage ) ) { 00556 // If the Title stored in the context is the same as the one 00557 // of the new revision, we can use its associated WikiPage 00558 // object. 00559 $wikiPage = $this->getWikiPage(); 00560 } else { 00561 // Otherwise we need to create our own WikiPage object 00562 $wikiPage = WikiPage::factory( $this->mNewPage ); 00563 } 00564 00565 $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev ); 00566 00567 # Also try to load it as a redirect 00568 $rt = $this->mNewContent ? $this->mNewContent->getRedirectTarget() : null; 00569 00570 if ( $rt ) { 00571 $article = Article::newFromTitle( $this->mNewPage, $this->getContext() ); 00572 $out->addHTML( $article->viewRedirect( $rt ) ); 00573 00574 # WikiPage::getParserOutput() should not return false, but just in case 00575 if ( $parserOutput ) { 00576 # Show categories etc. 00577 $out->addParserOutputNoText( $parserOutput ); 00578 } 00579 } else if ( $parserOutput ) { 00580 $out->addParserOutput( $parserOutput ); 00581 } 00582 } 00583 } 00584 # Add redundant patrol link on bottom... 00585 $out->addHTML( $this->markPatrolledLink() ); 00586 00587 wfProfileOut( __METHOD__ ); 00588 } 00589 00590 protected function getParserOutput( WikiPage $page, Revision $rev ) { 00591 $parserOptions = $page->makeParserOptions( $this->getContext() ); 00592 00593 if ( !$rev->isCurrent() || !$rev->getTitle()->quickUserCan( "edit" ) ) { 00594 $parserOptions->setEditSection( false ); 00595 } 00596 00597 $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() ); 00598 return $parserOutput; 00599 } 00600 00607 function showDiff( $otitle, $ntitle, $notice = '' ) { 00608 $diff = $this->getDiff( $otitle, $ntitle, $notice ); 00609 if ( $diff === false ) { 00610 $this->showMissingRevision(); 00611 return false; 00612 } else { 00613 $this->showDiffStyle(); 00614 $this->getOutput()->addHTML( $diff ); 00615 return true; 00616 } 00617 } 00618 00622 function showDiffStyle() { 00623 $this->getOutput()->addModuleStyles( 'mediawiki.action.history.diff' ); 00624 } 00625 00634 function getDiff( $otitle, $ntitle, $notice = '' ) { 00635 $body = $this->getDiffBody(); 00636 if ( $body === false ) { 00637 return false; 00638 } else { 00639 $multi = $this->getMultiNotice(); 00640 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice ); 00641 } 00642 } 00643 00649 public function getDiffBody() { 00650 global $wgMemc; 00651 wfProfileIn( __METHOD__ ); 00652 $this->mCacheHit = true; 00653 // Check if the diff should be hidden from this user 00654 if ( !$this->loadRevisionData() ) { 00655 wfProfileOut( __METHOD__ ); 00656 return false; 00657 } elseif ( $this->mOldRev && !$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { 00658 wfProfileOut( __METHOD__ ); 00659 return false; 00660 } elseif ( $this->mNewRev && !$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { 00661 wfProfileOut( __METHOD__ ); 00662 return false; 00663 } 00664 // Short-circuit 00665 // If mOldRev is false, it means that the 00666 if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev 00667 && $this->mOldRev->getID() == $this->mNewRev->getID() ) ) 00668 { 00669 wfProfileOut( __METHOD__ ); 00670 return ''; 00671 } 00672 // Cacheable? 00673 $key = false; 00674 if ( $this->mOldid && $this->mNewid ) { 00675 $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 00676 'oldid', $this->mOldid, 'newid', $this->mNewid ); 00677 // Try cache 00678 if ( !$this->mRefreshCache ) { 00679 $difftext = $wgMemc->get( $key ); 00680 if ( $difftext ) { 00681 wfIncrStats( 'diff_cache_hit' ); 00682 $difftext = $this->localiseLineNumbers( $difftext ); 00683 $difftext .= "\n<!-- diff cache key $key -->\n"; 00684 wfProfileOut( __METHOD__ ); 00685 return $difftext; 00686 } 00687 } // don't try to load but save the result 00688 } 00689 $this->mCacheHit = false; 00690 00691 // Loadtext is permission safe, this just clears out the diff 00692 if ( !$this->loadText() ) { 00693 wfProfileOut( __METHOD__ ); 00694 return false; 00695 } 00696 00697 $difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent ); 00698 00699 // Save to cache for 7 days 00700 if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) { 00701 wfIncrStats( 'diff_uncacheable' ); 00702 } elseif ( $key !== false && $difftext !== false ) { 00703 wfIncrStats( 'diff_cache_miss' ); 00704 $wgMemc->set( $key, $difftext, 7 * 86400 ); 00705 } else { 00706 wfIncrStats( 'diff_uncacheable' ); 00707 } 00708 // Replace line numbers with the text in the user's language 00709 if ( $difftext !== false ) { 00710 $difftext = $this->localiseLineNumbers( $difftext ); 00711 } 00712 wfProfileOut( __METHOD__ ); 00713 return $difftext; 00714 } 00715 00720 private function initDiffEngines() { 00721 global $wgExternalDiffEngine; 00722 if ( $wgExternalDiffEngine == 'wikidiff' && !function_exists( 'wikidiff_do_diff' ) ) { 00723 wfProfileIn( __METHOD__ . '-php_wikidiff.so' ); 00724 wfDl( 'php_wikidiff' ); 00725 wfProfileOut( __METHOD__ . '-php_wikidiff.so' ); 00726 } 00727 elseif ( $wgExternalDiffEngine == 'wikidiff2' && !function_exists( 'wikidiff2_do_diff' ) ) { 00728 wfProfileIn( __METHOD__ . '-php_wikidiff2.so' ); 00729 wfDl( 'wikidiff2' ); 00730 wfProfileOut( __METHOD__ . '-php_wikidiff2.so' ); 00731 } 00732 } 00733 00752 function generateContentDiffBody( Content $old, Content $new ) { 00753 if ( !( $old instanceof TextContent ) ) { 00754 throw new MWException( "Diff not implemented for " . get_class( $old ) . "; " 00755 . "override generateContentDiffBody to fix this." ); 00756 } 00757 00758 if ( !( $new instanceof TextContent ) ) { 00759 throw new MWException( "Diff not implemented for " . get_class( $new ) . "; " 00760 . "override generateContentDiffBody to fix this." ); 00761 } 00762 00763 $otext = $old->serialize(); 00764 $ntext = $new->serialize(); 00765 00766 return $this->generateTextDiffBody( $otext, $ntext ); 00767 } 00768 00777 function generateDiffBody( $otext, $ntext ) { 00778 ContentHandler::deprecated( __METHOD__, "1.21" ); 00779 00780 return $this->generateTextDiffBody( $otext, $ntext ); 00781 } 00782 00792 function generateTextDiffBody( $otext, $ntext ) { 00793 global $wgExternalDiffEngine, $wgContLang; 00794 00795 wfProfileIn( __METHOD__ ); 00796 00797 $otext = str_replace( "\r\n", "\n", $otext ); 00798 $ntext = str_replace( "\r\n", "\n", $ntext ); 00799 00800 $this->initDiffEngines(); 00801 00802 if ( $wgExternalDiffEngine == 'wikidiff' && function_exists( 'wikidiff_do_diff' ) ) { 00803 # For historical reasons, external diff engine expects 00804 # input text to be HTML-escaped already 00805 $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) ); 00806 $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) ); 00807 wfProfileOut( __METHOD__ ); 00808 return $wgContLang->unsegmentForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) . 00809 $this->debug( 'wikidiff1' ); 00810 } 00811 00812 if ( $wgExternalDiffEngine == 'wikidiff2' && function_exists( 'wikidiff2_do_diff' ) ) { 00813 # Better external diff engine, the 2 may some day be dropped 00814 # This one does the escaping and segmenting itself 00815 wfProfileIn( 'wikidiff2_do_diff' ); 00816 $text = wikidiff2_do_diff( $otext, $ntext, 2 ); 00817 $text .= $this->debug( 'wikidiff2' ); 00818 wfProfileOut( 'wikidiff2_do_diff' ); 00819 wfProfileOut( __METHOD__ ); 00820 return $text; 00821 } 00822 if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) { 00823 # Diff via the shell 00824 $tmpDir = wfTempDir(); 00825 $tempName1 = tempnam( $tmpDir, 'diff_' ); 00826 $tempName2 = tempnam( $tmpDir, 'diff_' ); 00827 00828 $tempFile1 = fopen( $tempName1, "w" ); 00829 if ( !$tempFile1 ) { 00830 wfProfileOut( __METHOD__ ); 00831 return false; 00832 } 00833 $tempFile2 = fopen( $tempName2, "w" ); 00834 if ( !$tempFile2 ) { 00835 wfProfileOut( __METHOD__ ); 00836 return false; 00837 } 00838 fwrite( $tempFile1, $otext ); 00839 fwrite( $tempFile2, $ntext ); 00840 fclose( $tempFile1 ); 00841 fclose( $tempFile2 ); 00842 $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 ); 00843 wfProfileIn( __METHOD__ . "-shellexec" ); 00844 $difftext = wfShellExec( $cmd ); 00845 $difftext .= $this->debug( "external $wgExternalDiffEngine" ); 00846 wfProfileOut( __METHOD__ . "-shellexec" ); 00847 unlink( $tempName1 ); 00848 unlink( $tempName2 ); 00849 wfProfileOut( __METHOD__ ); 00850 return $difftext; 00851 } 00852 00853 # Native PHP diff 00854 $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) ); 00855 $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) ); 00856 $diffs = new Diff( $ota, $nta ); 00857 $formatter = new TableDiffFormatter(); 00858 $difftext = $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) . 00859 wfProfileOut( __METHOD__ ); 00860 return $difftext; 00861 } 00862 00868 protected function debug( $generator = "internal" ) { 00869 global $wgShowHostnames; 00870 if ( !$this->enableDebugComment ) { 00871 return ''; 00872 } 00873 $data = array( $generator ); 00874 if ( $wgShowHostnames ) { 00875 $data[] = wfHostname(); 00876 } 00877 $data[] = wfTimestamp( TS_DB ); 00878 return "<!-- diff generator: " . 00879 implode( " ", 00880 array_map( 00881 "htmlspecialchars", 00882 $data ) ) . 00883 " -->\n"; 00884 } 00885 00890 function localiseLineNumbers( $text ) { 00891 return preg_replace_callback( '/<!--LINE (\d+)-->/', 00892 array( &$this, 'localiseLineNumbersCb' ), $text ); 00893 } 00894 00895 function localiseLineNumbersCb( $matches ) { 00896 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) return ''; 00897 return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped(); 00898 } 00899 00904 function getMultiNotice() { 00905 if ( !is_object( $this->mOldRev ) || !is_object( $this->mNewRev ) ) { 00906 return ''; 00907 } elseif ( !$this->mOldPage->equals( $this->mNewPage ) ) { 00908 // Comparing two different pages? Count would be meaningless. 00909 return ''; 00910 } 00911 00912 if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) { 00913 $oldRev = $this->mNewRev; // flip 00914 $newRev = $this->mOldRev; // flip 00915 } else { // normal case 00916 $oldRev = $this->mOldRev; 00917 $newRev = $this->mNewRev; 00918 } 00919 00920 $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev ); 00921 if ( $nEdits > 0 ) { 00922 $limit = 100; // use diff-multi-manyusers if too many users 00923 $numUsers = $this->mNewPage->countAuthorsBetween( $oldRev, $newRev, $limit ); 00924 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit ); 00925 } 00926 return ''; // nothing 00927 } 00928 00936 public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) { 00937 if ( $numUsers > $limit ) { 00938 $msg = 'diff-multi-manyusers'; 00939 $numUsers = $limit; 00940 } else { 00941 $msg = 'diff-multi'; 00942 } 00943 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse(); 00944 } 00945 00954 protected function getRevisionHeader( Revision $rev, $complete = '' ) { 00955 $lang = $this->getLanguage(); 00956 $user = $this->getUser(); 00957 $revtimestamp = $rev->getTimestamp(); 00958 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user ); 00959 $dateofrev = $lang->userDate( $revtimestamp, $user ); 00960 $timeofrev = $lang->userTime( $revtimestamp, $user ); 00961 00962 $header = $this->msg( 00963 $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof', 00964 $timestamp, 00965 $dateofrev, 00966 $timeofrev 00967 )->escaped(); 00968 00969 if ( $complete !== 'complete' ) { 00970 return $header; 00971 } 00972 00973 $title = $rev->getTitle(); 00974 00975 $header = Linker::linkKnown( $title, $header, array(), 00976 array( 'oldid' => $rev->getID() ) ); 00977 00978 if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) { 00979 $editQuery = array( 'action' => 'edit' ); 00980 if ( !$rev->isCurrent() ) { 00981 $editQuery['oldid'] = $rev->getID(); 00982 } 00983 00984 $msg = $this->msg( $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold' )->escaped(); 00985 $header .= ' ' . $this->msg( 'parentheses' )->rawParams( 00986 Linker::linkKnown( $title, $msg, array(), $editQuery ) )->plain(); 00987 if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { 00988 $header = Html::rawElement( 'span', array( 'class' => 'history-deleted' ), $header ); 00989 } 00990 } else { 00991 $header = Html::rawElement( 'span', array( 'class' => 'history-deleted' ), $header ); 00992 } 00993 00994 return $header; 00995 } 00996 01002 function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) { 01003 // shared.css sets diff in interface language/dir, but the actual content 01004 // is often in a different language, mostly the page content language/dir 01005 $tableClass = 'diff diff-contentalign-' . htmlspecialchars( $this->getDiffLang()->alignStart() ); 01006 $header = "<table class='$tableClass'>"; 01007 01008 if ( !$diff && !$otitle ) { 01009 $header .= " 01010 <tr style='vertical-align: top;'> 01011 <td class='diff-ntitle'>{$ntitle}</td> 01012 </tr>"; 01013 $multiColspan = 1; 01014 } else { 01015 if ( $diff ) { // Safari/Chrome show broken output if cols not used 01016 $header .= " 01017 <col class='diff-marker' /> 01018 <col class='diff-content' /> 01019 <col class='diff-marker' /> 01020 <col class='diff-content' />"; 01021 $colspan = 2; 01022 $multiColspan = 4; 01023 } else { 01024 $colspan = 1; 01025 $multiColspan = 2; 01026 } 01027 $header .= " 01028 <tr style='vertical-align: top;'> 01029 <td colspan='$colspan' class='diff-otitle'>{$otitle}</td> 01030 <td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td> 01031 </tr>"; 01032 } 01033 01034 if ( $multi != '' ) { 01035 $header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;' class='diff-multi'>{$multi}</td></tr>"; 01036 } 01037 if ( $notice != '' ) { 01038 $header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;'>{$notice}</td></tr>"; 01039 } 01040 01041 return $header . $diff . "</table>"; 01042 } 01043 01048 function setText( $oldText, $newText ) { 01049 ContentHandler::deprecated( __METHOD__, "1.21" ); 01050 01051 $oldContent = ContentHandler::makeContent( $oldText, $this->getTitle() ); 01052 $newContent = ContentHandler::makeContent( $newText, $this->getTitle() ); 01053 01054 $this->setContent( $oldContent, $newContent ); 01055 } 01056 01061 function setContent( Content $oldContent, Content $newContent ) { 01062 $this->mOldContent = $oldContent; 01063 $this->mNewContent = $newContent; 01064 01065 $this->mTextLoaded = 2; 01066 $this->mRevisionsLoaded = true; 01067 } 01068 01074 function setTextLanguage( $lang ) { 01075 $this->mDiffLang = wfGetLangObj( $lang ); 01076 } 01077 01081 private function loadRevisionIds() { 01082 if ( $this->mRevisionsIdsLoaded ) { 01083 return; 01084 } 01085 01086 $this->mRevisionsIdsLoaded = true; 01087 01088 $old = $this->mOldid; 01089 $new = $this->mNewid; 01090 01091 if ( $new === 'prev' ) { 01092 # Show diff between revision $old and the previous one. 01093 # Get previous one from DB. 01094 $this->mNewid = intval( $old ); 01095 $this->mOldid = $this->getTitle()->getPreviousRevisionID( $this->mNewid ); 01096 } elseif ( $new === 'next' ) { 01097 # Show diff between revision $old and the next one. 01098 # Get next one from DB. 01099 $this->mOldid = intval( $old ); 01100 $this->mNewid = $this->getTitle()->getNextRevisionID( $this->mOldid ); 01101 if ( $this->mNewid === false ) { 01102 # if no result, NewId points to the newest old revision. The only newer 01103 # revision is cur, which is "0". 01104 $this->mNewid = 0; 01105 } 01106 } else { 01107 $this->mOldid = intval( $old ); 01108 $this->mNewid = intval( $new ); 01109 wfRunHooks( 'NewDifferenceEngine', array( $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ) ); 01110 } 01111 } 01112 01125 function loadRevisionData() { 01126 if ( $this->mRevisionsLoaded ) { 01127 return true; 01128 } 01129 01130 // Whether it succeeds or fails, we don't want to try again 01131 $this->mRevisionsLoaded = true; 01132 01133 $this->loadRevisionIds(); 01134 01135 // Load the new revision object 01136 $this->mNewRev = $this->mNewid 01137 ? Revision::newFromId( $this->mNewid ) 01138 : Revision::newFromTitle( $this->getTitle(), false, Revision::READ_NORMAL ); 01139 01140 if ( !$this->mNewRev instanceof Revision ) { 01141 return false; 01142 } 01143 01144 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff) 01145 $this->mNewid = $this->mNewRev->getId(); 01146 $this->mNewPage = $this->mNewRev->getTitle(); 01147 01148 // Load the old revision object 01149 $this->mOldRev = false; 01150 if ( $this->mOldid ) { 01151 $this->mOldRev = Revision::newFromId( $this->mOldid ); 01152 } elseif ( $this->mOldid === 0 ) { 01153 $rev = $this->mNewRev->getPrevious(); 01154 if ( $rev ) { 01155 $this->mOldid = $rev->getId(); 01156 $this->mOldRev = $rev; 01157 } else { 01158 // No previous revision; mark to show as first-version only. 01159 $this->mOldid = false; 01160 $this->mOldRev = false; 01161 } 01162 } /* elseif ( $this->mOldid === false ) leave mOldRev false; */ 01163 01164 if ( is_null( $this->mOldRev ) ) { 01165 return false; 01166 } 01167 01168 if ( $this->mOldRev ) { 01169 $this->mOldPage = $this->mOldRev->getTitle(); 01170 } 01171 01172 return true; 01173 } 01174 01180 function loadText() { 01181 if ( $this->mTextLoaded == 2 ) { 01182 return true; 01183 } else { 01184 // Whether it succeeds or fails, we don't want to try again 01185 $this->mTextLoaded = 2; 01186 } 01187 01188 if ( !$this->loadRevisionData() ) { 01189 return false; 01190 } 01191 if ( $this->mOldRev ) { 01192 $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); 01193 if ( $this->mOldContent === null ) { 01194 return false; 01195 } 01196 } 01197 if ( $this->mNewRev ) { 01198 $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); 01199 if ( $this->mNewContent === null ) { 01200 return false; 01201 } 01202 } 01203 return true; 01204 } 01205 01211 function loadNewText() { 01212 if ( $this->mTextLoaded >= 1 ) { 01213 return true; 01214 } else { 01215 $this->mTextLoaded = 1; 01216 } 01217 if ( !$this->loadRevisionData() ) { 01218 return false; 01219 } 01220 $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); 01221 return true; 01222 } 01223 }