MediaWiki
REL1_20
|
00001 <?php 00030 define( 'MW_DIFF_VERSION', '1.11a' ); 00031 00036 class DifferenceEngine extends ContextSource { 00040 var $mOldid, $mNewid; 00041 var $mOldtext, $mNewtext; 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 $urls = array( 00228 'File' => array( 'Extension' => 'wiki', 'URL' => 00229 # This should be mOldPage, but it may not be set, see below. 00230 $this->mNewPage->getCanonicalURL( array( 00231 'action' => 'raw', 'oldid' => $this->mOldid ) ) 00232 ), 00233 'File2' => array( 'Extension' => 'wiki', 'URL' => 00234 $this->mNewPage->getCanonicalURL( array( 00235 'action' => 'raw', 'oldid' => $this->mNewid ) ) 00236 ), 00237 ); 00238 00239 $externalEditor = new ExternalEdit( $this->getContext(), $urls ); 00240 $externalEditor->execute(); 00241 00242 wfProfileOut( __METHOD__ ); 00243 return; 00244 } 00245 00246 $rollback = ''; 00247 $undoLink = ''; 00248 00249 $query = array(); 00250 # Carry over 'diffonly' param via navigation links 00251 if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) { 00252 $query['diffonly'] = $diffOnly; 00253 } 00254 # Cascade unhide param in links for easy deletion browsing 00255 if ( $this->unhide ) { 00256 $query['unhide'] = 1; 00257 } 00258 00259 # Check if one of the revisions is deleted/suppressed 00260 $deleted = $suppressed = false; 00261 $allowed = $this->mNewRev->userCan( Revision::DELETED_TEXT, $user ); 00262 00263 # mOldRev is false if the difference engine is called with a "vague" query for 00264 # a diff between a version V and its previous version V' AND the version V 00265 # is the first version of that article. In that case, V' does not exist. 00266 if ( $this->mOldRev === false ) { 00267 $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) ); 00268 $samePage = true; 00269 $oldHeader = ''; 00270 } else { 00271 wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) ); 00272 00273 $sk = $this->getSkin(); 00274 if ( method_exists( $sk, 'suppressQuickbar' ) ) { 00275 $sk->suppressQuickbar(); 00276 } 00277 00278 if ( $this->mNewPage->equals( $this->mOldPage ) ) { 00279 $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) ); 00280 $samePage = true; 00281 } else { 00282 $out->setPageTitle( $this->msg( 'difference-title-multipage', $this->mOldPage->getPrefixedText(), 00283 $this->mNewPage->getPrefixedText() ) ); 00284 $out->addSubtitle( $this->msg( 'difference-multipage' ) ); 00285 $samePage = false; 00286 } 00287 00288 if ( $samePage && $this->mNewPage->quickUserCan( 'edit', $user ) ) { 00289 if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) { 00290 $out->preventClickjacking(); 00291 $rollback = '   ' . Linker::generateRollback( $this->mNewRev, $this->getContext() ); 00292 } 00293 if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) { 00294 $undoLink = ' ' . $this->msg( 'parentheses' )->rawParams( 00295 Html::element( 'a', array( 00296 'href' => $this->mNewPage->getLocalUrl( array( 00297 'action' => 'edit', 00298 'undoafter' => $this->mOldid, 00299 'undo' => $this->mNewid ) ), 00300 'title' => Linker::titleAttrib( 'undo' ) 00301 ), 00302 $this->msg( 'editundo' )->text() 00303 ) )->escaped(); 00304 } 00305 } 00306 00307 # Make "previous revision link" 00308 if ( $samePage && $this->mOldRev->getPrevious() ) { 00309 $prevlink = Linker::linkKnown( 00310 $this->mOldPage, 00311 $this->msg( 'previousdiff' )->escaped(), 00312 array( 'id' => 'differences-prevlink' ), 00313 array( 'diff' => 'prev', 'oldid' => $this->mOldid ) + $query 00314 ); 00315 } else { 00316 $prevlink = ' '; 00317 } 00318 00319 if ( $this->mOldRev->isMinor() ) { 00320 $oldminor = ChangesList::flag( 'minor' ); 00321 } else { 00322 $oldminor = ''; 00323 } 00324 00325 $ldel = $this->revisionDeleteLink( $this->mOldRev ); 00326 $oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' ); 00327 00328 $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' . 00329 '<div id="mw-diff-otitle2">' . 00330 Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' . 00331 '<div id="mw-diff-otitle3">' . $oldminor . 00332 Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' . 00333 '<div id="mw-diff-otitle4">' . $prevlink . '</div>'; 00334 00335 if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) { 00336 $deleted = true; // old revisions text is hidden 00337 if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) { 00338 $suppressed = true; // also suppressed 00339 } 00340 } 00341 00342 # Check if this user can see the revisions 00343 if ( !$this->mOldRev->userCan( Revision::DELETED_TEXT, $user ) ) { 00344 $allowed = false; 00345 } 00346 } 00347 00348 # Make "next revision link" 00349 # Skip next link on the top revision 00350 if ( $samePage && !$this->mNewRev->isCurrent() ) { 00351 $nextlink = Linker::linkKnown( 00352 $this->mNewPage, 00353 $this->msg( 'nextdiff' )->escaped(), 00354 array( 'id' => 'differences-nextlink' ), 00355 array( 'diff' => 'next', 'oldid' => $this->mNewid ) + $query 00356 ); 00357 } else { 00358 $nextlink = ' '; 00359 } 00360 00361 if ( $this->mNewRev->isMinor() ) { 00362 $newminor = ChangesList::flag( 'minor' ); 00363 } else { 00364 $newminor = ''; 00365 } 00366 00367 # Handle RevisionDelete links... 00368 $rdel = $this->revisionDeleteLink( $this->mNewRev ); 00369 $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) . $undoLink; 00370 00371 $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' . 00372 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) . 00373 " $rollback</div>" . 00374 '<div id="mw-diff-ntitle3">' . $newminor . 00375 Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' . 00376 '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>'; 00377 00378 if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) { 00379 $deleted = true; // new revisions text is hidden 00380 if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) 00381 $suppressed = true; // also suppressed 00382 } 00383 00384 # If the diff cannot be shown due to a deleted revision, then output 00385 # the diff header and links to unhide (if available)... 00386 if ( $deleted && ( !$this->unhide || !$allowed ) ) { 00387 $this->showDiffStyle(); 00388 $multi = $this->getMultiNotice(); 00389 $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) ); 00390 if ( !$allowed ) { 00391 $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff'; 00392 # Give explanation for why revision is not visible 00393 $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n", 00394 array( $msg ) ); 00395 } else { 00396 # Give explanation and add a link to view the diff... 00397 $link = $this->getTitle()->getFullUrl( $this->getRequest()->appendQueryValue( 'unhide', '1', true ) ); 00398 $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff'; 00399 $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n", array( $msg, $link ) ); 00400 } 00401 # Otherwise, output a regular diff... 00402 } else { 00403 # Add deletion notice if the user is viewing deleted content 00404 $notice = ''; 00405 if ( $deleted ) { 00406 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view'; 00407 $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" . $this->msg( $msg )->parse() . "</div>\n"; 00408 } 00409 $this->showDiff( $oldHeader, $newHeader, $notice ); 00410 if ( !$diffOnly ) { 00411 $this->renderNewRevision(); 00412 } 00413 } 00414 wfProfileOut( __METHOD__ ); 00415 } 00416 00425 protected function markPatrolledLink() { 00426 global $wgUseRCPatrol; 00427 00428 if ( $this->mMarkPatrolledLink === null ) { 00429 // Prepare a change patrol link, if applicable 00430 if ( $wgUseRCPatrol && $this->mNewPage->quickUserCan( 'patrol', $this->getUser() ) ) { 00431 // If we've been given an explicit change identifier, use it; saves time 00432 if ( $this->mRcidMarkPatrolled ) { 00433 $rcid = $this->mRcidMarkPatrolled; 00434 $rc = RecentChange::newFromId( $rcid ); 00435 // Already patrolled? 00436 $rcid = is_object( $rc ) && !$rc->getAttribute( 'rc_patrolled' ) ? $rcid : 0; 00437 } else { 00438 // Look for an unpatrolled change corresponding to this diff 00439 $db = wfGetDB( DB_SLAVE ); 00440 $change = RecentChange::newFromConds( 00441 array( 00442 // Redundant user,timestamp condition so we can use the existing index 00443 'rc_user_text' => $this->mNewRev->getRawUserText(), 00444 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ), 00445 'rc_this_oldid' => $this->mNewid, 00446 'rc_last_oldid' => $this->mOldid, 00447 'rc_patrolled' => 0 00448 ), 00449 __METHOD__ 00450 ); 00451 if ( $change instanceof RecentChange ) { 00452 $rcid = $change->mAttribs['rc_id']; 00453 $this->mRcidMarkPatrolled = $rcid; 00454 } else { 00455 // None found 00456 $rcid = 0; 00457 } 00458 } 00459 // Build the link 00460 if ( $rcid ) { 00461 $this->getOutput()->preventClickjacking(); 00462 $token = $this->getUser()->getEditToken( $rcid ); 00463 $this->mMarkPatrolledLink = ' <span class="patrollink">[' . Linker::linkKnown( 00464 $this->mNewPage, 00465 $this->msg( 'markaspatrolleddiff' )->escaped(), 00466 array(), 00467 array( 00468 'action' => 'markpatrolled', 00469 'rcid' => $rcid, 00470 'token' => $token, 00471 ) 00472 ) . ']</span>'; 00473 } else { 00474 $this->mMarkPatrolledLink = ''; 00475 } 00476 } else { 00477 $this->mMarkPatrolledLink = ''; 00478 } 00479 } 00480 00481 return $this->mMarkPatrolledLink; 00482 } 00483 00488 protected function revisionDeleteLink( $rev ) { 00489 $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() ); 00490 if ( $link !== '' ) { 00491 $link = '   ' . $link . ' '; 00492 } 00493 return $link; 00494 } 00495 00499 function renderNewRevision() { 00500 wfProfileIn( __METHOD__ ); 00501 $out = $this->getOutput(); 00502 $revHeader = $this->getRevisionHeader( $this->mNewRev ); 00503 # Add "current version as of X" title 00504 $out->addHTML( "<hr class='diff-hr' /> 00505 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" ); 00506 # Page content may be handled by a hooked call instead... 00507 if ( wfRunHooks( 'ArticleContentOnDiff', array( $this, $out ) ) ) { 00508 $this->loadNewText(); 00509 $out->setRevisionId( $this->mNewid ); 00510 $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() ); 00511 $out->setArticleFlag( true ); 00512 00513 if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) { 00514 // Stolen from Article::view --AG 2007-10-11 00515 // Give hooks a chance to customise the output 00516 // @TODO: standardize this crap into one function 00517 if ( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mNewPage, $out ) ) ) { 00518 // Wrap the whole lot in a <pre> and don't parse 00519 $m = array(); 00520 preg_match( '!\.(css|js)$!u', $this->mNewPage->getText(), $m ); 00521 $out->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" ); 00522 $out->addHTML( htmlspecialchars( $this->mNewtext ) ); 00523 $out->addHTML( "\n</pre>\n" ); 00524 } 00525 } elseif ( !wfRunHooks( 'ArticleViewCustom', array( $this->mNewtext, $this->mNewPage, $out ) ) ) { 00526 // Handled by extension 00527 } else { 00528 // Normal page 00529 if ( $this->getTitle()->equals( $this->mNewPage ) ) { 00530 // If the Title stored in the context is the same as the one 00531 // of the new revision, we can use its associated WikiPage 00532 // object. 00533 $wikiPage = $this->getWikiPage(); 00534 } else { 00535 // Otherwise we need to create our own WikiPage object 00536 $wikiPage = WikiPage::factory( $this->mNewPage ); 00537 } 00538 00539 $parserOptions = $wikiPage->makeParserOptions( $this->getContext() ); 00540 00541 if ( !$this->mNewRev->isCurrent() ) { 00542 $parserOptions->setEditSection( false ); 00543 } 00544 00545 $parserOutput = $wikiPage->getParserOutput( $parserOptions, $this->mNewid ); 00546 00547 # WikiPage::getParserOutput() should not return false, but just in case 00548 if( $parserOutput ) { 00549 $out->addParserOutput( $parserOutput ); 00550 } 00551 } 00552 } 00553 # Add redundant patrol link on bottom... 00554 $out->addHTML( $this->markPatrolledLink() ); 00555 00556 wfProfileOut( __METHOD__ ); 00557 } 00558 00565 function showDiff( $otitle, $ntitle, $notice = '' ) { 00566 $diff = $this->getDiff( $otitle, $ntitle, $notice ); 00567 if ( $diff === false ) { 00568 $this->showMissingRevision(); 00569 return false; 00570 } else { 00571 $this->showDiffStyle(); 00572 $this->getOutput()->addHTML( $diff ); 00573 return true; 00574 } 00575 } 00576 00580 function showDiffStyle() { 00581 $this->getOutput()->addModuleStyles( 'mediawiki.action.history.diff' ); 00582 } 00583 00592 function getDiff( $otitle, $ntitle, $notice = '' ) { 00593 $body = $this->getDiffBody(); 00594 if ( $body === false ) { 00595 return false; 00596 } else { 00597 $multi = $this->getMultiNotice(); 00598 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice ); 00599 } 00600 } 00601 00607 public function getDiffBody() { 00608 global $wgMemc; 00609 wfProfileIn( __METHOD__ ); 00610 $this->mCacheHit = true; 00611 // Check if the diff should be hidden from this user 00612 if ( !$this->loadRevisionData() ) { 00613 wfProfileOut( __METHOD__ ); 00614 return false; 00615 } elseif ( $this->mOldRev && !$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { 00616 wfProfileOut( __METHOD__ ); 00617 return false; 00618 } elseif ( $this->mNewRev && !$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { 00619 wfProfileOut( __METHOD__ ); 00620 return false; 00621 } 00622 // Short-circuit 00623 // If mOldRev is false, it means that the 00624 if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev 00625 && $this->mOldRev->getID() == $this->mNewRev->getID() ) ) 00626 { 00627 wfProfileOut( __METHOD__ ); 00628 return ''; 00629 } 00630 // Cacheable? 00631 $key = false; 00632 if ( $this->mOldid && $this->mNewid ) { 00633 $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 00634 'oldid', $this->mOldid, 'newid', $this->mNewid ); 00635 // Try cache 00636 if ( !$this->mRefreshCache ) { 00637 $difftext = $wgMemc->get( $key ); 00638 if ( $difftext ) { 00639 wfIncrStats( 'diff_cache_hit' ); 00640 $difftext = $this->localiseLineNumbers( $difftext ); 00641 $difftext .= "\n<!-- diff cache key $key -->\n"; 00642 wfProfileOut( __METHOD__ ); 00643 return $difftext; 00644 } 00645 } // don't try to load but save the result 00646 } 00647 $this->mCacheHit = false; 00648 00649 // Loadtext is permission safe, this just clears out the diff 00650 if ( !$this->loadText() ) { 00651 wfProfileOut( __METHOD__ ); 00652 return false; 00653 } 00654 00655 $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext ); 00656 00657 // Save to cache for 7 days 00658 if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) { 00659 wfIncrStats( 'diff_uncacheable' ); 00660 } elseif ( $key !== false && $difftext !== false ) { 00661 wfIncrStats( 'diff_cache_miss' ); 00662 $wgMemc->set( $key, $difftext, 7 * 86400 ); 00663 } else { 00664 wfIncrStats( 'diff_uncacheable' ); 00665 } 00666 // Replace line numbers with the text in the user's language 00667 if ( $difftext !== false ) { 00668 $difftext = $this->localiseLineNumbers( $difftext ); 00669 } 00670 wfProfileOut( __METHOD__ ); 00671 return $difftext; 00672 } 00673 00678 private function initDiffEngines() { 00679 global $wgExternalDiffEngine; 00680 if ( $wgExternalDiffEngine == 'wikidiff' && !function_exists( 'wikidiff_do_diff' ) ) { 00681 wfProfileIn( __METHOD__ . '-php_wikidiff.so' ); 00682 wfDl( 'php_wikidiff' ); 00683 wfProfileOut( __METHOD__ . '-php_wikidiff.so' ); 00684 } 00685 elseif ( $wgExternalDiffEngine == 'wikidiff2' && !function_exists( 'wikidiff2_do_diff' ) ) { 00686 wfProfileIn( __METHOD__ . '-php_wikidiff2.so' ); 00687 wfDl( 'wikidiff2' ); 00688 wfProfileOut( __METHOD__ . '-php_wikidiff2.so' ); 00689 } 00690 } 00691 00699 function generateDiffBody( $otext, $ntext ) { 00700 global $wgExternalDiffEngine, $wgContLang; 00701 00702 wfProfileIn( __METHOD__ ); 00703 00704 $otext = str_replace( "\r\n", "\n", $otext ); 00705 $ntext = str_replace( "\r\n", "\n", $ntext ); 00706 00707 $this->initDiffEngines(); 00708 00709 if ( $wgExternalDiffEngine == 'wikidiff' && function_exists( 'wikidiff_do_diff' ) ) { 00710 # For historical reasons, external diff engine expects 00711 # input text to be HTML-escaped already 00712 $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) ); 00713 $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) ); 00714 wfProfileOut( __METHOD__ ); 00715 return $wgContLang->unsegmentForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) . 00716 $this->debug( 'wikidiff1' ); 00717 } 00718 00719 if ( $wgExternalDiffEngine == 'wikidiff2' && function_exists( 'wikidiff2_do_diff' ) ) { 00720 # Better external diff engine, the 2 may some day be dropped 00721 # This one does the escaping and segmenting itself 00722 wfProfileIn( 'wikidiff2_do_diff' ); 00723 $text = wikidiff2_do_diff( $otext, $ntext, 2 ); 00724 $text .= $this->debug( 'wikidiff2' ); 00725 wfProfileOut( 'wikidiff2_do_diff' ); 00726 wfProfileOut( __METHOD__ ); 00727 return $text; 00728 } 00729 if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) { 00730 # Diff via the shell 00731 $tmpDir = wfTempDir(); 00732 $tempName1 = tempnam( $tmpDir, 'diff_' ); 00733 $tempName2 = tempnam( $tmpDir, 'diff_' ); 00734 00735 $tempFile1 = fopen( $tempName1, "w" ); 00736 if ( !$tempFile1 ) { 00737 wfProfileOut( __METHOD__ ); 00738 return false; 00739 } 00740 $tempFile2 = fopen( $tempName2, "w" ); 00741 if ( !$tempFile2 ) { 00742 wfProfileOut( __METHOD__ ); 00743 return false; 00744 } 00745 fwrite( $tempFile1, $otext ); 00746 fwrite( $tempFile2, $ntext ); 00747 fclose( $tempFile1 ); 00748 fclose( $tempFile2 ); 00749 $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 ); 00750 wfProfileIn( __METHOD__ . "-shellexec" ); 00751 $difftext = wfShellExec( $cmd ); 00752 $difftext .= $this->debug( "external $wgExternalDiffEngine" ); 00753 wfProfileOut( __METHOD__ . "-shellexec" ); 00754 unlink( $tempName1 ); 00755 unlink( $tempName2 ); 00756 wfProfileOut( __METHOD__ ); 00757 return $difftext; 00758 } 00759 00760 # Native PHP diff 00761 $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) ); 00762 $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) ); 00763 $diffs = new Diff( $ota, $nta ); 00764 $formatter = new TableDiffFormatter(); 00765 $difftext = $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) . 00766 wfProfileOut( __METHOD__ ); 00767 return $difftext; 00768 } 00769 00775 protected function debug( $generator = "internal" ) { 00776 global $wgShowHostnames; 00777 if ( !$this->enableDebugComment ) { 00778 return ''; 00779 } 00780 $data = array( $generator ); 00781 if ( $wgShowHostnames ) { 00782 $data[] = wfHostname(); 00783 } 00784 $data[] = wfTimestamp( TS_DB ); 00785 return "<!-- diff generator: " . 00786 implode( " ", 00787 array_map( 00788 "htmlspecialchars", 00789 $data ) ) . 00790 " -->\n"; 00791 } 00792 00797 function localiseLineNumbers( $text ) { 00798 return preg_replace_callback( '/<!--LINE (\d+)-->/', 00799 array( &$this, 'localiseLineNumbersCb' ), $text ); 00800 } 00801 00802 function localiseLineNumbersCb( $matches ) { 00803 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) return ''; 00804 return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped(); 00805 } 00806 00807 00812 function getMultiNotice() { 00813 if ( !is_object( $this->mOldRev ) || !is_object( $this->mNewRev ) ) { 00814 return ''; 00815 } elseif ( !$this->mOldPage->equals( $this->mNewPage ) ) { 00816 // Comparing two different pages? Count would be meaningless. 00817 return ''; 00818 } 00819 00820 if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) { 00821 $oldRev = $this->mNewRev; // flip 00822 $newRev = $this->mOldRev; // flip 00823 } else { // normal case 00824 $oldRev = $this->mOldRev; 00825 $newRev = $this->mNewRev; 00826 } 00827 00828 $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev ); 00829 if ( $nEdits > 0 ) { 00830 $limit = 100; // use diff-multi-manyusers if too many users 00831 $numUsers = $this->mNewPage->countAuthorsBetween( $oldRev, $newRev, $limit ); 00832 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit ); 00833 } 00834 return ''; // nothing 00835 } 00836 00844 public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) { 00845 if ( $numUsers > $limit ) { 00846 $msg = 'diff-multi-manyusers'; 00847 $numUsers = $limit; 00848 } else { 00849 $msg = 'diff-multi'; 00850 } 00851 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse(); 00852 } 00853 00862 private function getRevisionHeader( Revision $rev, $complete = '' ) { 00863 $lang = $this->getLanguage(); 00864 $user = $this->getUser(); 00865 $revtimestamp = $rev->getTimestamp(); 00866 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user ); 00867 $dateofrev = $lang->userDate( $revtimestamp, $user ); 00868 $timeofrev = $lang->userTime( $revtimestamp, $user ); 00869 00870 $header = $this->msg( 00871 $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof', 00872 $timestamp, 00873 $dateofrev, 00874 $timeofrev 00875 )->escaped(); 00876 00877 if ( $complete !== 'complete' ) { 00878 return $header; 00879 } 00880 00881 $title = $rev->getTitle(); 00882 00883 $header = Linker::linkKnown( $title, $header, array(), 00884 array( 'oldid' => $rev->getID() ) ); 00885 00886 if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) { 00887 $editQuery = array( 'action' => 'edit' ); 00888 if ( !$rev->isCurrent() ) { 00889 $editQuery['oldid'] = $rev->getID(); 00890 } 00891 00892 $msg = $this->msg( $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold' )->escaped(); 00893 $header .= ' ' . $this->msg( 'parentheses' )->rawParams( 00894 Linker::linkKnown( $title, $msg, array(), $editQuery ) )->plain(); 00895 if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { 00896 $header = Html::rawElement( 'span', array( 'class' => 'history-deleted' ), $header ); 00897 } 00898 } else { 00899 $header = Html::rawElement( 'span', array( 'class' => 'history-deleted' ), $header ); 00900 } 00901 00902 return $header; 00903 } 00904 00910 function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) { 00911 // shared.css sets diff in interface language/dir, but the actual content 00912 // is often in a different language, mostly the page content language/dir 00913 $tableClass = 'diff diff-contentalign-' . htmlspecialchars( $this->getDiffLang()->alignStart() ); 00914 $header = "<table class='$tableClass'>"; 00915 00916 if ( !$diff && !$otitle ) { 00917 $header .= " 00918 <tr style='vertical-align: top;'> 00919 <td class='diff-ntitle'>{$ntitle}</td> 00920 </tr>"; 00921 $multiColspan = 1; 00922 } else { 00923 if ( $diff ) { // Safari/Chrome show broken output if cols not used 00924 $header .= " 00925 <col class='diff-marker' /> 00926 <col class='diff-content' /> 00927 <col class='diff-marker' /> 00928 <col class='diff-content' />"; 00929 $colspan = 2; 00930 $multiColspan = 4; 00931 } else { 00932 $colspan = 1; 00933 $multiColspan = 2; 00934 } 00935 $header .= " 00936 <tr style='vertical-align: top;'> 00937 <td colspan='$colspan' class='diff-otitle'>{$otitle}</td> 00938 <td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td> 00939 </tr>"; 00940 } 00941 00942 if ( $multi != '' ) { 00943 $header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;' class='diff-multi'>{$multi}</td></tr>"; 00944 } 00945 if ( $notice != '' ) { 00946 $header .= "<tr><td colspan='{$multiColspan}' style='text-align: center;'>{$notice}</td></tr>"; 00947 } 00948 00949 return $header . $diff . "</table>"; 00950 } 00951 00955 function setText( $oldText, $newText ) { 00956 $this->mOldtext = $oldText; 00957 $this->mNewtext = $newText; 00958 $this->mTextLoaded = 2; 00959 $this->mRevisionsLoaded = true; 00960 } 00961 00967 function setTextLanguage( $lang ) { 00968 $this->mDiffLang = wfGetLangObj( $lang ); 00969 } 00970 00974 private function loadRevisionIds() { 00975 if ( $this->mRevisionsIdsLoaded ) { 00976 return; 00977 } 00978 00979 $this->mRevisionsIdsLoaded = true; 00980 00981 $old = $this->mOldid; 00982 $new = $this->mNewid; 00983 00984 if ( $new === 'prev' ) { 00985 # Show diff between revision $old and the previous one. 00986 # Get previous one from DB. 00987 $this->mNewid = intval( $old ); 00988 $this->mOldid = $this->getTitle()->getPreviousRevisionID( $this->mNewid ); 00989 } elseif ( $new === 'next' ) { 00990 # Show diff between revision $old and the next one. 00991 # Get next one from DB. 00992 $this->mOldid = intval( $old ); 00993 $this->mNewid = $this->getTitle()->getNextRevisionID( $this->mOldid ); 00994 if ( $this->mNewid === false ) { 00995 # if no result, NewId points to the newest old revision. The only newer 00996 # revision is cur, which is "0". 00997 $this->mNewid = 0; 00998 } 00999 } else { 01000 $this->mOldid = intval( $old ); 01001 $this->mNewid = intval( $new ); 01002 wfRunHooks( 'NewDifferenceEngine', array( $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ) ); 01003 } 01004 } 01005 01018 function loadRevisionData() { 01019 if ( $this->mRevisionsLoaded ) { 01020 return true; 01021 } 01022 01023 // Whether it succeeds or fails, we don't want to try again 01024 $this->mRevisionsLoaded = true; 01025 01026 $this->loadRevisionIds(); 01027 01028 // Load the new revision object 01029 $this->mNewRev = $this->mNewid 01030 ? Revision::newFromId( $this->mNewid ) 01031 : Revision::newFromTitle( $this->getTitle(), false, Revision::READ_NORMAL ); 01032 01033 if ( !$this->mNewRev instanceof Revision ) { 01034 return false; 01035 } 01036 01037 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff) 01038 $this->mNewid = $this->mNewRev->getId(); 01039 $this->mNewPage = $this->mNewRev->getTitle(); 01040 01041 // Load the old revision object 01042 $this->mOldRev = false; 01043 if ( $this->mOldid ) { 01044 $this->mOldRev = Revision::newFromId( $this->mOldid ); 01045 } elseif ( $this->mOldid === 0 ) { 01046 $rev = $this->mNewRev->getPrevious(); 01047 if ( $rev ) { 01048 $this->mOldid = $rev->getId(); 01049 $this->mOldRev = $rev; 01050 } else { 01051 // No previous revision; mark to show as first-version only. 01052 $this->mOldid = false; 01053 $this->mOldRev = false; 01054 } 01055 } /* elseif ( $this->mOldid === false ) leave mOldRev false; */ 01056 01057 if ( is_null( $this->mOldRev ) ) { 01058 return false; 01059 } 01060 01061 if ( $this->mOldRev ) { 01062 $this->mOldPage = $this->mOldRev->getTitle(); 01063 } 01064 01065 return true; 01066 } 01067 01073 function loadText() { 01074 if ( $this->mTextLoaded == 2 ) { 01075 return true; 01076 } else { 01077 // Whether it succeeds or fails, we don't want to try again 01078 $this->mTextLoaded = 2; 01079 } 01080 01081 if ( !$this->loadRevisionData() ) { 01082 return false; 01083 } 01084 if ( $this->mOldRev ) { 01085 $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER ); 01086 if ( $this->mOldtext === false ) { 01087 return false; 01088 } 01089 } 01090 if ( $this->mNewRev ) { 01091 $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER ); 01092 if ( $this->mNewtext === false ) { 01093 return false; 01094 } 01095 } 01096 return true; 01097 } 01098 01104 function loadNewText() { 01105 if ( $this->mTextLoaded >= 1 ) { 01106 return true; 01107 } else { 01108 $this->mTextLoaded = 1; 01109 } 01110 if ( !$this->loadRevisionData() ) { 01111 return false; 01112 } 01113 $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER ); 01114 return true; 01115 } 01116 }