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