MediaWiki
REL1_19
|
00001 <?php 00015 define( 'MW_DIFF_VERSION', '1.11a' ); 00016 00021 class DifferenceEngine extends ContextSource { 00025 var $mOldid, $mNewid; 00026 var $mOldtext, $mNewtext; 00027 protected $mDiffLang; 00028 00032 var $mOldPage, $mNewPage; 00033 var $mRcidMarkPatrolled; 00034 00038 var $mOldRev, $mNewRev; 00039 private $mRevisionsIdsLoaded = false; // Have the revisions IDs been loaded 00040 var $mRevisionsLoaded = false; // Have the revisions been loaded 00041 var $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2? 00042 var $mCacheHit = false; // Was the diff fetched from cache? 00043 00049 var $enableDebugComment = false; 00050 00051 // If true, line X is not displayed when X is 1, for example to increase 00052 // readability and conserve space with many small diffs. 00053 protected $mReducedLineNumbers = false; 00054 00055 // Link to action=markpatrolled 00056 protected $mMarkPatrolledLink = null; 00057 00058 protected $unhide = false; # show rev_deleted content if allowed 00070 function __construct( $context = null, $old = 0, $new = 0, $rcid = 0, 00071 $refreshCache = false, $unhide = false ) 00072 { 00073 if ( $context instanceof IContextSource ) { 00074 $this->setContext( $context ); 00075 } 00076 00077 wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'\n" ); 00078 00079 $this->mOldid = $old; 00080 $this->mNewid = $new; 00081 $this->mRcidMarkPatrolled = intval( $rcid ); # force it to be an integer 00082 $this->mRefreshCache = $refreshCache; 00083 $this->unhide = $unhide; 00084 } 00085 00089 function setReducedLineNumbers( $value = true ) { 00090 $this->mReducedLineNumbers = $value; 00091 } 00092 00096 function getDiffLang() { 00097 if ( $this->mDiffLang === null ) { 00098 # Default language in which the diff text is written. 00099 $this->mDiffLang = $this->getTitle()->getPageLanguage(); 00100 } 00101 return $this->mDiffLang; 00102 } 00103 00107 function wasCacheHit() { 00108 return $this->mCacheHit; 00109 } 00110 00114 function getOldid() { 00115 $this->loadRevisionIds(); 00116 return $this->mOldid; 00117 } 00118 00122 function getNewid() { 00123 $this->loadRevisionIds(); 00124 return $this->mNewid; 00125 } 00126 00134 function deletedLink( $id ) { 00135 if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) { 00136 $dbr = wfGetDB( DB_SLAVE ); 00137 $row = $dbr->selectRow('archive', '*', 00138 array( 'ar_rev_id' => $id ), 00139 __METHOD__ ); 00140 if ( $row ) { 00141 $rev = Revision::newFromArchiveRow( $row ); 00142 $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title ); 00143 return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( array( 00144 'target' => $title->getPrefixedText(), 00145 'timestamp' => $rev->getTimestamp() 00146 )); 00147 } 00148 } 00149 return false; 00150 } 00151 00158 function deletedIdMarker( $id ) { 00159 $link = $this->deletedLink( $id ); 00160 if ( $link ) { 00161 return "[$link $id]"; 00162 } else { 00163 return $id; 00164 } 00165 } 00166 00167 function showDiffPage( $diffOnly = false ) { 00168 wfProfileIn( __METHOD__ ); 00169 00170 # Allow frames except in certain special cases 00171 $out = $this->getOutput(); 00172 $out->allowClickjacking(); 00173 $out->setRobotPolicy( 'noindex,nofollow' ); 00174 00175 if ( !$this->loadRevisionData() ) { 00176 // Sounds like a deleted revision... Let's see what we can do. 00177 $t = $this->getTitle()->getPrefixedText(); 00178 $d = $this->msg( 'missingarticle-diff', 00179 $this->deletedIdMarker( $this->mOldid ), 00180 $this->deletedIdMarker( $this->mNewid ) )->escaped(); 00181 $out->setPageTitle( $this->msg( 'errorpagetitle' ) ); 00182 $out->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", "<span class='plainlinks'>$d</span>" ); 00183 wfProfileOut( __METHOD__ ); 00184 return; 00185 } 00186 00187 $user = $this->getUser(); 00188 $permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user ); 00189 if ( $this->mOldPage ) { # mOldPage might not be set, see below. 00190 $permErrors = wfMergeErrorArrays( $permErrors, 00191 $this->mOldPage->getUserPermissionsErrors( 'read', $user ) ); 00192 } 00193 if ( count( $permErrors ) ) { 00194 wfProfileOut( __METHOD__ ); 00195 throw new PermissionsError( 'read', $permErrors ); 00196 } 00197 00198 # If external diffs are enabled both globally and for the user, 00199 # we'll use the application/x-external-editor interface to call 00200 # an external diff tool like kompare, kdiff3, etc. 00201 if ( ExternalEdit::useExternalEngine( $this->getContext(), 'diff' ) ) { 00202 $urls = array( 00203 'File' => array( 'Extension' => 'wiki', 'URL' => 00204 # This should be mOldPage, but it may not be set, see below. 00205 $this->mNewPage->getCanonicalURL( array( 00206 'action' => 'raw', 'oldid' => $this->mOldid ) ) 00207 ), 00208 'File2' => array( 'Extension' => 'wiki', 'URL' => 00209 $this->mNewPage->getCanonicalURL( array( 00210 'action' => 'raw', 'oldid' => $this->mNewid ) ) 00211 ), 00212 ); 00213 00214 $externalEditor = new ExternalEdit( $this->getContext(), $urls ); 00215 $externalEditor->execute(); 00216 00217 wfProfileOut( __METHOD__ ); 00218 return; 00219 } 00220 00221 $rollback = ''; 00222 $undoLink = ''; 00223 00224 $query = array(); 00225 # Carry over 'diffonly' param via navigation links 00226 if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) { 00227 $query['diffonly'] = $diffOnly; 00228 } 00229 # Cascade unhide param in links for easy deletion browsing 00230 if ( $this->unhide ) { 00231 $query['unhide'] = 1; 00232 } 00233 00234 # Check if one of the revisions is deleted/suppressed 00235 $deleted = $suppressed = false; 00236 $allowed = $this->mNewRev->userCan( Revision::DELETED_TEXT, $user ); 00237 00238 # mOldRev is false if the difference engine is called with a "vague" query for 00239 # a diff between a version V and its previous version V' AND the version V 00240 # is the first version of that article. In that case, V' does not exist. 00241 if ( $this->mOldRev === false ) { 00242 $out->setPageTitle( $this->mNewPage->getPrefixedText() ); 00243 $out->addSubtitle( $this->msg( 'difference' ) ); 00244 $samePage = true; 00245 $oldHeader = ''; 00246 } else { 00247 wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) ); 00248 00249 $sk = $this->getSkin(); 00250 if ( method_exists( $sk, 'suppressQuickbar' ) ) { 00251 $sk->suppressQuickbar(); 00252 } 00253 00254 if ( $this->mNewPage->equals( $this->mOldPage ) ) { 00255 $out->setPageTitle( $this->mNewPage->getPrefixedText() ); 00256 $out->addSubtitle( $this->msg( 'difference' ) ); 00257 $samePage = true; 00258 } else { 00259 $out->setPageTitle( $this->mOldPage->getPrefixedText() . ', ' . $this->mNewPage->getPrefixedText() ); 00260 $out->addSubtitle( $this->msg( 'difference-multipage' ) ); 00261 $samePage = false; 00262 } 00263 00264 if ( $samePage && $this->mNewPage->userCan( 'edit', $user ) ) { 00265 if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) { 00266 $out->preventClickjacking(); 00267 $rollback = '   ' . Linker::generateRollback( $this->mNewRev ); 00268 } 00269 if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) { 00270 $undoLink = ' ' . $this->msg( 'parentheses' )->rawParams( 00271 Html::element( 'a', array( 00272 'href' => $this->mNewPage->getLocalUrl( array( 00273 'action' => 'edit', 00274 'undoafter' => $this->mOldid, 00275 'undo' => $this->mNewid ) ), 00276 'title' => Linker::titleAttrib( 'undo' ) 00277 ), 00278 $this->msg( 'editundo' )->text() 00279 ) )->escaped(); 00280 } 00281 } 00282 00283 # Make "previous revision link" 00284 if ( $samePage && $this->mOldRev->getPrevious() ) { 00285 $prevlink = Linker::linkKnown( 00286 $this->mOldPage, 00287 $this->msg( 'previousdiff' )->escaped(), 00288 array( 'id' => 'differences-prevlink' ), 00289 array( 'diff' => 'prev', 'oldid' => $this->mOldid ) + $query 00290 ); 00291 } else { 00292 $prevlink = ' '; 00293 } 00294 00295 if ( $this->mOldRev->isMinor() ) { 00296 $oldminor = ChangesList::flag( 'minor' ); 00297 } else { 00298 $oldminor = ''; 00299 } 00300 00301 $ldel = $this->revisionDeleteLink( $this->mOldRev ); 00302 $oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' ); 00303 00304 $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' . 00305 '<div id="mw-diff-otitle2">' . 00306 Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' . 00307 '<div id="mw-diff-otitle3">' . $oldminor . 00308 Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' . 00309 '<div id="mw-diff-otitle4">' . $prevlink . '</div>'; 00310 00311 if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) { 00312 $deleted = true; // old revisions text is hidden 00313 if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) { 00314 $suppressed = true; // also suppressed 00315 } 00316 } 00317 00318 # Check if this user can see the revisions 00319 if ( !$this->mOldRev->userCan( Revision::DELETED_TEXT, $user ) ) { 00320 $allowed = false; 00321 } 00322 } 00323 00324 # Make "next revision link" 00325 # Skip next link on the top revision 00326 if ( $samePage && !$this->mNewRev->isCurrent() ) { 00327 $nextlink = Linker::linkKnown( 00328 $this->mNewPage, 00329 $this->msg( 'nextdiff' )->escaped(), 00330 array( 'id' => 'differences-nextlink' ), 00331 array( 'diff' => 'next', 'oldid' => $this->mNewid ) + $query 00332 ); 00333 } else { 00334 $nextlink = ' '; 00335 } 00336 00337 if ( $this->mNewRev->isMinor() ) { 00338 $newminor = ChangesList::flag( 'minor' ); 00339 } else { 00340 $newminor = ''; 00341 } 00342 00343 # Handle RevisionDelete links... 00344 $rdel = $this->revisionDeleteLink( $this->mNewRev ); 00345 $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) . $undoLink; 00346 00347 $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' . 00348 '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) . 00349 " $rollback</div>" . 00350 '<div id="mw-diff-ntitle3">' . $newminor . 00351 Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' . 00352 '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>'; 00353 00354 if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) { 00355 $deleted = true; // new revisions text is hidden 00356 if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) ) 00357 $suppressed = true; // also suppressed 00358 } 00359 00360 # If the diff cannot be shown due to a deleted revision, then output 00361 # the diff header and links to unhide (if available)... 00362 if ( $deleted && ( !$this->unhide || !$allowed ) ) { 00363 $this->showDiffStyle(); 00364 $multi = $this->getMultiNotice(); 00365 $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) ); 00366 if ( !$allowed ) { 00367 $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff'; 00368 # Give explanation for why revision is not visible 00369 $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n", 00370 array( $msg ) ); 00371 } else { 00372 # Give explanation and add a link to view the diff... 00373 $link = $this->getTitle()->getFullUrl( $this->getRequest()->appendQueryValue( 'unhide', '1', true ) ); 00374 $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff'; 00375 $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n", array( $msg, $link ) ); 00376 } 00377 # Otherwise, output a regular diff... 00378 } else { 00379 # Add deletion notice if the user is viewing deleted content 00380 $notice = ''; 00381 if ( $deleted ) { 00382 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view'; 00383 $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" . $this->msg( $msg )->parse() . "</div>\n"; 00384 } 00385 $this->showDiff( $oldHeader, $newHeader, $notice ); 00386 if ( !$diffOnly ) { 00387 $this->renderNewRevision(); 00388 } 00389 } 00390 wfProfileOut( __METHOD__ ); 00391 } 00392 00401 protected function markPatrolledLink() { 00402 global $wgUseRCPatrol; 00403 00404 if ( $this->mMarkPatrolledLink === null ) { 00405 // Prepare a change patrol link, if applicable 00406 if ( $wgUseRCPatrol && $this->mNewPage->userCan( 'patrol', $this->getUser() ) ) { 00407 // If we've been given an explicit change identifier, use it; saves time 00408 if ( $this->mRcidMarkPatrolled ) { 00409 $rcid = $this->mRcidMarkPatrolled; 00410 $rc = RecentChange::newFromId( $rcid ); 00411 // Already patrolled? 00412 $rcid = is_object( $rc ) && !$rc->getAttribute( 'rc_patrolled' ) ? $rcid : 0; 00413 } else { 00414 // Look for an unpatrolled change corresponding to this diff 00415 $db = wfGetDB( DB_SLAVE ); 00416 $change = RecentChange::newFromConds( 00417 array( 00418 // Redundant user,timestamp condition so we can use the existing index 00419 'rc_user_text' => $this->mNewRev->getRawUserText(), 00420 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ), 00421 'rc_this_oldid' => $this->mNewid, 00422 'rc_last_oldid' => $this->mOldid, 00423 'rc_patrolled' => 0 00424 ), 00425 __METHOD__ 00426 ); 00427 if ( $change instanceof RecentChange ) { 00428 $rcid = $change->mAttribs['rc_id']; 00429 $this->mRcidMarkPatrolled = $rcid; 00430 } else { 00431 // None found 00432 $rcid = 0; 00433 } 00434 } 00435 // Build the link 00436 if ( $rcid ) { 00437 $this->getOutput()->preventClickjacking(); 00438 $token = $this->getUser()->getEditToken( $rcid ); 00439 $this->mMarkPatrolledLink = ' <span class="patrollink">[' . Linker::linkKnown( 00440 $this->mNewPage, 00441 $this->msg( 'markaspatrolleddiff' )->escaped(), 00442 array(), 00443 array( 00444 'action' => 'markpatrolled', 00445 'rcid' => $rcid, 00446 'token' => $token, 00447 ) 00448 ) . ']</span>'; 00449 } else { 00450 $this->mMarkPatrolledLink = ''; 00451 } 00452 } else { 00453 $this->mMarkPatrolledLink = ''; 00454 } 00455 } 00456 00457 return $this->mMarkPatrolledLink; 00458 } 00459 00464 protected function revisionDeleteLink( $rev ) { 00465 $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() ); 00466 if ( $link !== '' ) { 00467 $link = '   ' . $link . ' '; 00468 } 00469 return $link; 00470 } 00471 00475 function renderNewRevision() { 00476 wfProfileIn( __METHOD__ ); 00477 $out = $this->getOutput(); 00478 $revHeader = $this->getRevisionHeader( $this->mNewRev ); 00479 # Add "current version as of X" title 00480 $out->addHTML( "<hr class='diff-hr' /> 00481 <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" ); 00482 # Page content may be handled by a hooked call instead... 00483 if ( wfRunHooks( 'ArticleContentOnDiff', array( $this, $out ) ) ) { 00484 $this->loadNewText(); 00485 $out->setRevisionId( $this->mNewid ); 00486 $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() ); 00487 $out->setArticleFlag( true ); 00488 00489 if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) { 00490 // Stolen from Article::view --AG 2007-10-11 00491 // Give hooks a chance to customise the output 00492 // @TODO: standardize this crap into one function 00493 if ( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mNewPage, $out ) ) ) { 00494 // Wrap the whole lot in a <pre> and don't parse 00495 $m = array(); 00496 preg_match( '!\.(css|js)$!u', $this->mNewPage->getText(), $m ); 00497 $out->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" ); 00498 $out->addHTML( htmlspecialchars( $this->mNewtext ) ); 00499 $out->addHTML( "\n</pre>\n" ); 00500 } 00501 } elseif ( !wfRunHooks( 'ArticleViewCustom', array( $this->mNewtext, $this->mNewPage, $out ) ) ) { 00502 // Handled by extension 00503 } else { 00504 // Normal page 00505 if ( $this->getTitle()->equals( $this->mNewPage ) ) { 00506 // If the Title stored in the context is the same as the one 00507 // of the new revision, we can use its associated WikiPage 00508 // object. 00509 $wikiPage = $this->getWikiPage(); 00510 } else { 00511 // Otherwise we need to create our own WikiPage object 00512 $wikiPage = WikiPage::factory( $this->mNewPage ); 00513 } 00514 00515 $parserOptions = ParserOptions::newFromContext( $this->getContext() ); 00516 $parserOptions->enableLimitReport(); 00517 $parserOptions->setTidy( true ); 00518 00519 if ( !$this->mNewRev->isCurrent() ) { 00520 $parserOptions->setEditSection( false ); 00521 } 00522 00523 $parserOutput = $wikiPage->getParserOutput( $parserOptions, $this->mNewid ); 00524 00525 # WikiPage::getParserOutput() should not return false, but just in case 00526 if( $parserOutput ) { 00527 $out->addParserOutput( $parserOutput ); 00528 } 00529 } 00530 } 00531 # Add redundant patrol link on bottom... 00532 $out->addHTML( $this->markPatrolledLink() ); 00533 00534 wfProfileOut( __METHOD__ ); 00535 } 00536 00543 function showDiff( $otitle, $ntitle, $notice = '' ) { 00544 $diff = $this->getDiff( $otitle, $ntitle, $notice ); 00545 if ( $diff === false ) { 00546 $this->getOutput()->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' ); 00547 return false; 00548 } else { 00549 $this->showDiffStyle(); 00550 $this->getOutput()->addHTML( $diff ); 00551 return true; 00552 } 00553 } 00554 00558 function showDiffStyle() { 00559 $this->getOutput()->addModuleStyles( 'mediawiki.action.history.diff' ); 00560 } 00561 00570 function getDiff( $otitle, $ntitle, $notice = '' ) { 00571 $body = $this->getDiffBody(); 00572 if ( $body === false ) { 00573 return false; 00574 } else { 00575 $multi = $this->getMultiNotice(); 00576 return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice ); 00577 } 00578 } 00579 00585 public function getDiffBody() { 00586 global $wgMemc; 00587 wfProfileIn( __METHOD__ ); 00588 $this->mCacheHit = true; 00589 // Check if the diff should be hidden from this user 00590 if ( !$this->loadRevisionData() ) { 00591 wfProfileOut( __METHOD__ ); 00592 return false; 00593 } elseif ( $this->mOldRev && !$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { 00594 wfProfileOut( __METHOD__ ); 00595 return false; 00596 } elseif ( $this->mNewRev && !$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { 00597 wfProfileOut( __METHOD__ ); 00598 return false; 00599 } 00600 // Short-circuit 00601 // If mOldRev is false, it means that the 00602 if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev 00603 && $this->mOldRev->getID() == $this->mNewRev->getID() ) ) 00604 { 00605 wfProfileOut( __METHOD__ ); 00606 return ''; 00607 } 00608 // Cacheable? 00609 $key = false; 00610 if ( $this->mOldid && $this->mNewid ) { 00611 $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 00612 'oldid', $this->mOldid, 'newid', $this->mNewid ); 00613 // Try cache 00614 if ( !$this->mRefreshCache ) { 00615 $difftext = $wgMemc->get( $key ); 00616 if ( $difftext ) { 00617 wfIncrStats( 'diff_cache_hit' ); 00618 $difftext = $this->localiseLineNumbers( $difftext ); 00619 $difftext .= "\n<!-- diff cache key $key -->\n"; 00620 wfProfileOut( __METHOD__ ); 00621 return $difftext; 00622 } 00623 } // don't try to load but save the result 00624 } 00625 $this->mCacheHit = false; 00626 00627 // Loadtext is permission safe, this just clears out the diff 00628 if ( !$this->loadText() ) { 00629 wfProfileOut( __METHOD__ ); 00630 return false; 00631 } 00632 00633 $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext ); 00634 00635 // Save to cache for 7 days 00636 if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) { 00637 wfIncrStats( 'diff_uncacheable' ); 00638 } elseif ( $key !== false && $difftext !== false ) { 00639 wfIncrStats( 'diff_cache_miss' ); 00640 $wgMemc->set( $key, $difftext, 7 * 86400 ); 00641 } else { 00642 wfIncrStats( 'diff_uncacheable' ); 00643 } 00644 // Replace line numbers with the text in the user's language 00645 if ( $difftext !== false ) { 00646 $difftext = $this->localiseLineNumbers( $difftext ); 00647 } 00648 wfProfileOut( __METHOD__ ); 00649 return $difftext; 00650 } 00651 00656 private function initDiffEngines() { 00657 global $wgExternalDiffEngine; 00658 if ( $wgExternalDiffEngine == 'wikidiff' && !function_exists( 'wikidiff_do_diff' ) ) { 00659 wfProfileIn( __METHOD__ . '-php_wikidiff.so' ); 00660 wfDl( 'php_wikidiff' ); 00661 wfProfileOut( __METHOD__ . '-php_wikidiff.so' ); 00662 } 00663 elseif ( $wgExternalDiffEngine == 'wikidiff2' && !function_exists( 'wikidiff2_do_diff' ) ) { 00664 wfProfileIn( __METHOD__ . '-php_wikidiff2.so' ); 00665 wfDl( 'wikidiff2' ); 00666 wfProfileOut( __METHOD__ . '-php_wikidiff2.so' ); 00667 } 00668 } 00669 00676 function generateDiffBody( $otext, $ntext ) { 00677 global $wgExternalDiffEngine, $wgContLang; 00678 00679 wfProfileIn( __METHOD__ ); 00680 00681 $otext = str_replace( "\r\n", "\n", $otext ); 00682 $ntext = str_replace( "\r\n", "\n", $ntext ); 00683 00684 $this->initDiffEngines(); 00685 00686 if ( $wgExternalDiffEngine == 'wikidiff' && function_exists( 'wikidiff_do_diff' ) ) { 00687 # For historical reasons, external diff engine expects 00688 # input text to be HTML-escaped already 00689 $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) ); 00690 $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) ); 00691 wfProfileOut( __METHOD__ ); 00692 return $wgContLang->unsegmentForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) . 00693 $this->debug( 'wikidiff1' ); 00694 } 00695 00696 if ( $wgExternalDiffEngine == 'wikidiff2' && function_exists( 'wikidiff2_do_diff' ) ) { 00697 # Better external diff engine, the 2 may some day be dropped 00698 # This one does the escaping and segmenting itself 00699 wfProfileIn( 'wikidiff2_do_diff' ); 00700 $text = wikidiff2_do_diff( $otext, $ntext, 2 ); 00701 $text .= $this->debug( 'wikidiff2' ); 00702 wfProfileOut( 'wikidiff2_do_diff' ); 00703 wfProfileOut( __METHOD__ ); 00704 return $text; 00705 } 00706 if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) { 00707 # Diff via the shell 00708 global $wgTmpDirectory; 00709 $tempName1 = tempnam( $wgTmpDirectory, 'diff_' ); 00710 $tempName2 = tempnam( $wgTmpDirectory, 'diff_' ); 00711 00712 $tempFile1 = fopen( $tempName1, "w" ); 00713 if ( !$tempFile1 ) { 00714 wfProfileOut( __METHOD__ ); 00715 return false; 00716 } 00717 $tempFile2 = fopen( $tempName2, "w" ); 00718 if ( !$tempFile2 ) { 00719 wfProfileOut( __METHOD__ ); 00720 return false; 00721 } 00722 fwrite( $tempFile1, $otext ); 00723 fwrite( $tempFile2, $ntext ); 00724 fclose( $tempFile1 ); 00725 fclose( $tempFile2 ); 00726 $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 ); 00727 wfProfileIn( __METHOD__ . "-shellexec" ); 00728 $difftext = wfShellExec( $cmd ); 00729 $difftext .= $this->debug( "external $wgExternalDiffEngine" ); 00730 wfProfileOut( __METHOD__ . "-shellexec" ); 00731 unlink( $tempName1 ); 00732 unlink( $tempName2 ); 00733 wfProfileOut( __METHOD__ ); 00734 return $difftext; 00735 } 00736 00737 # Native PHP diff 00738 $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) ); 00739 $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) ); 00740 $diffs = new Diff( $ota, $nta ); 00741 $formatter = new TableDiffFormatter(); 00742 $difftext = $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) . 00743 wfProfileOut( __METHOD__ ); 00744 return $difftext; 00745 } 00746 00751 protected function debug( $generator = "internal" ) { 00752 global $wgShowHostnames; 00753 if ( !$this->enableDebugComment ) { 00754 return ''; 00755 } 00756 $data = array( $generator ); 00757 if ( $wgShowHostnames ) { 00758 $data[] = wfHostname(); 00759 } 00760 $data[] = wfTimestamp( TS_DB ); 00761 return "<!-- diff generator: " . 00762 implode( " ", 00763 array_map( 00764 "htmlspecialchars", 00765 $data ) ) . 00766 " -->\n"; 00767 } 00768 00772 function localiseLineNumbers( $text ) { 00773 return preg_replace_callback( '/<!--LINE (\d+)-->/', 00774 array( &$this, 'localiseLineNumbersCb' ), $text ); 00775 } 00776 00777 function localiseLineNumbersCb( $matches ) { 00778 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) return ''; 00779 return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped(); 00780 } 00781 00782 00787 function getMultiNotice() { 00788 if ( !is_object( $this->mOldRev ) || !is_object( $this->mNewRev ) ) { 00789 return ''; 00790 } elseif ( !$this->mOldPage->equals( $this->mNewPage ) ) { 00791 // Comparing two different pages? Count would be meaningless. 00792 return ''; 00793 } 00794 00795 if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) { 00796 $oldRev = $this->mNewRev; // flip 00797 $newRev = $this->mOldRev; // flip 00798 } else { // normal case 00799 $oldRev = $this->mOldRev; 00800 $newRev = $this->mNewRev; 00801 } 00802 00803 $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev ); 00804 if ( $nEdits > 0 ) { 00805 $limit = 100; // use diff-multi-manyusers if too many users 00806 $numUsers = $this->mNewPage->countAuthorsBetween( $oldRev, $newRev, $limit ); 00807 return self::intermediateEditsMsg( $nEdits, $numUsers, $limit ); 00808 } 00809 return ''; // nothing 00810 } 00811 00819 public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) { 00820 if ( $numUsers > $limit ) { 00821 $msg = 'diff-multi-manyusers'; 00822 $numUsers = $limit; 00823 } else { 00824 $msg = 'diff-multi'; 00825 } 00826 return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse(); 00827 } 00828 00837 private function getRevisionHeader( Revision $rev, $complete = '' ) { 00838 $lang = $this->getLanguage(); 00839 $user = $this->getUser(); 00840 $revtimestamp = $rev->getTimestamp(); 00841 $timestamp = $lang->userTimeAndDate( $revtimestamp, $user ); 00842 $dateofrev = $lang->userDate( $revtimestamp, $user ); 00843 $timeofrev = $lang->userTime( $revtimestamp, $user ); 00844 00845 $header = $this->msg( 00846 $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof', 00847 $timestamp, 00848 $dateofrev, 00849 $timeofrev 00850 )->escaped(); 00851 00852 if ( $complete !== 'complete' ) { 00853 return $header; 00854 } 00855 00856 $title = $rev->getTitle(); 00857 00858 $header = Linker::linkKnown( $title, $header, array(), 00859 array( 'oldid' => $rev->getID() ) ); 00860 00861 if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) { 00862 $editQuery = array( 'action' => 'edit' ); 00863 if ( !$rev->isCurrent() ) { 00864 $editQuery['oldid'] = $rev->getID(); 00865 } 00866 00867 $msg = $this->msg( $title->userCan( 'edit', $user ) ? 'editold' : 'viewsourceold' )->escaped(); 00868 $header .= ' (' . Linker::linkKnown( $title, $msg, array(), $editQuery ) . ')'; 00869 if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { 00870 $header = Html::rawElement( 'span', array( 'class' => 'history-deleted' ), $header ); 00871 } 00872 } else { 00873 $header = Html::rawElement( 'span', array( 'class' => 'history-deleted' ), $header ); 00874 } 00875 00876 return $header; 00877 } 00878 00884 function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) { 00885 // shared.css sets diff in interface language/dir, but the actual content 00886 // is often in a different language, mostly the page content language/dir 00887 $tableClass = 'diff diff-contentalign-' . htmlspecialchars( $this->getDiffLang()->alignStart() ); 00888 $header = "<table class='$tableClass'>"; 00889 00890 if ( !$diff && !$otitle ) { 00891 $header .= " 00892 <tr valign='top'> 00893 <td class='diff-ntitle'>{$ntitle}</td> 00894 </tr>"; 00895 $multiColspan = 1; 00896 } else { 00897 if ( $diff ) { // Safari/Chrome show broken output if cols not used 00898 $header .= " 00899 <col class='diff-marker' /> 00900 <col class='diff-content' /> 00901 <col class='diff-marker' /> 00902 <col class='diff-content' />"; 00903 $colspan = 2; 00904 $multiColspan = 4; 00905 } else { 00906 $colspan = 1; 00907 $multiColspan = 2; 00908 } 00909 $header .= " 00910 <tr valign='top'> 00911 <td colspan='$colspan' class='diff-otitle'>{$otitle}</td> 00912 <td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td> 00913 </tr>"; 00914 } 00915 00916 if ( $multi != '' ) { 00917 $header .= "<tr><td colspan='{$multiColspan}' align='center' class='diff-multi'>{$multi}</td></tr>"; 00918 } 00919 if ( $notice != '' ) { 00920 $header .= "<tr><td colspan='{$multiColspan}' align='center'>{$notice}</td></tr>"; 00921 } 00922 00923 return $header . $diff . "</table>"; 00924 } 00925 00929 function setText( $oldText, $newText ) { 00930 $this->mOldtext = $oldText; 00931 $this->mNewtext = $newText; 00932 $this->mTextLoaded = 2; 00933 $this->mRevisionsLoaded = true; 00934 } 00935 00941 function setTextLanguage( $lang ) { 00942 $this->mDiffLang = wfGetLangObj( $lang ); 00943 } 00944 00948 private function loadRevisionIds() { 00949 if ( $this->mRevisionsIdsLoaded ) { 00950 return; 00951 } 00952 00953 $this->mRevisionsIdsLoaded = true; 00954 00955 $old = $this->mOldid; 00956 $new = $this->mNewid; 00957 00958 if ( $new === 'prev' ) { 00959 # Show diff between revision $old and the previous one. 00960 # Get previous one from DB. 00961 $this->mNewid = intval( $old ); 00962 $this->mOldid = $this->getTitle()->getPreviousRevisionID( $this->mNewid ); 00963 } elseif ( $new === 'next' ) { 00964 # Show diff between revision $old and the next one. 00965 # Get next one from DB. 00966 $this->mOldid = intval( $old ); 00967 $this->mNewid = $this->getTitle()->getNextRevisionID( $this->mOldid ); 00968 if ( $this->mNewid === false ) { 00969 # if no result, NewId points to the newest old revision. The only newer 00970 # revision is cur, which is "0". 00971 $this->mNewid = 0; 00972 } 00973 } else { 00974 $this->mOldid = intval( $old ); 00975 $this->mNewid = intval( $new ); 00976 wfRunHooks( 'NewDifferenceEngine', array( $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ) ); 00977 } 00978 } 00979 00992 function loadRevisionData() { 00993 if ( $this->mRevisionsLoaded ) { 00994 return true; 00995 } 00996 00997 // Whether it succeeds or fails, we don't want to try again 00998 $this->mRevisionsLoaded = true; 00999 01000 $this->loadRevisionIds(); 01001 01002 // Load the new revision object 01003 $this->mNewRev = $this->mNewid 01004 ? Revision::newFromId( $this->mNewid ) 01005 : Revision::newFromTitle( $this->getTitle() ); 01006 01007 if ( !$this->mNewRev instanceof Revision ) { 01008 return false; 01009 } 01010 01011 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff) 01012 $this->mNewid = $this->mNewRev->getId(); 01013 $this->mNewPage = $this->mNewRev->getTitle(); 01014 01015 // Load the old revision object 01016 $this->mOldRev = false; 01017 if ( $this->mOldid ) { 01018 $this->mOldRev = Revision::newFromId( $this->mOldid ); 01019 } elseif ( $this->mOldid === 0 ) { 01020 $rev = $this->mNewRev->getPrevious(); 01021 if ( $rev ) { 01022 $this->mOldid = $rev->getId(); 01023 $this->mOldRev = $rev; 01024 } else { 01025 // No previous revision; mark to show as first-version only. 01026 $this->mOldid = false; 01027 $this->mOldRev = false; 01028 } 01029 } /* elseif ( $this->mOldid === false ) leave mOldRev false; */ 01030 01031 if ( is_null( $this->mOldRev ) ) { 01032 return false; 01033 } 01034 01035 if ( $this->mOldRev ) { 01036 $this->mOldPage = $this->mOldRev->getTitle(); 01037 } 01038 01039 return true; 01040 } 01041 01047 function loadText() { 01048 if ( $this->mTextLoaded == 2 ) { 01049 return true; 01050 } else { 01051 // Whether it succeeds or fails, we don't want to try again 01052 $this->mTextLoaded = 2; 01053 } 01054 01055 if ( !$this->loadRevisionData() ) { 01056 return false; 01057 } 01058 if ( $this->mOldRev ) { 01059 $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER ); 01060 if ( $this->mOldtext === false ) { 01061 return false; 01062 } 01063 } 01064 if ( $this->mNewRev ) { 01065 $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER ); 01066 if ( $this->mNewtext === false ) { 01067 return false; 01068 } 01069 } 01070 return true; 01071 } 01072 01078 function loadNewText() { 01079 if ( $this->mTextLoaded >= 1 ) { 01080 return true; 01081 } else { 01082 $this->mTextLoaded = 1; 01083 } 01084 if ( !$this->loadRevisionData() ) { 01085 return false; 01086 } 01087 $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER ); 01088 return true; 01089 } 01090 }