Merge "Remove unused $wgDebugDBTransactions"
[lhc/web/wiklou.git] / includes / diff / DifferenceEngine.php
index d85dcfc..e8f35f0 100644 (file)
@@ -5,7 +5,7 @@
  * @file
  * @ingroup DifferenceEngine
  */
+
 /**
  * Constant to indicate diff cache compatibility.
  * Bump this when changing the diff formatting in a way that
@@ -18,16 +18,25 @@ define( 'MW_DIFF_VERSION', '1.11a' );
  * @todo document
  * @ingroup DifferenceEngine
  */
-class DifferenceEngine {
+class DifferenceEngine extends ContextSource {
        /**#@+
         * @private
         */
-       var $mOldid, $mNewid, $mTitle;
-       var $mOldtitle, $mNewtitle, $mPagetitle;
+       var $mOldid, $mNewid;
        var $mOldtext, $mNewtext;
+       protected $mDiffLang;
+
+       /**
+        * @var Title
+        */
        var $mOldPage, $mNewPage;
        var $mRcidMarkPatrolled;
+
+       /**
+        * @var Revision
+        */
        var $mOldRev, $mNewRev;
+       private $mRevisionsIdsLoaded = false; // Have the revisions IDs been loaded
        var $mRevisionsLoaded = false; // Have the revisions been loaded
        var $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2?
        var $mCacheHit = false; // Was the diff fetched from cache?
@@ -43,537 +52,502 @@ class DifferenceEngine {
        // readability and conserve space with many small diffs.
        protected $mReducedLineNumbers = false;
 
+       // Link to action=markpatrolled
+       protected $mMarkPatrolledLink = null;
+
        protected $unhide = false; # show rev_deleted content if allowed
        /**#@-*/
 
        /**
         * Constructor
-        * @param $titleObj Title object that the diff is associated with
-        * @param $old Integer: old ID we want to show and diff with.
-        * @param $new String: either 'prev' or 'next'.
-        * @param $rcid Integer: ??? FIXME (default 0)
+        * @param $context IContextSource context to use, anything else will be ignored
+        * @param $old Integer old ID we want to show and diff with.
+        * @param $new String either 'prev' or 'next'.
+        * @param $rcid Integer ??? FIXME (default 0)
         * @param $refreshCache boolean If set, refreshes the diff cache
         * @param $unhide boolean If set, allow viewing deleted revs
         */
-       function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0,
+       function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
                $refreshCache = false, $unhide = false )
        {
-               if ( $titleObj ) {
-                       $this->mTitle = $titleObj;
-               } else {
-                       global $wgTitle;
-                       $this->mTitle = $wgTitle;
+               if ( $context instanceof IContextSource ) {
+                       $this->setContext( $context );
                }
-               wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
 
-               if ( 'prev' === $new ) {
-                       # Show diff between revision $old and the previous one.
-                       # Get previous one from DB.
-                       $this->mNewid = intval($old);
-                       $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid );
-               } elseif ( 'next' === $new ) {
-                       # Show diff between revision $old and the next one.
-                       # Get next one from DB.
-                       $this->mOldid = intval($old);
-                       $this->mNewid = $this->mTitle->getNextRevisionID( $this->mOldid );
-                       if ( false === $this->mNewid ) {
-                               # if no result, NewId points to the newest old revision. The only newer
-                               # revision is cur, which is "0".
-                               $this->mNewid = 0;
-                       }
-               } else {
-                       $this->mOldid = intval($old);
-                       $this->mNewid = intval($new);
-                       wfRunHooks( 'NewDifferenceEngine', array(&$titleObj, &$this->mOldid, &$this->mNewid, $old, $new) ); 
-               }
-               $this->mRcidMarkPatrolled = intval($rcid);  # force it to be an integer
+               wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'\n" );
+
+               $this->mOldid = $old;
+               $this->mNewid = $new;
+               $this->mRcidMarkPatrolled = intval( $rcid );  # force it to be an integer
                $this->mRefreshCache = $refreshCache;
                $this->unhide = $unhide;
        }
 
+       /**
+        * @param $value bool
+        */
        function setReducedLineNumbers( $value = true ) {
                $this->mReducedLineNumbers = $value;
        }
 
-       function getTitle() {
-               return $this->mTitle;
+       /**
+        * @return Language
+        */
+       function getDiffLang() {
+               if ( $this->mDiffLang === null ) {
+                       # Default language in which the diff text is written.
+                       $this->mDiffLang = $this->getTitle()->getPageLanguage();
+               }
+               return $this->mDiffLang;
        }
-       
+
+       /**
+        * @return bool
+        */
        function wasCacheHit() {
                return $this->mCacheHit;
        }
-       
+
+       /**
+        * @return int
+        */
        function getOldid() {
+               $this->loadRevisionIds();
                return $this->mOldid;
        }
-       
+
+       /**
+        * @return Bool|int
+        */
        function getNewid() {
+               $this->loadRevisionIds();
                return $this->mNewid;
        }
 
+       /**
+        * Look up a special:Undelete link to the given deleted revision id,
+        * as a workaround for being unable to load deleted diffs in currently.
+        *
+        * @param int $id revision ID
+        * @return mixed URL or false
+        */
+       function deletedLink( $id ) {
+               if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) {
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $row = $dbr->selectRow('archive', '*',
+                               array( 'ar_rev_id' => $id ),
+                               __METHOD__ );
+                       if ( $row ) {
+                               $rev = Revision::newFromArchiveRow( $row );
+                               $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
+                               return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( array(
+                                       'target' => $title->getPrefixedText(),
+                                       'timestamp' => $rev->getTimestamp()
+                               ));
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Build a wikitext link toward a deleted revision, if viewable.
+        *
+        * @param int $id revision ID
+        * @return string wikitext fragment
+        */
+       function deletedIdMarker( $id ) {
+               $link = $this->deletedLink( $id );
+               if ( $link ) {
+                       return "[$link $id]";
+               } else {
+                       return $id;
+               }
+       }
+
        function showDiffPage( $diffOnly = false ) {
-               global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol;
                wfProfileIn( __METHOD__ );
 
+               # Allow frames except in certain special cases
+               $out = $this->getOutput();
+               $out->allowClickjacking();
+               $out->setRobotPolicy( 'noindex,nofollow' );
 
-               # If external diffs are enabled both globally and for the user,
-               # we'll use the application/x-external-editor interface to call
-               # an external diff tool like kompare, kdiff3, etc.
-               if($wgUseExternalEditor && $wgUser->getOption('externaldiff')) {
-                       global $wgInputEncoding,$wgServer,$wgScript,$wgLang;
-                       $wgOut->disable();
-                       header ( "Content-type: application/x-external-editor; charset=".$wgInputEncoding );
-                       $url1=$this->mTitle->getFullURL( array(
-                               'action' => 'raw',
-                               'oldid' => $this->mOldid
-                       ) );
-                       $url2=$this->mTitle->getFullURL( array(
-                               'action' => 'raw',
-                               'oldid' => $this->mNewid
-                       ) );
-                       $special=$wgLang->getNsText(NS_SPECIAL);
-                       $control=<<<CONTROL
-                       [Process]
-                       Type=Diff text
-                       Engine=MediaWiki
-                       Script={$wgServer}{$wgScript}
-                       Special namespace={$special}
-
-                       [File]
-                       Extension=wiki
-                       URL=$url1
-
-                       [File 2]
-                       Extension=wiki
-                       URL=$url2
-CONTROL;
-                       echo($control);
+               if ( !$this->loadRevisionData() ) {
+                       // Sounds like a deleted revision... Let's see what we can do.
+                       $t = $this->getTitle()->getPrefixedText();
+                       $d = $this->msg( 'missingarticle-diff',
+                               $this->deletedIdMarker( $this->mOldid ),
+                               $this->deletedIdMarker( $this->mNewid ) )->escaped();
+                       $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
+                       $out->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", "<span class='plainlinks'>$d</span>" );
+                       wfProfileOut( __METHOD__ );
                        return;
                }
 
-               $wgOut->setArticleFlag( false );
-               if ( !$this->loadRevisionData() ) {
-                       $t = $this->mTitle->getPrefixedText();
-                       $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
-                       $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
-                       $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
+               $user = $this->getUser();
+               $permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user );
+               if ( $this->mOldPage ) { # mOldPage might not be set, see below.
+                       $permErrors = wfMergeErrorArrays( $permErrors,
+                               $this->mOldPage->getUserPermissionsErrors( 'read', $user ) );
+               }
+               if ( count( $permErrors ) ) {
                        wfProfileOut( __METHOD__ );
-                       return;
+                       throw new PermissionsError( 'read', $permErrors );
                }
 
-               wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
+               # If external diffs are enabled both globally and for the user,
+               # we'll use the application/x-external-editor interface to call
+               # an external diff tool like kompare, kdiff3, etc.
+               if ( ExternalEdit::useExternalEngine( $this->getContext(), 'diff' ) ) {
+                       $urls = array(
+                               'File' => array( 'Extension' => 'wiki', 'URL' =>
+                                       # This should be mOldPage, but it may not be set, see below.
+                                       $this->mNewPage->getCanonicalURL( array(
+                                               'action' => 'raw', 'oldid' => $this->mOldid ) )
+                               ),
+                               'File2' => array( 'Extension' => 'wiki', 'URL' =>
+                                       $this->mNewPage->getCanonicalURL( array(
+                                               'action' => 'raw', 'oldid' => $this->mNewid ) )
+                               ),
+                       );
 
-               if ( $this->mNewRev->isCurrent() ) {
-                       $wgOut->setArticleFlag( true );
-               }
+                       $externalEditor = new ExternalEdit( $this->getContext(), $urls );
+                       $externalEditor->execute();
 
-               # mOldid is false if the difference engine is called with a "vague" query for
-               # a diff between a version V and its previous version V' AND the version V
-               # is the first version of that article. In that case, V' does not exist.
-               if ( $this->mOldid === false ) {
-                       $this->showFirstRevision();
-                       $this->renderNewRevision();  // should we respect $diffOnly here or not?
                        wfProfileOut( __METHOD__ );
                        return;
                }
 
-               $wgOut->suppressQuickbar();
+               $rollback = '';
+               $undoLink = '';
 
-               $oldTitle = $this->mOldPage->getPrefixedText();
-               $newTitle = $this->mNewPage->getPrefixedText();
-               if( $oldTitle == $newTitle ) {
-                       $wgOut->setPageTitle( $newTitle );
-               } else {
-                       $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle );
-               }
-               if ( $this->mNewPage->equals( $this->mOldPage ) ) {
-                       $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
-               } else {
-                       $wgOut->setSubtitle( wfMsgExt( 'difference-multipage', array( 'parseinline' ) ) );
+               $query = array();
+               # Carry over 'diffonly' param via navigation links
+               if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
+                       $query['diffonly'] = $diffOnly;
                }
-               $wgOut->setRobotPolicy( 'noindex,nofollow' );
-
-               if ( !$this->mOldPage->userCanRead() || !$this->mNewPage->userCanRead() ) {
-                       $wgOut->loginToUse();
-                       $wgOut->output();
-                       $wgOut->disable();
-                       wfProfileOut( __METHOD__ );
-                       return;
+               # Cascade unhide param in links for easy deletion browsing
+               if ( $this->unhide ) {
+                       $query['unhide'] = 1;
                }
 
-               $sk = $wgUser->getSkin();
+               # Check if one of the revisions is deleted/suppressed
+               $deleted = $suppressed = false;
+               $allowed = $this->mNewRev->userCan( Revision::DELETED_TEXT, $user );
 
-               // Check if page is editable
-               $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
-               if ( $editable && $this->mNewRev->isCurrent() && $wgUser->isAllowed( 'rollback' ) ) {
-                       $rollback = '&#160;&#160;&#160;' . $sk->generateRollback( $this->mNewRev );
+               # mOldRev is false if the difference engine is called with a "vague" query for
+               # a diff between a version V and its previous version V' AND the version V
+               # is the first version of that article. In that case, V' does not exist.
+               if ( $this->mOldRev === false ) {
+                       $out->setPageTitle( $this->mNewPage->getPrefixedText() );
+                       $out->addSubtitle( $this->msg( 'difference' ) );
+                       $samePage = true;
+                       $oldHeader = '';
                } else {
-                       $rollback = '';
-               }
+                       wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
+
+                       $sk = $this->getSkin();
+                       if ( method_exists( $sk, 'suppressQuickbar' ) ) {
+                               $sk->suppressQuickbar();
+                       }
 
-               // Prepare a change patrol link, if applicable
-               if( $wgUseRCPatrol && $this->mTitle->userCan('patrol') ) {
-                       // If we've been given an explicit change identifier, use it; saves time
-                       if( $this->mRcidMarkPatrolled ) {
-                               $rcid = $this->mRcidMarkPatrolled;
-                               $rc = RecentChange::newFromId( $rcid );
-                               // Already patrolled?
-                               $rcid = is_object($rc) && !$rc->getAttribute('rc_patrolled') ? $rcid : 0;
+                       if ( $this->mNewPage->equals( $this->mOldPage ) ) {
+                               $out->setPageTitle( $this->mNewPage->getPrefixedText() );
+                               $out->addSubtitle( $this->msg( 'difference' ) );
+                               $samePage = true;
                        } else {
-                               // Look for an unpatrolled change corresponding to this diff
-                               $db = wfGetDB( DB_SLAVE );
-                               $change = RecentChange::newFromConds(
-                                       array(
-                                       // Redundant user,timestamp condition so we can use the existing index
-                                               'rc_user_text'  => $this->mNewRev->getRawUserText(),
-                                               'rc_timestamp'  => $db->timestamp( $this->mNewRev->getTimestamp() ),
-                                               'rc_this_oldid' => $this->mNewid,
-                                               'rc_last_oldid' => $this->mOldid,
-                                               'rc_patrolled'  => 0
-                                       ),
-                                       __METHOD__
-                               );
-                               if( $change instanceof RecentChange ) {
-                                       $rcid = $change->mAttribs['rc_id'];
-                                       $this->mRcidMarkPatrolled = $rcid;
-                               } else {
-                                       // None found
-                                       $rcid = 0;
+                               $out->setPageTitle( $this->mOldPage->getPrefixedText() . ', ' . $this->mNewPage->getPrefixedText() );
+                               $out->addSubtitle( $this->msg( 'difference-multipage' ) );
+                               $samePage = false;
+                       }
+
+                       if ( $samePage && $this->mNewPage->quickUserCan( 'edit', $user ) ) {
+                               if ( $this->mNewRev->isCurrent() && $this->mNewPage->userCan( 'rollback', $user ) ) {
+                                       $out->preventClickjacking();
+                                       $rollback = '&#160;&#160;&#160;' . Linker::generateRollback( $this->mNewRev );
+                               }
+                               if ( !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
+                                       $undoLink = ' ' . $this->msg( 'parentheses' )->rawParams(
+                                               Html::element( 'a', array(
+                                                       'href' => $this->mNewPage->getLocalUrl( array(
+                                                               'action' => 'edit',
+                                                               'undoafter' => $this->mOldid,
+                                                               'undo' => $this->mNewid ) ),
+                                                       'title' => Linker::titleAttrib( 'undo' )
+                                               ),
+                                               $this->msg( 'editundo' )->text()
+                                       ) )->escaped();
                                }
                        }
-                       // Build the link
-                       if( $rcid ) {
-                               $token = $wgUser->editToken( $rcid );
-                               $patrol = ' <span class="patrollink">[' . $sk->link(
-                                       $this->mTitle, 
-                                       wfMsgHtml( 'markaspatrolleddiff' ),
-                                       array(),
-                                       array(
-                                               'action' => 'markpatrolled',
-                                               'rcid' => $rcid,
-                                               'token' => $token,
-                                       ),
-                                       array(
-                                               'known',
-                                               'noclasses'
-                                       )
-                               ) . ']</span>';
+
+                       # Make "previous revision link"
+                       if ( $samePage && $this->mOldRev->getPrevious() ) {
+                               $prevlink = Linker::linkKnown(
+                                       $this->mOldPage,
+                                       $this->msg( 'previousdiff' )->escaped(),
+                                       array( 'id' => 'differences-prevlink' ),
+                                       array( 'diff' => 'prev', 'oldid' => $this->mOldid ) + $query
+                               );
                        } else {
-                               $patrol = '';
+                               $prevlink = '&#160;';
                        }
-               } else {
-                       $patrol = '';
-               }
 
-               # Carry over 'diffonly' param via navigation links
-               if( $diffOnly != $wgUser->getBoolOption('diffonly') ) {
-                       $query['diffonly'] = $diffOnly;
-               }
+                       if ( $this->mOldRev->isMinor() ) {
+                               $oldminor = ChangesList::flag( 'minor' );
+                       } else {
+                               $oldminor = '';
+                       }
 
-               # Make "previous revision link"
-               $query['diff'] = 'prev';
-               $query['oldid'] = $this->mOldid;
-               # Cascade unhide param in links for easy deletion browsing
-               if( $this->unhide ) {
-                       $query['unhide'] = 1;
-               }
-               if( !$this->mOldRev->getPrevious() ) {
-                       $prevlink = '&#160;';
-               } else {
-                       $prevlink = $sk->link(
-                               $this->mTitle,
-                               wfMsgHtml( 'previousdiff' ),
-                               array(
-                                       'id' => 'differences-prevlink'
-                               ),
-                               $query,
-                               array(
-                                       'known',
-                                       'noclasses'
-                               )
-                       );
+                       $ldel = $this->revisionDeleteLink( $this->mOldRev );
+                       $oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' );
+
+                       $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
+                               '<div id="mw-diff-otitle2">' .
+                                       Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' .
+                               '<div id="mw-diff-otitle3">' . $oldminor .
+                                       Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
+                               '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
+
+                       if ( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
+                               $deleted = true; // old revisions text is hidden
+                               if ( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
+                                       $suppressed = true; // also suppressed
+                               }
+                       }
+
+                       # Check if this user can see the revisions
+                       if ( !$this->mOldRev->userCan( Revision::DELETED_TEXT, $user ) ) {
+                               $allowed = false;
+                       }
                }
 
                # Make "next revision link"
-               $query['diff'] = 'next';
-               $query['oldid'] = $this->mNewid;
                # Skip next link on the top revision
-               if( $this->mNewRev->isCurrent() ) {
-                       $nextlink = '&#160;';
-               } else {
-                       $nextlink = $sk->link(
-                               $this->mTitle,
-                               wfMsgHtml( 'nextdiff' ),
-                               array(
-                                       'id' => 'differences-nextlink'
-                               ),
-                               $query,
-                               array(
-                                       'known',
-                                       'noclasses'
-                               )
+               if ( $samePage && !$this->mNewRev->isCurrent() ) {
+                       $nextlink = Linker::linkKnown(
+                               $this->mNewPage,
+                               $this->msg( 'nextdiff' )->escaped(),
+                               array( 'id' => 'differences-nextlink' ),
+                               array( 'diff' => 'next', 'oldid' => $this->mNewid ) + $query
                        );
+               } else {
+                       $nextlink = '&#160;';
                }
 
-               $oldminor = '';
-               $newminor = '';
-
-               if( $this->mOldRev->isMinor() ) {
-                       $oldminor = ChangesList::flag( 'minor' );
-               }
-               if( $this->mNewRev->isMinor() ) {
+               if ( $this->mNewRev->isMinor() ) {
                        $newminor = ChangesList::flag( 'minor' );
+               } else {
+                       $newminor = '';
                }
 
                # Handle RevisionDelete links...
-               $ldel = $this->revisionDeleteLink( $this->mOldRev );
                $rdel = $this->revisionDeleteLink( $this->mNewRev );
+               $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) . $undoLink;
 
-               $oldHeader = '<div id="mw-diff-otitle1"><strong>'.$this->mOldtitle.'</strong></div>' .
-                       '<div id="mw-diff-otitle2">' .
-                               $sk->revUserTools( $this->mOldRev, !$this->unhide ).'</div>' .
-                       '<div id="mw-diff-otitle3">' . $oldminor .
-                               $sk->revComment( $this->mOldRev, !$diffOnly, !$this->unhide ).$ldel.'</div>' .
-                       '<div id="mw-diff-otitle4">' . $prevlink .'</div>';
-               $newHeader = '<div id="mw-diff-ntitle1"><strong>'.$this->mNewtitle.'</strong></div>' .
-                       '<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev, !$this->unhide ) .
+               $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
+                       '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) .
                                " $rollback</div>" .
                        '<div id="mw-diff-ntitle3">' . $newminor .
-                               $sk->revComment( $this->mNewRev, !$diffOnly, !$this->unhide ).$rdel.'</div>' .
-                       '<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>';
+                               Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
+                       '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
 
-               # Check if this user can see the revisions
-               $allowed = $this->mOldRev->userCan(Revision::DELETED_TEXT)
-                       && $this->mNewRev->userCan(Revision::DELETED_TEXT);
-               # Check if one of the revisions is deleted/suppressed
-               $deleted = $suppressed = false;
-               if( $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) {
-                       $deleted = true; // old revisions text is hidden
-                       if( $this->mOldRev->isDeleted(Revision::DELETED_RESTRICTED) )
-                               $suppressed = true; // also suppressed
-               }
-               if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
+               if ( $this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
                        $deleted = true; // new revisions text is hidden
-                       if( $this->mNewRev->isDeleted(Revision::DELETED_RESTRICTED) )
+                       if ( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) )
                                $suppressed = true; // also suppressed
                }
+
                # If the diff cannot be shown due to a deleted revision, then output
                # the diff header and links to unhide (if available)...
-               if( $deleted && (!$this->unhide || !$allowed) ) {
+               if ( $deleted && ( !$this->unhide || !$allowed ) ) {
                        $this->showDiffStyle();
                        $multi = $this->getMultiNotice();
-                       $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
-                       if( !$allowed ) {
+                       $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
+                       if ( !$allowed ) {
                                $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
                                # Give explanation for why revision is not visible
-                               $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
+                               $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
                                        array( $msg ) );
                        } else {
                                # Give explanation and add a link to view the diff...
-                               $link = $this->mTitle->getFullUrl( array(
-                                       'diff' => $this->mNewid,
-                                       'oldid' => $this->mOldid,
-                                       'unhide' => 1
-                               ) );
+                               $link = $this->getTitle()->getFullUrl( $this->getRequest()->appendQueryValue( 'unhide', '1', true ) );
                                $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
-                               $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", array( $msg, $link ) );
+                               $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n", array( $msg, $link ) );
                        }
                # Otherwise, output a regular diff...
                } else {
                        # Add deletion notice if the user is viewing deleted content
                        $notice = '';
-                       if( $deleted ) {
+                       if ( $deleted ) {
                                $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
-                               $notice = "<div class='mw-warning plainlinks'>\n".wfMsgExt($msg,'parseinline')."</div>\n";
+                               $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" . $this->msg( $msg )->parse() . "</div>\n";
                        }
                        $this->showDiff( $oldHeader, $newHeader, $notice );
-                       if( !$diffOnly ) {
+                       if ( !$diffOnly ) {
                                $this->renderNewRevision();
                        }
                }
                wfProfileOut( __METHOD__ );
        }
-       
-       protected function revisionDeleteLink( $rev ) {
-               global $wgUser;
-               $link = '';
-               $canHide = $wgUser->isAllowed( 'deleterevision' );
-               // Show del/undel link if:
-               // (a) the user can delete revisions, or
-               // (b) the user can view deleted revision *and* this one is deleted
-               if( $canHide || ($rev->getVisibility() && $wgUser->isAllowed( 'deletedhistory' )) ) {
-                       $sk = $wgUser->getSkin();
-                       if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
-                               $link = $sk->revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
-                       } else {
-                               $query = array(
-                                       'type'   => 'revision',
-                                       'target' => $rev->mTitle->getPrefixedDbkey(),
-                                       'ids'    => $rev->getId()
-                               );
-                               $link = $sk->revDeleteLink( $query,
-                                       $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide );
-                       }
-                       $link = '&#160;&#160;&#160;' . $link . ' ';
-               }
-               return $link;
-       }
 
        /**
-        * Show the new revision of the page.
+        * Get a link to mark the change as patrolled, or '' if there's either no
+        * revision to patrol or the user is not allowed to to it.
+        * Side effect: this method will call OutputPage::preventClickjacking()
+        * when a link is builded.
+        *
+        * @return String
         */
-       function renderNewRevision() {
-               global $wgOut, $wgUser;
-               wfProfileIn( __METHOD__ );
-
-               $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
-               # Add deleted rev tag if needed
-               if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
-                       $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-permission' );
-               } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
-                       $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-view' );
-               }
-
-               $pCache = true;
-               if( !$this->mNewRev->isCurrent() ) {
-                       $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
-                       $pCache = false;
-               }
-
-               $this->loadNewText();
-               if( is_object( $this->mNewRev ) ) {
-                       $wgOut->setRevisionId( $this->mNewRev->getId() );
-               }
-
-               if( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
-                       // Stolen from Article::view --AG 2007-10-11
-                       // Give hooks a chance to customise the output
-                       if( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mTitle, $wgOut ) ) ) {
-                               // Wrap the whole lot in a <pre> and don't parse
-                               $m = array();
-                               preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
-                               $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
-                               $wgOut->addHTML( htmlspecialchars( $this->mNewtext ) );
-                               $wgOut->addHTML( "\n</pre>\n" );
-                       }
-               } elseif( wfRunHooks( 'ArticleContentOnDiff', array( $this, $wgOut ) ) ) {
-                       if ( $pCache ) {
-                               $article = new Article( $this->mTitle, 0 );
-                               $pOutput = ParserCache::singleton()->get( $article, $wgOut->parserOptions() );
-                               if( $pOutput ) {
-                                       $wgOut->addParserOutput( $pOutput );
+       protected function markPatrolledLink() {
+               global $wgUseRCPatrol;
+
+               if ( $this->mMarkPatrolledLink === null ) {
+                       // Prepare a change patrol link, if applicable
+                       if ( $wgUseRCPatrol && $this->mNewPage->quickUserCan( 'patrol', $this->getUser() ) ) {
+                               // If we've been given an explicit change identifier, use it; saves time
+                               if ( $this->mRcidMarkPatrolled ) {
+                                       $rcid = $this->mRcidMarkPatrolled;
+                                       $rc = RecentChange::newFromId( $rcid );
+                                       // Already patrolled?
+                                       $rcid = is_object( $rc ) && !$rc->getAttribute( 'rc_patrolled' ) ? $rcid : 0;
                                } else {
-                                       $article->doViewParse();
-                               } 
+                                       // Look for an unpatrolled change corresponding to this diff
+                                       $db = wfGetDB( DB_SLAVE );
+                                       $change = RecentChange::newFromConds(
+                                               array(
+                                               // Redundant user,timestamp condition so we can use the existing index
+                                                       'rc_user_text'  => $this->mNewRev->getRawUserText(),
+                                                       'rc_timestamp'  => $db->timestamp( $this->mNewRev->getTimestamp() ),
+                                                       'rc_this_oldid' => $this->mNewid,
+                                                       'rc_last_oldid' => $this->mOldid,
+                                                       'rc_patrolled'  => 0
+                                               ),
+                                               __METHOD__
+                                       );
+                                       if ( $change instanceof RecentChange ) {
+                                               $rcid = $change->mAttribs['rc_id'];
+                                               $this->mRcidMarkPatrolled = $rcid;
+                                       } else {
+                                               // None found
+                                               $rcid = 0;
+                                       }
+                               }
+                               // Build the link
+                               if ( $rcid ) {
+                                       $this->getOutput()->preventClickjacking();
+                                       $token = $this->getUser()->getEditToken( $rcid );
+                                       $this->mMarkPatrolledLink = ' <span class="patrollink">[' . Linker::linkKnown(
+                                               $this->mNewPage,
+                                               $this->msg( 'markaspatrolleddiff' )->escaped(),
+                                               array(),
+                                               array(
+                                                       'action' => 'markpatrolled',
+                                                       'rcid' => $rcid,
+                                                       'token' => $token,
+                                               )
+                                       ) . ']</span>';
+                               } else {
+                                       $this->mMarkPatrolledLink = '';
+                               }
                        } else {
-                               $wgOut->addWikiTextTidy( $this->mNewtext );
+                               $this->mMarkPatrolledLink = '';
                        }
                }
-       
-               if( is_object( $this->mNewRev ) && !$this->mNewRev->isCurrent() ) {
-                       $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
-               }
 
-               # Add redundant patrol link on bottom...
-               if( $this->mRcidMarkPatrolled && $this->mTitle->quickUserCan('patrol') ) {
-                       $sk = $wgUser->getSkin();
-                       $token = $wgUser->editToken( $this->mRcidMarkPatrolled );
-                       $wgOut->addHTML(
-                               "<div class='patrollink'>[" . $sk->link(
-                                       $this->mTitle,
-                                       wfMsgHtml( 'markaspatrolleddiff' ),
-                                       array(),
-                                       array(
-                                               'action' => 'markpatrolled',
-                                               'rcid' => $this->mRcidMarkPatrolled,
-                                               'token' => $token,
-                                       )
-                               ) . ']</div>'
-                        );
-               }
+               return $this->mMarkPatrolledLink;
+       }
 
-               wfProfileOut( __METHOD__ );
+       /**
+        * @param $rev Revision
+        * @return String
+        */
+       protected function revisionDeleteLink( $rev ) {
+               $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() );
+               if ( $link !== '' ) {
+                       $link = '&#160;&#160;&#160;' . $link . ' ';
+               }
+               return $link;
        }
 
        /**
-        * Show the first revision of an article. Uses normal diff headers in
-        * contrast to normal "old revision" display style.
+        * Show the new revision of the page.
         */
-       function showFirstRevision() {
-               global $wgOut, $wgUser;
+       function renderNewRevision() {
                wfProfileIn( __METHOD__ );
+               $out = $this->getOutput();
+               $revHeader = $this->getRevisionHeader( $this->mNewRev );
+               # Add "current version as of X" title
+               $out->addHTML( "<hr class='diff-hr' />
+               <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
+               # Page content may be handled by a hooked call instead...
+               if ( wfRunHooks( 'ArticleContentOnDiff', array( $this, $out ) ) ) {
+                       $this->loadNewText();
+                       $out->setRevisionId( $this->mNewid );
+                       $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
+                       $out->setArticleFlag( true );
+
+                       if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) {
+                               // Stolen from Article::view --AG 2007-10-11
+                               // Give hooks a chance to customise the output
+                               // @TODO: standardize this crap into one function
+                               if ( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mNewPage, $out ) ) ) {
+                                       // Wrap the whole lot in a <pre> and don't parse
+                                       $m = array();
+                                       preg_match( '!\.(css|js)$!u', $this->mNewPage->getText(), $m );
+                                       $out->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
+                                       $out->addHTML( htmlspecialchars( $this->mNewtext ) );
+                                       $out->addHTML( "\n</pre>\n" );
+                               }
+                       } elseif ( !wfRunHooks( 'ArticleViewCustom', array( $this->mNewtext, $this->mNewPage, $out ) ) ) {
+                               // Handled by extension
+                       } else {
+                               // Normal page
+                               if ( $this->getTitle()->equals( $this->mNewPage ) ) {
+                                       // If the Title stored in the context is the same as the one
+                                       // of the new revision, we can use its associated WikiPage
+                                       // object.
+                                       $wikiPage = $this->getWikiPage();
+                               } else {
+                                       // Otherwise we need to create our own WikiPage object
+                                       $wikiPage = WikiPage::factory( $this->mNewPage );
+                               }
 
-               # Get article text from the DB
-               #
-               if ( ! $this->loadNewText() ) {
-                       $t = $this->mTitle->getPrefixedText();
-                       $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
-                       $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
-                       $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
-                       wfProfileOut( __METHOD__ );
-                       return;
-               }
-               if ( $this->mNewRev->isCurrent() ) {
-                       $wgOut->setArticleFlag( true );
-               }
+                               $parserOptions = ParserOptions::newFromContext( $this->getContext() );
+                               $parserOptions->enableLimitReport();
+                               $parserOptions->setTidy( true );
 
-               # Check if user is allowed to look at this page. If not, bail out.
-               #
-               if ( !$this->mTitle->userCanRead() ) {
-                       $wgOut->loginToUse();
-                       $wgOut->output();
-                       wfProfileOut( __METHOD__ );
-                       throw new MWException("Permission Error: you do not have access to view this page");
-               }
+                               if ( !$this->mNewRev->isCurrent() ) {
+                                       $parserOptions->setEditSection( false );
+                               }
 
-               # Prepare the header box
-               #
-               $sk = $wgUser->getSkin();
+                               $parserOutput = $wikiPage->getParserOutput( $parserOptions, $this->mNewid );
 
-               $next = $this->mTitle->getNextRevisionID( $this->mNewid );
-               if( !$next ) {
-                       $nextlink = '';
-               } else {
-                       $nextlink = '<br />' . $sk->link(
-                               $this->mTitle,
-                               wfMsgHtml( 'nextdiff' ),
-                               array(
-                                       'id' => 'differences-nextlink'
-                               ),
-                               array(
-                                       'diff' => 'next',
-                                       'oldid' => $this->mNewid,
-                               ),
-                               array(
-                                       'known',
-                                       'noclasses'
-                               )
-                       );
+                               # WikiPage::getParserOutput() should not return false, but just in case
+                               if( $parserOutput ) {
+                                       $out->addParserOutput( $parserOutput );
+                               }
+                       }
                }
-               $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\">" .
-                       $sk->revUserTools( $this->mNewRev ) . "<br />" . $sk->revComment( $this->mNewRev ) . $nextlink . "</div>\n";
-
-               $wgOut->addHTML( $header );
-
-               $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
-               $wgOut->setRobotPolicy( 'noindex,nofollow' );
+               # Add redundant patrol link on bottom...
+               $out->addHTML( $this->markPatrolledLink() );
 
                wfProfileOut( __METHOD__ );
        }
 
        /**
-        * Get the diff text, send it to $wgOut
+        * Get the diff text, send it to the OutputPage object
         * Returns false if the diff could not be generated, otherwise returns true
+        *
+        * @return bool
         */
        function showDiff( $otitle, $ntitle, $notice = '' ) {
-               global $wgOut;
                $diff = $this->getDiff( $otitle, $ntitle, $notice );
                if ( $diff === false ) {
-                       $wgOut->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' );
+                       $this->getOutput()->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' );
                        return false;
                } else {
                        $this->showDiffStyle();
-                       $wgOut->addHTML( $diff );
+                       $this->getOutput()->addHTML( $diff );
                        return true;
                }
        }
@@ -582,8 +556,7 @@ CONTROL;
         * Add style sheets and supporting JS for diff display.
         */
        function showDiffStyle() {
-               global $wgOut;
-               $wgOut->addModules( 'mediawiki.legacy.diff' );
+               $this->getOutput()->addModuleStyles( 'mediawiki.action.history.diff' );
        }
 
        /**
@@ -607,26 +580,36 @@ CONTROL;
        /**
         * Get the diff table body, without header
         *
-        * @return mixed
+        * @return mixed (string/false)
         */
-       function getDiffBody() {
+       public function getDiffBody() {
                global $wgMemc;
                wfProfileIn( __METHOD__ );
                $this->mCacheHit = true;
                // Check if the diff should be hidden from this user
-               if ( !$this->loadRevisionData() )
-                       return '';
-               if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
-                       return '';
-               } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
-                       return '';
-               } else if ( $this->mOldRev && $this->mNewRev && $this->mOldRev->getID() == $this->mNewRev->getID() ) {
+               if ( !$this->loadRevisionData() ) {
+                       wfProfileOut( __METHOD__ );
+                       return false;
+               } elseif ( $this->mOldRev && !$this->mOldRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
+                       wfProfileOut( __METHOD__ );
+                       return false;
+               } elseif ( $this->mNewRev && !$this->mNewRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
+                       wfProfileOut( __METHOD__ );
+                       return false;
+               }
+               // Short-circuit
+               // If mOldRev is false, it means that the 
+               if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev
+                       && $this->mOldRev->getID() == $this->mNewRev->getID() ) )
+               {
+                       wfProfileOut( __METHOD__ );
                        return '';
                }
                // Cacheable?
                $key = false;
                if ( $this->mOldid && $this->mNewid ) {
-                       $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid );
+                       $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION,
+                               'oldid', $this->mOldid, 'newid', $this->mNewid );
                        // Try cache
                        if ( !$this->mRefreshCache ) {
                                $difftext = $wgMemc->get( $key );
@@ -652,9 +635,9 @@ CONTROL;
                // Save to cache for 7 days
                if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
                        wfIncrStats( 'diff_uncacheable' );
-               } else if ( $key !== false && $difftext !== false ) {
+               } elseif ( $key !== false && $difftext !== false ) {
                        wfIncrStats( 'diff_cache_miss' );
-                       $wgMemc->set( $key, $difftext, 7*86400 );
+                       $wgMemc->set( $key, $difftext, 7 * 86400 );
                } else {
                        wfIncrStats( 'diff_uncacheable' );
                }
@@ -674,16 +657,12 @@ CONTROL;
                global $wgExternalDiffEngine;
                if ( $wgExternalDiffEngine == 'wikidiff' && !function_exists( 'wikidiff_do_diff' ) ) {
                        wfProfileIn( __METHOD__ . '-php_wikidiff.so' );
-                       wfSuppressWarnings();
-                       dl( 'php_wikidiff.so' );
-                       wfRestoreWarnings();
+                       wfDl( 'php_wikidiff' );
                        wfProfileOut( __METHOD__ . '-php_wikidiff.so' );
                }
-               else if ( $wgExternalDiffEngine == 'wikidiff2' && !function_exists( 'wikidiff2_do_diff' ) ) {
+               elseif ( $wgExternalDiffEngine == 'wikidiff2' && !function_exists( 'wikidiff2_do_diff' ) ) {
                        wfProfileIn( __METHOD__ . '-php_wikidiff2.so' );
-                       wfSuppressWarnings();
                        wfDl( 'wikidiff2' );
-                       wfRestoreWarnings();
                        wfProfileOut( __METHOD__ . '-php_wikidiff2.so' );
                }
        }
@@ -693,10 +672,13 @@ CONTROL;
         *
         * @param $otext String: old text, must be already segmented
         * @param $ntext String: new text, must be already segmented
+        * @return bool|string
         */
        function generateDiffBody( $otext, $ntext ) {
                global $wgExternalDiffEngine, $wgContLang;
 
+               wfProfileIn( __METHOD__ );
+
                $otext = str_replace( "\r\n", "\n", $otext );
                $ntext = str_replace( "\r\n", "\n", $ntext );
 
@@ -707,7 +689,8 @@ CONTROL;
                        # input text to be HTML-escaped already
                        $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) );
                        $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) );
-                       return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) .
+                       wfProfileOut( __METHOD__ );
+                       return $wgContLang->unsegmentForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) .
                        $this->debug( 'wikidiff1' );
                }
 
@@ -718,6 +701,7 @@ CONTROL;
                        $text = wikidiff2_do_diff( $otext, $ntext, 2 );
                        $text .= $this->debug( 'wikidiff2' );
                        wfProfileOut( 'wikidiff2_do_diff' );
+                       wfProfileOut( __METHOD__ );
                        return $text;
                }
                if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) {
@@ -747,6 +731,7 @@ CONTROL;
                        wfProfileOut( __METHOD__ . "-shellexec" );
                        unlink( $tempName1 );
                        unlink( $tempName2 );
+                       wfProfileOut( __METHOD__ );
                        return $difftext;
                }
 
@@ -755,21 +740,23 @@ CONTROL;
                $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
                $diffs = new Diff( $ota, $nta );
                $formatter = new TableDiffFormatter();
-               return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) .
-               $this->debug();
+               $difftext = $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) .
+               wfProfileOut( __METHOD__ );
+               return $difftext;
        }
 
        /**
         * Generate a debug comment indicating diff generating time,
         * server node, and generator backend.
+        * @return string
         */
-       protected function debug( $generator="internal" ) {
+       protected function debug( $generator = "internal" ) {
                global $wgShowHostnames;
                if ( !$this->enableDebugComment ) {
                        return '';
                }
                $data = array( $generator );
-               if( $wgShowHostnames ) {
+               if ( $wgShowHostnames ) {
                        $data[] = wfHostname();
                }
                $data[] = wfTimestamp( TS_DB );
@@ -783,6 +770,7 @@ CONTROL;
 
        /**
         * Replace line numbers with the text in the user's language
+        * @return mixed
         */
        function localiseLineNumbers( $text ) {
                return preg_replace_callback( '/<!--LINE (\d+)-->/',
@@ -790,84 +778,143 @@ CONTROL;
        }
 
        function localiseLineNumbersCb( $matches ) {
-               global $wgLang;
                if ( $matches[1] === '1' && $this->mReducedLineNumbers ) return '';
-               return wfMsgExt( 'lineno', 'escape', $wgLang->formatNum( $matches[1] ) );
+               return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
        }
 
 
        /**
         * If there are revisions between the ones being compared, return a note saying so.
+        * @return string
         */
        function getMultiNotice() {
-               if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) )
-               return '';
-
-               if( !$this->mOldPage->equals( $this->mNewPage ) ) {
+               if ( !is_object( $this->mOldRev ) || !is_object( $this->mNewRev ) ) {
+                       return '';
+               } elseif ( !$this->mOldPage->equals( $this->mNewPage ) ) {
                        // Comparing two different pages? Count would be meaningless.
                        return '';
                }
 
-               $oldid = $this->mOldRev->getId();
-               $newid = $this->mNewRev->getId();
-               if ( $oldid > $newid ) {
-                       $tmp = $oldid; $oldid = $newid; $newid = $tmp;
+               if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) {
+                       $oldRev = $this->mNewRev; // flip
+                       $newRev = $this->mOldRev; // flip
+               } else { // normal case
+                       $oldRev = $this->mOldRev;
+                       $newRev = $this->mNewRev;
                }
 
-               $n = $this->mTitle->countRevisionsBetween( $oldid, $newid );
-               if ( !$n ) {
-                       return '';
+               $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev );
+               if ( $nEdits > 0 ) {
+                       $limit = 100; // use diff-multi-manyusers if too many users
+                       $numUsers = $this->mNewPage->countAuthorsBetween( $oldRev, $newRev, $limit );
+                       return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
+               }
+               return ''; // nothing
+       }
+
+       /**
+        * Get a notice about how many intermediate edits and users there are
+        * @param $numEdits int
+        * @param $numUsers int
+        * @param $limit int
+        * @return string
+        */
+       public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
+               if ( $numUsers > $limit ) {
+                       $msg = 'diff-multi-manyusers';
+                       $numUsers = $limit;
                } else {
-                       global $wgLang;
-                       $dbr = wfGetDB( DB_SLAVE );
-                       
-                       // Actually, the limit is $limit + 1. We do this so we can detect
-                       // if there are > 100 authors in a given revision range. If they
-                       // are, $limit will be passed to diff-multi-manyusers for l10n.
-                       $limit = 100;
-                       $res = $dbr->select( 'revision', 'DISTINCT rev_user_text',
-                               array(
-                                       'rev_page = ' . $this->mOldRev->getPage(),
-                                       'rev_id > ' . $this->mOldRev->getId(),
-                                       'rev_id < ' . $this->mNewRev->getId()
-                               ), __METHOD__,
-                               array( 'LIMIT' => $limit + 1 )
-                       );
-                       $numUsers = $dbr->numRows( $res );
-                       if( $numUsers > $limit ) {
-                               $msg = 'diff-multi-manyusers';
-                               $numUsers = $limit;
-                       } else {
-                               $msg = 'diff-multi';
-                       }
-                       return wfMsgExt( $msg, array( 'parseinline' ), $wgLang->formatnum( $n ),
-                               $wgLang->formatnum( $numUsers ) );
+                       $msg = 'diff-multi';
                }
+               return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
        }
 
+       /**
+        * Get a header for a specified revision.
+        *
+        * @param $rev Revision
+        * @param $complete String: 'complete' to get the header wrapped depending
+        *        the visibility of the revision and a link to edit the page.
+        * @return String HTML fragment
+        */
+       private function getRevisionHeader( Revision $rev, $complete = '' ) {
+               $lang = $this->getLanguage();
+               $user = $this->getUser();
+               $revtimestamp = $rev->getTimestamp();
+               $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
+               $dateofrev = $lang->userDate( $revtimestamp, $user );
+               $timeofrev = $lang->userTime( $revtimestamp, $user );
+
+               $header = $this->msg(
+                       $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
+                       $timestamp,
+                       $dateofrev,
+                       $timeofrev
+               )->escaped();
+
+               if ( $complete !== 'complete' ) {
+                       return $header;
+               }
+
+               $title = $rev->getTitle();
+
+               $header = Linker::linkKnown( $title, $header, array(),
+                       array( 'oldid' => $rev->getID() ) );
+
+               if ( $rev->userCan( Revision::DELETED_TEXT, $user ) ) {
+                       $editQuery = array( 'action' => 'edit' );
+                       if ( !$rev->isCurrent() ) {
+                               $editQuery['oldid'] = $rev->getID();
+                       }
+
+                       $msg = $this->msg( $title->quickUserCan( 'edit', $user ) ? 'editold' : 'viewsourceold' )->escaped();
+                       $header .= ' (' . Linker::linkKnown( $title, $msg, array(), $editQuery ) . ')';
+                       if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+                               $header = Html::rawElement( 'span', array( 'class' => 'history-deleted' ), $header );
+                       }
+               } else {
+                       $header = Html::rawElement( 'span', array( 'class' => 'history-deleted' ), $header );
+               }
+
+               return $header;
+       }
 
        /**
         * Add the header to a diff body
+        *
+        * @return string
         */
-       static function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
-               $header = "<table class='diff'>";
-               if( $diff ) { // Safari/Chrome show broken output if cols not used
+       function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
+               // shared.css sets diff in interface language/dir, but the actual content
+               // is often in a different language, mostly the page content language/dir
+               $tableClass = 'diff diff-contentalign-' . htmlspecialchars( $this->getDiffLang()->alignStart() );
+               $header = "<table class='$tableClass'>";
+
+               if ( !$diff && !$otitle ) {
                        $header .= "
-                       <col class='diff-marker' />
-                       <col class='diff-content' />
-                       <col class='diff-marker' />
-                       <col class='diff-content' />";
-                       $colspan = 2;
-                       $multiColspan = 4;
+                       <tr valign='top'>
+                       <td class='diff-ntitle'>{$ntitle}</td>
+                       </tr>";
+                       $multiColspan = 1;
                } else {
-                       $colspan = 1;
-                       $multiColspan = 2;
+                       if ( $diff ) { // Safari/Chrome show broken output if cols not used
+                               $header .= "
+                               <col class='diff-marker' />
+                               <col class='diff-content' />
+                               <col class='diff-marker' />
+                               <col class='diff-content' />";
+                               $colspan = 2;
+                               $multiColspan = 4;
+                       } else {
+                               $colspan = 1;
+                               $multiColspan = 2;
+                       }
+                       $header .= "
+                       <tr valign='top'>
+                       <td colspan='$colspan' class='diff-otitle'>{$otitle}</td>
+                       <td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td>
+                       </tr>";
                }
-               $header .= "
-               <tr valign='top'>
-               <td colspan='$colspan' class='diff-otitle'>{$otitle}</td>
-               <td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td>
-               </tr>";
 
                if ( $multi != '' ) {
                        $header .= "<tr><td colspan='{$multiColspan}' align='center' class='diff-multi'>{$multi}</td></tr>";
@@ -889,6 +936,50 @@ CONTROL;
                $this->mRevisionsLoaded = true;
        }
 
+       /**
+        * Set the language in which the diff text is written
+        * (Defaults to page content language).
+        * @since 1.19
+        */
+       function setTextLanguage( $lang ) {
+               $this->mDiffLang = wfGetLangObj( $lang );
+       }
+
+       /**
+        * Load revision IDs
+        */
+       private function loadRevisionIds() {
+               if ( $this->mRevisionsIdsLoaded ) {
+                       return;
+               }
+
+               $this->mRevisionsIdsLoaded = true;
+
+               $old = $this->mOldid;
+               $new = $this->mNewid;
+
+               if ( $new === 'prev' ) {
+                       # Show diff between revision $old and the previous one.
+                       # Get previous one from DB.
+                       $this->mNewid = intval( $old );
+                       $this->mOldid = $this->getTitle()->getPreviousRevisionID( $this->mNewid );
+               } elseif ( $new === 'next' ) {
+                       # Show diff between revision $old and the next one.
+                       # Get next one from DB.
+                       $this->mOldid = intval( $old );
+                       $this->mNewid = $this->getTitle()->getNextRevisionID( $this->mOldid );
+                       if ( $this->mNewid === false ) {
+                               # if no result, NewId points to the newest old revision. The only newer
+                               # revision is cur, which is "0".
+                               $this->mNewid = 0;
+                       }
+               } else {
+                       $this->mOldid = intval( $old );
+                       $this->mNewid = intval( $new );
+                       wfRunHooks( 'NewDifferenceEngine', array( $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ) );
+               }
+       }
+
        /**
         * Load revision metadata for the specified articles. If newid is 0, then compare
         * the old article in oldid to the current article; if oldid is 0, then
@@ -898,81 +989,39 @@ CONTROL;
         * If oldid is false, leave the corresponding revision object set
         * to false. This is impossible via ordinary user input, and is provided for
         * API convenience.
+        *
+        * @return bool
         */
        function loadRevisionData() {
-               global $wgLang, $wgUser;
                if ( $this->mRevisionsLoaded ) {
                        return true;
-               } else {
-                       // Whether it succeeds or fails, we don't want to try again
-                       $this->mRevisionsLoaded = true;
                }
 
+               // Whether it succeeds or fails, we don't want to try again
+               $this->mRevisionsLoaded = true;
+
+               $this->loadRevisionIds();
+
                // Load the new revision object
                $this->mNewRev = $this->mNewid
                        ? Revision::newFromId( $this->mNewid )
-                       : Revision::newFromTitle( $this->mTitle );
-               if( !$this->mNewRev instanceof Revision )
+                       : Revision::newFromTitle( $this->getTitle() );
+
+               if ( !$this->mNewRev instanceof Revision ) {
                        return false;
+               }
 
                // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
                $this->mNewid = $this->mNewRev->getId();
-
-               // Check if page is editable
-               $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
-
-               // Set assorted variables
-               $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true );
-               $dateofrev = $wgLang->date( $this->mNewRev->getTimestamp(), true );
-               $timeofrev = $wgLang->time( $this->mNewRev->getTimestamp(), true );
                $this->mNewPage = $this->mNewRev->getTitle();
-               if( $this->mNewRev->isCurrent() ) {
-                       $newLink = $this->mNewPage->escapeLocalUrl( array(
-                               'oldid' => $this->mNewid
-                       ) );
-                       $this->mPagetitle = htmlspecialchars( wfMsg(
-                               'currentrev-asof',
-                               $timestamp,
-                               $dateofrev,
-                               $timeofrev
-                       ) );
-                       $newEdit = $this->mNewPage->escapeLocalUrl( array(
-                               'action' => 'edit'
-                       ) );
-
-                       $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
-                       $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
-               } else {
-                       $newLink = $this->mNewPage->escapeLocalUrl( array(
-                               'oldid' => $this->mNewid
-                       ) );
-                       $newEdit = $this->mNewPage->escapeLocalUrl( array(
-                               'action' => 'edit',
-                               'oldid' => $this->mNewid
-                       ) );
-                       $this->mPagetitle = htmlspecialchars( wfMsg(
-                               'revisionasof',
-                               $timestamp,
-                               $dateofrev,
-                               $timeofrev
-                       ) );
-
-                       $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
-                       $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
-               }
-               if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
-                       $this->mNewtitle = "<span class='history-deleted'>{$this->mPagetitle}</span>";
-               } else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
-                       $this->mNewtitle = "<span class='history-deleted'>{$this->mNewtitle}</span>";
-               }
 
                // Load the old revision object
                $this->mOldRev = false;
-               if( $this->mOldid ) {
+               if ( $this->mOldid ) {
                        $this->mOldRev = Revision::newFromId( $this->mOldid );
                } elseif ( $this->mOldid === 0 ) {
                        $rev = $this->mNewRev->getPrevious();
-                       if( $rev ) {
+                       if ( $rev ) {
                                $this->mOldid = $rev->getId();
                                $this->mOldRev = $rev;
                        } else {
@@ -980,46 +1029,14 @@ CONTROL;
                                $this->mOldid = false;
                                $this->mOldRev = false;
                        }
-               }/* elseif ( $this->mOldid === false ) leave mOldRev false; */
+               } /* elseif ( $this->mOldid === false ) leave mOldRev false; */
 
-               if( is_null( $this->mOldRev ) ) {
+               if ( is_null( $this->mOldRev ) ) {
                        return false;
                }
 
                if ( $this->mOldRev ) {
                        $this->mOldPage = $this->mOldRev->getTitle();
-
-                       $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true );
-                       $dateofrev = $wgLang->date( $this->mOldRev->getTimestamp(), true );
-                       $timeofrev = $wgLang->time( $this->mOldRev->getTimestamp(), true );
-                       $oldLink = $this->mOldPage->escapeLocalUrl( array(
-                               'oldid' => $this->mOldid
-                       ) );
-                       $oldEdit = $this->mOldPage->escapeLocalUrl( array(
-                               'action' => 'edit',
-                               'oldid' => $this->mOldid
-                       ) );
-                       $this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t, $dateofrev, $timeofrev ) );
-
-                       $this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>"
-                       . " (<a href='$oldEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
-                       // Add an "undo" link
-                       $newUndo = $this->mNewPage->escapeLocalUrl( array(
-                               'action' => 'edit',
-                               'undoafter' => $this->mOldid,
-                               'undo' => $this->mNewid
-                       ) );
-                       $htmlLink = htmlspecialchars( wfMsg( 'editundo' ) );
-                       $htmlTitle = $wgUser->getSkin()->titleAttrib( 'undo' );
-                       if( $editable && !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
-                               $this->mNewtitle .= " (<a href='$newUndo' $htmlTitle>" . $htmlLink . "</a>)";
-                       }
-
-                       if( !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) {
-                               $this->mOldtitle = '<span class="history-deleted">' . $this->mOldPagetitle . '</span>';
-                       } else if( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
-                               $this->mOldtitle = '<span class="history-deleted">' . $this->mOldtitle . '</span>';
-                       }
                }
 
                return true;
@@ -1027,6 +1044,8 @@ CONTROL;
 
        /**
         * Load the text of the revisions, as well as revision data.
+        *
+        * @return bool
         */
        function loadText() {
                if ( $this->mTextLoaded == 2 ) {
@@ -1056,6 +1075,8 @@ CONTROL;
 
        /**
         * Load the text of the new revision, not the old one
+        *
+        * @return bool
         */
        function loadNewText() {
                if ( $this->mTextLoaded >= 1 ) {