*Redo newRevisionFromEditComplete hook. Pass an Article instead of a title
[lhc/web/wiklou.git] / includes / Article.php
index 575f48f..dedf4e7 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 /**
  * File for articles
+ * @file
  */
 
 /**
@@ -34,27 +35,16 @@ class Article {
        var $mTouched;                  //!<
        var $mUser;                             //!<
        var $mUserText;                 //!<
+       var $mRedirectTarget;           //!<
+       var $mIsRedirect;
        /**@}}*/
 
-       /**
-        * Constants used by internal components to get rollback results
-        */
-       const SUCCESS = 0;                      // Operation successful
-       const PERM_DENIED = 1;          // Permission denied
-       const BLOCKED = 2;                      // User has been blocked
-       const READONLY = 3;                     // Wiki is in read-only mode
-       const BAD_TOKEN = 4;            // Invalid token specified
-       const BAD_TITLE = 5;            // $this is not a valid Article
-       const ALREADY_ROLLED = 6;       // Someone else already rolled this back. $from and $summary will be set
-       const ONLY_AUTHOR = 7;          // User is the only author of the page
-       const RATE_LIMITED = 8;
        /**
         * Constructor and clear the article
         * @param $title Reference to a Title object.
         * @param $oldId Integer revision ID, null to fetch from request, zero for current
         */
-       function __construct( &$title, $oldId = null ) {
+       function __construct( Title $title, $oldId = null ) {
                $this->mTitle =& $title;
                $this->mOldId = $oldId;
                $this->clear();
@@ -70,6 +60,55 @@ class Article {
        }
 
        /**
+        * If this page is a redirect, get its target
+        *
+        * The target will be fetched from the redirect table if possible.
+        * If this page doesn't have an entry there, call insertRedirect()
+        * @return mixed Title object, or null if this page is not a redirect
+        */
+       public function getRedirectTarget() {
+               if(!$this->mTitle || !$this->mTitle->isRedirect())
+                       return null;
+               if(!is_null($this->mRedirectTarget))
+                       return $this->mRedirectTarget;
+
+               # Query the redirect table
+               $dbr = wfGetDB(DB_SLAVE);
+               $res = $dbr->select('redirect',
+                               array('rd_namespace', 'rd_title'),
+                               array('rd_from' => $this->getID()),
+                               __METHOD__
+               );
+               $row = $dbr->fetchObject($res);
+               if($row)
+                       return $this->mRedirectTarget = Title::makeTitle($row->rd_namespace, $row->rd_title);
+
+               # This page doesn't have an entry in the redirect table
+               return $this->mRedirectTarget = $this->insertRedirect();
+       }
+
+       /**
+        * Insert an entry for this page into the redirect table.
+        *
+        * Don't call this function directly unless you know what you're doing.
+        * @return Title object
+        */
+       public function insertRedirect() {
+               $retval = Title::newFromRedirect($this->getContent());
+               if(!$retval)
+                       return null;
+               $dbw = wfGetDB(DB_MASTER);
+               $dbw->replace('redirect', array('rd_from'), array(
+                               'rd_from' => $this->getID(),
+                               'rd_namespace' => $retval->getNamespace(),
+                               'rd_title' => $retval->getDBKey()
+               ), __METHOD__);
+               return $retval;
+       }
+
+       /**
+        * Get the Title object this page redirects to
+        *
         * @return mixed false, Title of in-wiki target, or string with URL
         */
        function followRedirect() {
@@ -127,6 +166,7 @@ class Article {
 
                $this->mCurID = $this->mUser = $this->mCounter = -1; # Not loaded
                $this->mRedirectedFrom = null; # Title object if set
+               $this->mRedirectTarget = null; # Title object if set
                $this->mUserText =
                $this->mTimestamp = $this->mComment = '';
                $this->mGoodAdjustment = $this->mTotalAdjustment = 0;
@@ -151,7 +191,7 @@ class Article {
         * @return Return the text of this revision
        */
        function getContent() {
-               global $wgUser, $wgOut;
+               global $wgUser, $wgOut, $wgMessageCache;
 
                wfProfileIn( __METHOD__ );
 
@@ -160,12 +200,13 @@ class Article {
                        $wgOut->setRobotpolicy( 'noindex,nofollow' );
 
                        if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
+                               $wgMessageCache->loadAllMessages();
                                $ret = wfMsgWeirdKey ( $this->mTitle->getText() ) ;
                        } else {
                                $ret = wfMsg( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon' );
                        }
 
-                       return "<div class='noarticletext'>$ret</div>";
+                       return "<div class='noarticletext'>\n$ret\n</div>";
                } else {
                        $this->loadContent();
                        wfProfileOut( __METHOD__ );
@@ -315,9 +356,9 @@ class Article {
                        $data = $this->pageDataFromId( $dbr, $this->getId() );
                }
 
-               $lc =& LinkCache::singleton();
+               $lc = LinkCache::singleton();
                if ( $data ) {
-                       $lc->addGoodLinkObj( $data->page_id, $this->mTitle );
+                       $lc->addGoodLinkObj( $data->page_id, $this->mTitle, $data->page_len, $data->page_is_redirect );
 
                        $this->mTitle->mArticleID = $data->page_id;
 
@@ -355,7 +396,7 @@ class Article {
                # fails we'll have something telling us what we intended.
                $t = $this->mTitle->getPrefixedText();
                if( $oldid ) {
-                       $t .= ',oldid='.$oldid;
+                       $t .= ' ' . wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid );
                }
                $this->mContent = wfMsg( 'missingarticle', $t ) ;
 
@@ -390,8 +431,7 @@ class Article {
 
                // FIXME: Horrible, horrible! This content-loading interface just plain sucks.
                // We should instead work with the Revision object when we need it...
-               $this->mContent = $revision->userCan( Revision::DELETED_TEXT ) ? $revision->getRawText() : "";
-               //$this->mContent   = $revision->getText();
+               $this->mContent   = $revision->revText(); // Loads if user is allowed
 
                $this->mUser      = $revision->getUser();
                $this->mUserText  = $revision->getUserText();
@@ -503,6 +543,10 @@ class Article {
         */
        function isRedirect( $text = false ) {
                if ( $text === false ) {
+                       if ( $this->mDataLoaded ) 
+                               return $this->mIsRedirect;
+                       
+                       // Apparently loadPageData was never called
                        $this->loadContent();
                        $titleObj = Title::newFromRedirect( $this->fetchContent() );
                } else {
@@ -622,15 +666,16 @@ class Article {
         * This is the default action of the script: just view the page of
         * the given title.
        */
-       function view() {
+       function view() {
                global $wgUser, $wgOut, $wgRequest, $wgContLang;
                global $wgEnableParserCache, $wgStylePath, $wgParser;
                global $wgUseTrackbacks, $wgNamespaceRobotPolicies, $wgArticleRobotPolicies;
+               global $wgDefaultRobotPolicy;
                $sk = $wgUser->getSkin();
 
                wfProfileIn( __METHOD__ );
 
-               $parserCache =& ParserCache::singleton();
+               $parserCache = ParserCache::singleton();
                $ns = $this->mTitle->getNamespace(); # shortcut
 
                # Get variables from query string
@@ -660,8 +705,7 @@ class Article {
                        # Honour customised robot policies for this namespace
                        $policy = $wgNamespaceRobotPolicies[$ns];
                } else {
-                       # Default to encourage indexing and following links
-                       $policy = 'index,follow';
+                       $policy = $wgDefaultRobotPolicy;
                }
                $wgOut->setRobotPolicy( $policy );
 
@@ -759,10 +803,10 @@ class Article {
                                # Failed to load, replace text with error message
                                $t = $this->mTitle->getPrefixedText();
                                if( $oldid ) {
-                                       $t .= ',oldid='.$oldid;
+                                       $t .= ' ' . wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid );
                                        $text = wfMsg( 'missingarticle', $t );
                                } else {
-                                       $text = wfMsg( 'noarticletext', $t );
+                                       $text = wfMsg( 'noarticletext' );
                                }
                        }
 
@@ -770,6 +814,7 @@ class Article {
                        if ( !$this->mTitle->userCanRead() ) {
                                $wgOut->loginToUse();
                                $wgOut->output();
+                               wfProfileOut( __METHOD__ );
                                exit;
                        }
 
@@ -783,11 +828,12 @@ class Article {
                                        $this->setOldSubtitle( isset($this->mOldId) ? $this->mOldId : $oldid );
                                        if( $this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) {
                                                if( !$this->mRevision->userCan( Revision::DELETED_TEXT ) ) {
-                                                       $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) );
+                                                       $wgOut->addWikiMsg( 'rev-deleted-text-permission' );
                                                        $wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
+                                                       wfProfileOut( __METHOD__ );
                                                        return;
                                                } else {
-                                                       $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) );
+                                                       $wgOut->addWikiMsg( 'rev-deleted-text-view' );
                                                        // and we are allowed to see...
                                                }
                                        }
@@ -797,7 +843,7 @@ class Article {
                }
                if( !$outputDone ) {
                        $wgOut->setRevisionId( $this->getRevIdFetched() );
-                       
+
                         // Pages containing custom CSS or JavaScript get special treatment
                        if( $this->mTitle->isCssOrJsPage() || $this->mTitle->isCssJsSubpage() ) {
                                $wgOut->addHtml( wfMsgExt( 'clearyourcache', 'parse' ) );
@@ -811,22 +857,12 @@ class Article {
                                        $wgOut->addHtml( htmlspecialchars( $this->mContent ) );
                                        $wgOut->addHtml( "\n</pre>\n" );
                                }
-                       
+
                        }
-                       
+
                        elseif ( $rt = Title::newFromRedirect( $text ) ) {
-                               # Display redirect
-                               $imageDir = $wgContLang->isRTL() ? 'rtl' : 'ltr';
-                               $imageUrl = $wgStylePath.'/common/images/redirect' . $imageDir . '.png';
                                # Don't overwrite the subtitle if this was an old revision
-                               if( !$wasRedirected && $this->isCurrent() ) {
-                                       $wgOut->setSubtitle( wfMsgHtml( 'redirectpagesub' ) );
-                               }
-                               $link = $sk->makeLinkObj( $rt, $rt->getFullText() );
-
-                               $wgOut->addHTML( '<img src="'.$imageUrl.'" alt="#REDIRECT " />' .
-                                 '<span class="redirectText">'.$link.'</span>' );
-
+                               $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() );
                                $parseout = $wgParser->parse($text, $this->mTitle, ParserOptions::newFromUser($wgUser));
                                $wgOut->addParserOutputNoText( $parseout );
                        } else if ( $pcache ) {
@@ -864,8 +900,8 @@ class Article {
 
                # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
                if( $ns == NS_USER_TALK &&
-                       User::isIP( $this->mTitle->getText() ) ) {
-                       $wgOut->addWikiText( wfMsg('anontalkpagetext') );
+                       IP::isValid( $this->mTitle->getText() ) ) {
+                       $wgOut->addWikiMsg('anontalkpagetext');
                }
 
                # If we have been passed an &rcid= parameter, we want to give the user a
@@ -888,6 +924,27 @@ class Article {
                $this->viewUpdates();
                wfProfileOut( __METHOD__ );
        }
+       
+       protected function viewRedirect( $target, $overwriteSubtitle = true, $forceKnown = false ) {
+               global $wgParser, $wgOut, $wgContLang, $wgStylePath, $wgUser;
+               
+               # Display redirect
+               $imageDir = $wgContLang->isRTL() ? 'rtl' : 'ltr';
+               $imageUrl = $wgStylePath.'/common/images/redirect' . $imageDir . '.png';
+               
+               if( $overwriteSubtitle ) {
+                       $wgOut->setSubtitle( wfMsgHtml( 'redirectpagesub' ) );
+               }
+               $sk = $wgUser->getSkin();
+               if ( $forceKnown )
+                       $link = $sk->makeKnownLinkObj( $target, htmlspecialchars( $target->getFullText() ) );
+               else
+                       $link = $sk->makeLinkObj( $target, htmlspecialchars( $target->getFullText() ) );
+
+               $wgOut->addHTML( '<img src="'.$imageUrl.'" alt="#REDIRECT " />' .
+                       '<span class="redirectText">'.$link.'</span>' );
+               
+       }
 
        function addTrackbacks() {
                global $wgOut, $wgUser;
@@ -910,6 +967,7 @@ class Article {
                                                . $o->tb_id . "&token=" . urlencode( $wgUser->editToken() ) );
                                $rmvtxt = wfMsg( 'trackbackremove', htmlspecialchars( $delurl ) );
                        }
+                       $tbtext .= "\n";
                        $tbtext .= wfMsg(strlen($o->tb_ex) ? 'trackbackexcerpt' : 'trackback',
                                        $o->tb_title,
                                        $o->tb_url,
@@ -917,14 +975,14 @@ class Article {
                                        $o->tb_name,
                                        $rmvtxt);
                }
-               $wgOut->addWikitext(wfMsg('trackbackbox', $tbtext));
+               $wgOut->addWikiMsg( 'trackbackbox', $tbtext );
        }
 
        function deletetrackback() {
                global $wgUser, $wgRequest, $wgOut, $wgTitle;
 
                if (!$wgUser->matchEditToken($wgRequest->getVal('token'))) {
-                       $wgOut->addWikitext(wfMsg('sessionfailure'));
+                       $wgOut->addWikiMsg( 'sessionfailure' );
                        return;
                }
 
@@ -939,7 +997,7 @@ class Article {
                $db = wfGetDB(DB_MASTER);
                $db->delete('trackbacks', array('tb_id' => $wgRequest->getInt('tbid')));
                $wgTitle->invalidateCache();
-               $wgOut->addWikiText(wfMsg('trackbackdeleteok'));
+               $wgOut->addWikiMsg('trackbackdeleteok');
        }
 
        function render() {
@@ -991,6 +1049,15 @@ class Article {
                        $update = SquidUpdate::newSimplePurge( $this->mTitle );
                        $update->doUpdate();
                }
+               if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
+                       global $wgMessageCache;
+                       if ( $this->getID() == 0 ) {
+                               $text = false;
+                       } else {
+                               $text = $this->getContent();
+                       }
+                       $wgMessageCache->replace( $this->mTitle->getDBkey(), $text );
+               }
                $this->view();
        }
 
@@ -1071,7 +1138,6 @@ class Article {
                $result = $dbw->affectedRows() != 0;
 
                if ($result) {
-                       // FIXME: Should the result from updateRedirectOn() be returned instead?
                        $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
                }
 
@@ -1116,6 +1182,8 @@ class Article {
                                $dbw->delete( 'redirect', $where, __METHOD__);
                        }
 
+                       if( $this->getTitle()->getNamespace() == NS_IMAGE )
+                               RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
                        wfProfileOut( __METHOD__ );
                        return ( $dbw->affectedRows() != 0 );
                }
@@ -1201,10 +1269,11 @@ class Article {
        /**
         * @deprecated use Article::doEdit()
         */
-       function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC=false, $comment=false ) {
+       function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC=false, $comment=false, $bot=false ) {
                $flags = EDIT_NEW | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
                        ( $isminor ? EDIT_MINOR : 0 ) |
-                       ( $suppressRC ? EDIT_SUPPRESS_RC : 0 );
+                       ( $suppressRC ? EDIT_SUPPRESS_RC : 0 ) |
+                       ( $bot ? EDIT_FORCE_BOT : 0 );
 
                # If this is a comment, add the summary as headline
                if ( $comment && $summary != "" ) {
@@ -1294,10 +1363,11 @@ class Article {
         * EDIT_NEW is specified and the article does exist, a duplicate key error will cause an exception
         * to be thrown from the Database. These two conditions are also possible with auto-detection due
         * to MediaWiki's performance-optimised locking strategy.
+        * @param $baseRevId, the revision ID this edit was based off, if any
         *
         * @return bool success
         */
-       function doEdit( $text, $summary, $flags = 0 ) {
+       function doEdit( $text, $summary, $flags = 0, $baseRevId = false ) {
                global $wgUser, $wgDBtransactions;
 
                wfProfileIn( __METHOD__ );
@@ -1323,7 +1393,7 @@ class Article {
 
                # Silently ignore EDIT_MINOR if not allowed
                $isminor = ( $flags & EDIT_MINOR ) && $wgUser->isAllowed('minoredit');
-               $bot = $wgUser->isAllowed( 'bot' ) || ( $flags & EDIT_FORCE_BOT );
+               $bot = $flags & EDIT_FORCE_BOT;
 
                $oldtext = $this->getContent();
                $oldsize = strlen( $oldtext );
@@ -1349,7 +1419,7 @@ class Article {
 
                        $lastRevision = 0;
                        $revisionId = 0;
-                       
+
                        $changed = ( strcmp( $text, $oldtext ) != 0 );
 
                        if ( $changed ) {
@@ -1371,7 +1441,8 @@ class Article {
                                        'page'       => $this->getId(),
                                        'comment'    => $summary,
                                        'minor_edit' => $isminor,
-                                       'text'       => $text
+                                       'text'       => $text,
+                                       'parent_id'  => $lastRevision
                                        ) );
 
                                $dbw->begin();
@@ -1379,6 +1450,8 @@ class Article {
 
                                # Update page
                                $ok = $this->updateRevisionOn( $dbw, $revision, $lastRevision );
+                               
+                               wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, $baseRevId) );
 
                                if( !$ok ) {
                                        /* Belated edit conflict! Run away!! */
@@ -1417,7 +1490,7 @@ class Article {
                                # Invalidate cache of this article and all pages using this article
                                # as a template. Partly deferred.
                                Article::onArticleEdit( $this->mTitle );
-                               
+
                                # Update links tables, site stats, etc.
                                $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed );
                        }
@@ -1449,6 +1522,8 @@ class Article {
 
                        # Update the page record with revision data
                        $this->updateRevisionOn( $dbw, $revision, 0 );
+                       
+                       wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, false) );
 
                        if( !( $flags & EDIT_SUPPRESS_RC ) ) {
                                $rcid = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $wgUser, $summary, $bot,
@@ -1498,6 +1573,7 @@ class Article {
         *
         * @param boolean $noRedir Add redirect=no
         * @param string $sectionAnchor section to redirect to, including "#"
+        * @param string $extraq, extra query params
         */
        function doRedirect( $noRedir = false, $sectionAnchor = '', $extraq = '' ) {
                global $wgOut;
@@ -1521,8 +1597,8 @@ class Article {
                # Check patrol config options
 
                if ( !($wgUseNPPatrol || $wgUseRCPatrol)) {
-                       $wgOut->errorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' );
-                       return;         
+                       $wgOut->showErrorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' );
+                       return;
                }
 
                # If we haven't been given an rc_id value, we can't do anything
@@ -1530,18 +1606,18 @@ class Article {
                $rc = $rcid ? RecentChange::newFromId($rcid) : null;
                if ( is_null ( $rc ) )
                {
-                       $wgOut->errorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' );
+                       $wgOut->showErrorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' );
                        return;
                }
 
-               if ( !$wgUseRCPatrol && $rc->mAttribs['rc_type'] != RC_NEW) {
+               if ( !$wgUseRCPatrol && $rc->getAttribute( 'rc_type' ) != RC_NEW) {
                        // Only new pages can be patrolled if the general patrolling is off....???
                        // @fixme -- is this necessary? Shouldn't we only bother controlling the
                        // front end here?
-                       $wgOut->errorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' );
+                       $wgOut->showErrorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' );
                        return;
                }
-               
+
                # Check permissions
                $permission_errors = $this->mTitle->getUserPermissionsErrors( 'patrol', $wgUser );
 
@@ -1557,7 +1633,7 @@ class Article {
                }
 
                #It would be nice to see where the user had actually come from, but for now just guess
-               $returnto = $rc->mAttribs['rc_type'] == RC_NEW ? 'Newpages' : 'Recentchanges';
+               $returnto = $rc->getAttribute( 'rc_type' ) == RC_NEW ? 'Newpages' : 'Recentchanges';
                $return = Title::makeTitle( NS_SPECIAL, $returnto );
 
                # If it's left up to us, check that the user is allowed to patrol this edit
@@ -1572,20 +1648,24 @@ class Article {
                                # The user made this edit, and can't patrol it
                                # Tell them so, and then back off
                                $wgOut->setPageTitle( wfMsg( 'markedaspatrollederror' ) );
-                               $wgOut->addWikiText( wfMsgNoTrans( 'markedaspatrollederror-noautopatrol' ) );
+                               $wgOut->addWikiMsg( 'markedaspatrollederror-noautopatrol' );
                                $wgOut->returnToMain( false, $return );
                                return;
                        }
                }
 
-               # Mark the edit as patrolled
-               RecentChange::markPatrolled( $rcid );
-               PatrolLog::record( $rcid );
-               wfRunHooks( 'MarkPatrolledComplete', array( &$rcid, &$wgUser, false ) );
+               # Check that the revision isn't patrolled already
+               # Prevents duplicate log entries
+               if( !$rc->getAttribute( 'rc_patrolled' ) ) {
+                       # Mark the edit as patrolled
+                       RecentChange::markPatrolled( $rcid );
+                       PatrolLog::record( $rcid );
+                       wfRunHooks( 'MarkPatrolledComplete', array( &$rcid, &$wgUser, false ) );
+               }
 
                # Inform the user
                $wgOut->setPageTitle( wfMsg( 'markedaspatrolled' ) );
-               $wgOut->addWikiText( wfMsgNoTrans( 'markedaspatrolledtext' ) );
+               $wgOut->addWikiMsg( 'markedaspatrolledtext' );
                $wgOut->returnToMain( false, $return );
        }
 
@@ -1610,9 +1690,7 @@ class Article {
                        $wgOut->setPagetitle( wfMsg( 'addedwatch' ) );
                        $wgOut->setRobotpolicy( 'noindex,nofollow' );
 
-                       $link = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
-                       $text = wfMsg( 'addedwatchtext', $link );
-                       $wgOut->addWikiText( $text );
+                       $wgOut->addWikiMsg( 'addedwatchtext', $this->mTitle->getPrefixedText() );
                }
 
                $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() );
@@ -1657,9 +1735,7 @@ class Article {
                        $wgOut->setPagetitle( wfMsg( 'removedwatch' ) );
                        $wgOut->setRobotpolicy( 'noindex,nofollow' );
 
-                       $link = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
-                       $text = wfMsg( 'removedwatchtext', $link );
-                       $wgOut->addWikiText( $text );
+                       $wgOut->addWikiMsg( 'removedwatchtext', $this->mTitle->getPrefixedText() );
                }
 
                $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() );
@@ -1731,8 +1807,8 @@ class Article {
                $updated = Article::flattenRestrictions( $limit );
 
                $changed = ( $current != $updated );
-               $changed = $changed || ($this->mTitle->areRestrictionsCascading() != $cascade);
-               $changed = $changed || ($this->mTitle->mRestrictionsExpiry != $expiry);
+               $changed = $changed || ($updated && $this->mTitle->areRestrictionsCascading() != $cascade);
+               $changed = $changed || ($updated && $this->mTitle->mRestrictionsExpiry != $expiry);
                $protect = ( $updated != '' );
 
                # If nothing's changed, do nothing
@@ -1746,7 +1822,7 @@ class Article {
 
                                $expiry_description = '';
                                if ( $encodedExpiry != 'infinity' ) {
-                                       $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ) ).')';
+                                       $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry, false, false ) ).')';
                                }
 
                                # Prepare a null revision to be added to the history
@@ -1761,9 +1837,10 @@ class Article {
                                foreach( $limit as $action => $restrictions ) {
                                        # Check if the group level required to edit also can protect pages
                                        # Otherwise, people who cannot normally protect can "protect" pages via transclusion
-                                       $cascade = ( $cascade && isset($wgGroupPermissions[$restrictions]['protect']) && $wgGroupPermissions[$restrictions]['protect'] );       
+                                       $cascade = ( $cascade && isset($wgGroupPermissions[$restrictions]['protect']) &&
+                                               $wgGroupPermissions[$restrictions]['protect'] );
                                }
-                               
+
                                $cascade_description = '';
                                if ($cascade) {
                                        $cascade_description = ' ['.wfMsg('protect-summary-cascade').']';
@@ -1778,9 +1855,6 @@ class Article {
                                if ( $cascade )
                                        $comment .= "$cascade_description";
 
-                               $nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true );
-                               $nullRevId = $nullRevision->insertOn( $dbw );
-
                                # Update restrictions table
                                foreach( $limit as $action => $restrictions ) {
                                        if ($restrictions != '' ) {
@@ -1794,6 +1868,10 @@ class Article {
                                        }
                                }
 
+                               # Insert a null revision
+                               $nullRevision = Revision::newNullRevision( $dbw, $id, $comment, true );
+                               $nullRevId = $nullRevision->insertOn( $dbw );
+
                                # Update page record
                                $dbw->update( 'page',
                                        array( /* SET */
@@ -1804,11 +1882,12 @@ class Article {
                                                'page_id' => $id
                                        ), 'Article::protect'
                                );
+                               
+                               wfRunHooks( 'NewRevisionFromEditComplete', array($this, $nullRevision, false) );
                                wfRunHooks( 'ArticleProtectComplete', array( &$this, &$wgUser, $limit, $reason ) );
 
                                # Update the protection log
                                $log = new LogPage( 'protect' );
-
                                if( $protect ) {
                                        $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle, trim( $reason . " [$updated]$cascade_description$expiry_description" ) );
                                } else {
@@ -1841,7 +1920,7 @@ class Article {
                }
                return implode( ':', $bits );
        }
-       
+
        /**
         * Auto-generates a deletion reason
         * @param bool &$hasHistory Whether the page has a history
@@ -1884,27 +1963,26 @@ class Article {
                $row = $dbw->fetchObject($res);
                $onlyAuthor = $row->rev_user_text;
                // Try to find a second contributor
-               while(($row = $dbw->fetchObject($res)))
-                       if($row->rev_user_text != $onlyAuthor)
-                       {
+               while( $row = $dbw->fetchObject($res) ) {
+                       if($row->rev_user_text != $onlyAuthor) {
                                $onlyAuthor = false;
                                break;
                        }
+               }
                $dbw->freeResult($res);
 
                // Generate the summary with a '$1' placeholder
-               if($blank)
+               if($blank) {
                        // The current revision is blank and the one before is also
                        // blank. It's just not our lucky day
                        $reason = wfMsgForContent('exbeforeblank', '$1');
-               else
-               {
+               } else {
                        if($onlyAuthor)
                                $reason = wfMsgForContent('excontentauthor', '$1', $onlyAuthor);
                        else
                                $reason = wfMsgForContent('excontent', '$1');
                }
-               
+
                // Replace newlines with spaces to prevent uglyness
                $contents = preg_replace("/[\n\r]/", ' ', $contents);
                // Calculate the maximum amount of chars to get
@@ -1927,31 +2005,38 @@ class Article {
 
                $confirm = $wgRequest->wasPosted() &&
                                $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) );
-               
+
                $this->DeleteReasonList = $wgRequest->getText( 'wpDeleteReasonList', 'other' );
                $this->DeleteReason = $wgRequest->getText( 'wpReason' );
-               
+
                $reason = $this->DeleteReasonList;
-               
+
                if ( $reason != 'other' && $this->DeleteReason != '') {
                        // Entry from drop down menu + additional comment
                        $reason .= ': ' . $this->DeleteReason;
                } elseif ( $reason == 'other' ) {
                        $reason = $this->DeleteReason;
                }
+               # Flag to hide all contents of the archived revisions
+               $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed('hiderevision');
 
                # This code desperately needs to be totally rewritten
 
+               # Read-only check...
+               if ( wfReadOnly() ) {
+                       $wgOut->readOnlyPage();
+                       return;
+               }
+
                # Check permissions
                $permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser );
 
-               if (count($permission_errors)>0)
-               {
+               if (count($permission_errors)>0) {
                        $wgOut->showPermissionsErrorPage( $permission_errors );
                        return;
                }
 
-               $wgOut->setPagetitle( wfMsg( 'confirmdelete' ) );
+               $wgOut->setPagetitle( wfMsg( 'delete-confirm', $this->mTitle->getPrefixedText() ) );
 
                # Better double-check that it hasn't been deleted yet!
                $dbw = wfGetDB( DB_MASTER );
@@ -1962,8 +2047,17 @@ class Article {
                        return;
                }
 
+               # Hack for big sites
+               $bigHistory = $this->isBigDeletion();
+               if( $bigHistory && !$this->mTitle->userCan( 'bigdelete' ) ) {
+                       global $wgLang, $wgDeleteRevisionsLimit;
+                       $wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n",
+                               array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
+                       return;
+               }
+
                if( $confirm ) {
-                       $this->doDelete( $reason );
+                       $this->doDelete( $reason, $suppress );
                        if( $wgRequest->getCheck( 'wpWatch' ) ) {
                                $this->doWatch();
                        } elseif( $this->mTitle->userIsWatching() ) {
@@ -1980,11 +2074,40 @@ class Article {
                if( $hasHistory && !$confirm ) {
                        $skin=$wgUser->getSkin();
                        $wgOut->addHTML( '<strong>' . wfMsg( 'historywarning' ) . ' ' . $skin->historyLink() . '</strong>' );
+                       if( $bigHistory ) {
+                               global $wgLang, $wgDeleteRevisionsLimit;
+                               $wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n",
+                                       array( 'delete-warning-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
+                       }
                }
-               
+
                return $this->confirmDelete( '', $reason );
        }
 
+       /**
+        * @return bool whether or not the page surpasses $wgDeleteRevisionsLimit revisions
+        */
+       function isBigDeletion() {
+               global $wgDeleteRevisionsLimit;
+               if( $wgDeleteRevisionsLimit ) {
+                       $revCount = $this->estimateRevisionCount();
+                       return $revCount > $wgDeleteRevisionsLimit;
+               }
+               return false;
+       }
+
+       /**
+        * @return int approximate revision count
+        */
+       function estimateRevisionCount() {
+               $dbr = wfGetDB();
+               // For an exact count...
+               //return $dbr->selectField( 'revision', 'COUNT(*)',
+               //      array( 'rev_page' => $this->getId() ), __METHOD__ );
+               return $dbr->estimateRowCount( 'revision', '*',
+                       array( 'rev_page' => $this->getId() ), __METHOD__ );
+       }
+
        /**
         * Get the last N authors
         * @param int $num Number of revisions to get
@@ -2033,96 +2156,74 @@ class Article {
 
        /**
         * Output deletion confirmation dialog
+        * @param $par string FIXME: do we need this parameter? One Call from Article::delete with '' only.
+        * @param $reason string Prefilled reason
         */
        function confirmDelete( $par, $reason ) {
-               global $wgOut, $wgUser;
+               global $wgOut, $wgUser, $wgContLang;
+               $align = $wgContLang->isRtl() ? 'left' : 'right';
 
                wfDebug( "Article::confirmDelete\n" );
 
-               $sub = htmlspecialchars( $this->mTitle->getPrefixedText() );
-               $wgOut->setSubtitle( wfMsg( 'deletesub', $sub ) );
+               $wgOut->setSubtitle( wfMsg( 'delete-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->mTitle ) ) );
                $wgOut->setRobotpolicy( 'noindex,nofollow' );
-               $wgOut->addWikiText( wfMsg( 'confirmdeletetext' ) );
-
-               $formaction = $this->mTitle->escapeLocalURL( 'action=delete' . $par );
+               $wgOut->addWikiMsg( 'confirmdeletetext' );
 
-               $confirm = htmlspecialchars( wfMsg( 'deletepage' ) );
-               $delcom = Xml::label( wfMsg( 'deletecomment' ), 'wpDeleteReasonList' );
-               $token = htmlspecialchars( $wgUser->editToken() );
-               $watch = Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(), array( 'tabindex' => '2' ) );
-               
-               $mDeletereasonother = Xml::label( wfMsg( 'deleteotherreason' ), 'wpReason' );
-               $mDeletereasonotherlist = wfMsgHtml( 'deletereasonotherlist' );
-               $scDeleteReasonList = wfMsgForContent( 'deletereason-dropdown' );
-
-               $deleteReasonList = '';
-               if ( $scDeleteReasonList != '' && $scDeleteReasonList != '-' ) { 
-                       $deleteReasonList = "<option value=\"other\">$mDeletereasonotherlist</option>";
-                       $optgroup = "";
-                       foreach ( explode( "\n", $scDeleteReasonList ) as $option) {
-                               $value = trim( htmlspecialchars($option) );
-                               if ( $value == '' ) {
-                                       continue;
-                               } elseif ( substr( $value, 0, 1) == '*' && substr( $value, 1, 1) != '*' ) {
-                                       // A new group is starting ...
-                                       $value = trim( substr( $value, 1 ) );
-                                       $deleteReasonList .= "$optgroup<optgroup label=\"$value\">";
-                                       $optgroup = "</optgroup>";
-                               } elseif ( substr( $value, 0, 2) == '**' ) {
-                                       // groupmember
-                                       $selected = "";
-                                       $value = trim( substr( $value, 2 ) );
-                                       if ( $this->DeleteReasonList === $value)
-                                               $selected = ' selected="selected"';
-                                       $deleteReasonList .= "<option value=\"$value\"$selected>$value</option>";
-                               } else {
-                                       // groupless delete reason
-                                       $selected = "";
-                                       if ( $this->DeleteReasonList === $value)
-                                               $selected = ' selected="selected"';
-                                       $deleteReasonList .= "$optgroup<option value=\"$value\"$selected>$value</option>";
-                                       $optgroup = "";
-                               }
+               if( $wgUser->isAllowed( 'hiderevision' ) ) {
+                       $suppress = "<tr id=\"wpDeleteSuppressRow\" name=\"wpDeleteSuppressRow\"><td></td><td>";
+                       $suppress .= Xml::checkLabel( wfMsg( 'revdelete-suppress' ), 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '2' ) );
+                       $suppress .= "</td></tr>";
+               } else {
+                       $suppress = '';
+               }
+
+               $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->mTitle->getLocalURL( 'action=delete' . $par ), 'id' => 'deleteconfirm' ) ) .
+                       Xml::openElement( 'fieldset', array( 'id' => 'mw-delete-table' ) ) .
+                       Xml::element( 'legend', null, wfMsg( 'delete-legend' ) ) .
+                       Xml::openElement( 'table' ) .
+                       "<tr id=\"wpDeleteReasonListRow\">
+                               <td align='$align'>" .
+                                       Xml::label( wfMsg( 'deletecomment' ), 'wpDeleteReasonList' ) .
+                               "</td>
+                               <td>" .
+                                       Xml::listDropDown( 'wpDeleteReasonList',
+                                               wfMsgForContent( 'deletereason-dropdown' ),
+                                               wfMsgForContent( 'deletereasonotherlist' ), '', 'wpReasonDropDown', 1 ) .
+                               "</td>
+                       </tr>
+                       <tr id=\"wpDeleteReasonRow\">
+                               <td align='$align'>" .
+                                       Xml::label( wfMsg( 'deleteotherreason' ), 'wpReason' ) .
+                               "</td>
+                               <td>" .
+                                       Xml::input( 'wpReason', 60, $reason, array( 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ) ) .
+                               "</td>
+                       </tr>
+                       <tr>
+                               <td></td>
+                               <td>" .
+                                       Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(), array( 'tabindex' => '3' ) ) .
+                               "</td>
+                       </tr>
+                       $suppress
+                       <tr>
+                               <td></td>
+                               <td>" .
+                                       Xml::submitButton( wfMsg( 'deletepage' ), array( 'name' => 'wpConfirmB', 'id' => 'wpConfirmB', 'tabindex' => '4' ) ) .
+                               "</td>
+                       </tr>" .
+                       Xml::closeElement( 'table' ) .
+                       Xml::closeElement( 'fieldset' ) .
+                       Xml::hidden( 'wpEditToken', $wgUser->editToken() ) .
+                       Xml::closeElement( 'form' );
+
+                       if ( $wgUser->isAllowed( 'editinterface' ) ) {
+                               $skin = $wgUser->getSkin();
+                               $link = $skin->makeLink ( 'MediaWiki:Deletereason-dropdown', wfMsgHtml( 'delete-edit-reasonlist' ) );
+                               $form .= '<p class="mw-delete-editreasons">' . $link . '</p>';
                        }
-                       $deleteReasonList .= $optgroup;
-               }
-               $wgOut->addHTML( "
-<form id='deleteconfirm' method='post' action=\"{$formaction}\">
-       <table border='0'>
-               <tr id=\"wpDeleteReasonListRow\" name=\"wpDeleteReasonListRow\">
-                       <td align='right'>
-                               $delcom:
-                       </td>
-                       <td align='left'>
-                               <select tabindex='1' id='wpDeleteReasonList' name=\"wpDeleteReasonList\">
-                                       $deleteReasonList
-                               </select>
-                       </td>
-               </tr>
-               <tr id=\"wpDeleteReasonRow\" name=\"wpDeleteReasonRow\">
-                       <td>
-                               $mDeletereasonother
-                       </td>
-                       <td align='left'>
-                               <input type='text' maxlength='255' size='60' name='wpReason' id='wpReason' value=\"" . htmlspecialchars( $reason ) . "\" tabindex=\"2\" />
-                       </td>
-               </tr>
-               <tr>
-                       <td>&nbsp;</td>
-                       <td>$watch</td>
-               </tr>
-               <tr>
-                       <td>&nbsp;</td>
-                       <td>
-                               <input type='submit' name='wpConfirmB' id='wpConfirmB' value=\"{$confirm}\" tabindex=\"3\" />
-                       </td>
-               </tr>
-       </table>
-       <input type='hidden' name='wpEditToken' value=\"{$token}\" />
-</form>\n" );
-
-               $wgOut->returnToMain( false, $this->mTitle );
 
+               $wgOut->addHTML( $form );
                $this->showLogExtract( $wgOut );
        }
 
@@ -2131,36 +2232,32 @@ class Article {
         * Show relevant lines from the deletion log
         */
        function showLogExtract( $out ) {
-               $out->addHtml( '<h2>' . htmlspecialchars( LogPage::logName( 'delete' ) ) . '</h2>' );
-               $logViewer = new LogViewer(
-                       new LogReader(
-                               new FauxRequest(
-                                       array( 'page' => $this->mTitle->getPrefixedText(),
-                                              'type' => 'delete' ) ) ) );
-               $logViewer->showList( $out );
+               $out->addHtml( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
+               LogEventsList::showLogExtract( $out, 'delete', $this->mTitle->getPrefixedText() );
        }
 
 
        /**
         * Perform a deletion and output success or failure messages
         */
-       function doDelete( $reason ) {
+       function doDelete( $reason, $suppress = false ) {
                global $wgOut, $wgUser;
                wfDebug( __METHOD__."\n" );
+               
+               $id = $this->getId();
 
                if (wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason))) {
-                       if ( $this->doDeleteArticle( $reason ) ) {
-                               $deleted = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
+                       if ( $this->doDeleteArticle( $reason, $suppress ) ) {
+                               $deleted = $this->mTitle->getPrefixedText();
 
                                $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
                                $wgOut->setRobotpolicy( 'noindex,nofollow' );
 
-                               $loglink = '[[Special:Log/delete|' . wfMsg( 'deletionlog' ) . ']]';
-                               $text = wfMsg( 'deletedtext', $deleted, $loglink );
+                               $loglink = '[[Special:Log/delete|' . wfMsgNoTrans( 'deletionlog' ) . ']]';
 
-                               $wgOut->addWikiText( $text );
+                               $wgOut->addWikiMsg( 'deletedtext', $deleted, $loglink );
                                $wgOut->returnToMain( false );
-                               wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason));
+                               wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason, $id));
                        } else {
                                $wgOut->showFatalError( wfMsg( 'cannotdelete' ) );
                        }
@@ -2172,7 +2269,7 @@ class Article {
         * Deletes the article with database consistency, writes logs, purges caches
         * Returns success
         */
-       function doDeleteArticle( $reason ) {
+       function doDeleteArticle( $reason, $suppress = false ) {
                global $wgUseSquid, $wgDeferredUpdateList;
                global $wgUseTrackbacks;
 
@@ -2190,6 +2287,19 @@ class Article {
                $u = new SiteStatsUpdate( 0, 1, -(int)$this->isCountable( $this->getContent() ), -1 );
                array_push( $wgDeferredUpdateList, $u );
 
+               // Bitfields to further suppress the content
+               if ( $suppress ) {
+                       $bitfield = 0;
+                       // This should be 15...
+                       $bitfield |= Revision::DELETED_TEXT;
+                       $bitfield |= Revision::DELETED_COMMENT;
+                       $bitfield |= Revision::DELETED_USER;
+                       $bitfield |= Revision::DELETED_RESTRICTED;
+               } else {
+                       $bitfield = 'rev_deleted';
+               }
+
+               $dbw->begin();
                // For now, shunt the revision data into the archive table.
                // Text is *not* removed from the text table; bulk storage
                // is left intact to avoid breaking block-compression or
@@ -2213,8 +2323,9 @@ class Article {
                                'ar_text_id'    => 'rev_text_id',
                                'ar_text'       => '\'\'', // Be explicit to appease
                                'ar_flags'      => '\'\'', // MySQL's "strict mode"...
-                               'ar_len'                => 'rev_len',
+                               'ar_len'        => 'rev_len',
                                'ar_page_id'    => 'page_id',
+                               'ar_deleted'    => $bitfield
                        ), array(
                                'page_id' => $id,
                                'page_id = rev_page'
@@ -2224,12 +2335,25 @@ class Article {
                # Delete restrictions for it
                $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
 
+               # Fix category table counts
+               $cats = array();
+               $res = $dbw->select( 'categorylinks', 'cl_to',
+                       array( 'cl_from' => $id ), __METHOD__ );
+               foreach( $res as $row ) {
+                       $cats []= $row->cl_to;
+               }
+               $this->updateCategoryCounts( array(), $cats );
+
                # Now that it's safely backed up, delete it
                $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__);
+               $ok = ( $dbw->affectedRows() > 0 ); // getArticleId() uses slave, could be laggy
+               if( !$ok ) {
+                       $dbw->rollback();
+                       return false;
+               }
 
                # If using cascading deletes, we can skip some explicit deletes
                if ( !$dbw->cascadingDeletes() ) {
-
                        $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
 
                        if ($wgUseTrackbacks)
@@ -2249,96 +2373,123 @@ class Article {
                if ( !$dbw->cleanupTriggers() ) {
 
                        # Clean up recentchanges entries...
-                       $dbw->delete( 'recentchanges', array( 'rc_namespace' => $ns, 'rc_title' => $t ), __METHOD__ );
+                       $dbw->delete( 'recentchanges',
+                               array( 'rc_namespace' => $ns, 'rc_title' => $t, 'rc_type != '.RC_LOG ),
+                               __METHOD__ );
                }
+               $dbw->commit();
 
                # Clear caches
                Article::onArticleDelete( $this->mTitle );
 
-               # Log the deletion
-               $log = new LogPage( 'delete' );
-               $log->addEntry( 'delete', $this->mTitle, $reason );
-
                # Clear the cached article id so the interface doesn't act like we exist
                $this->mTitle->resetArticleID( 0 );
                $this->mTitle->mArticleID = 0;
+
+               # Log the deletion, if the page was suppressed, log it at Oversight instead
+               $logtype = $suppress ? 'suppress' : 'delete';
+               $log = new LogPage( $logtype );
+
+               # Make sure logging got through
+               $log->addEntry( 'delete', $this->mTitle, $reason, array() );
+
                return true;
        }
 
        /**
         * Roll back the most recent consecutive set of edits to a page
         * from the same user; fails if there are no eligible edits to
-        * roll back to, e.g. user is the sole contributor
+        * roll back to, e.g. user is the sole contributor. This function
+        * performs permissions checks on $wgUser, then calls commitRollback()
+        * to do the dirty work
         *
-        * @param string $fromP - Name of the user whose edits to rollback. 
+        * @param string $fromP - Name of the user whose edits to rollback.
         * @param string $summary - Custom summary. Set to default summary if empty.
         * @param string $token - Rollback token.
-        * @param bool $bot - If true, mark all reverted edits as bot.
-        * 
-        * @param array $resultDetails contains result-specific dict of additional values
-        *    ALREADY_ROLLED : 'current' (rev)
-        *    SUCCESS        : 'summary' (str), 'current' (rev), 'target' (rev)
-        * 
-        * @return self::SUCCESS on succes, self::* on failure
+        * @param bool   $bot - If true, mark all reverted edits as bot.
+        *
+        * @param array $resultDetails contains result-specific array of additional values
+        *    'alreadyrolled' : 'current' (rev)
+        *    success        : 'summary' (str), 'current' (rev), 'target' (rev)
+        *
+        * @return array of errors, each error formatted as
+        *   array(messagekey, param1, param2, ...).
+        * On success, the array is empty.  This array can also be passed to
+        * OutputPage::showPermissionsErrorPage().
         */
        public function doRollback( $fromP, $summary, $token, $bot, &$resultDetails ) {
-               global $wgUser, $wgUseRCPatrol;
+               global $wgUser;
                $resultDetails = null;
 
-               # Just in case it's being called from elsewhere         
-
-               if( $wgUser->isAllowed( 'rollback' ) && $this->mTitle->userCan( 'edit' ) ) {
-                       if( $wgUser->isBlocked() ) {
-                               return self::BLOCKED;
-                       }
-               } else {
-                       return self::PERM_DENIED;
-               }
-                       
-               if ( wfReadOnly() ) {
-                       return self::READONLY;
-               }
-
+               # Check permissions
+               $errors = array_merge( $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ),
+                                               $this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser ) );
                if( !$wgUser->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) )
-                       return self::BAD_TOKEN;
+                       $errors[] = array( 'sessionfailure' );
 
                if ( $wgUser->pingLimiter('rollback') || $wgUser->pingLimiter() ) {
-                       return self::RATE_LIMITED;
+                       $errors[] = array( 'actionthrottledtext' );
                }
+               # If there were errors, bail out now
+               if(!empty($errors))
+                       return $errors;
+
+               return $this->commitRollback($fromP, $summary, $bot, $resultDetails);
+       }
 
+       /**
+        * Backend implementation of doRollback(), please refer there for parameter
+        * and return value documentation
+        *
+        * NOTE: This function does NOT check ANY permissions, it just commits the
+        * rollback to the DB Therefore, you should only call this function direct-
+        * ly if you want to use custom permissions checks. If you don't, use
+        * doRollback() instead.
+        */
+       public function commitRollback($fromP, $summary, $bot, &$resultDetails) {
+               global $wgUseRCPatrol, $wgUser, $wgLang;
                $dbw = wfGetDB( DB_MASTER );
 
+               if( wfReadOnly() ) {
+                       return array( array( 'readonlytext' ) );
+               }
+
                # Get the last editor
                $current = Revision::newFromTitle( $this->mTitle );
                if( is_null( $current ) ) {
                        # Something wrong... no page?
-                       return self::BAD_TITLE;
+                       return array(array('notanarticle'));
                }
 
                $from = str_replace( '_', ' ', $fromP );
                if( $from != $current->getUserText() ) {
                        $resultDetails = array( 'current' => $current );
-                       return self::ALREADY_ROLLED;
+                       return array(array('alreadyrolled',
+                               htmlspecialchars($this->mTitle->getPrefixedText()),
+                               htmlspecialchars($fromP),
+                               htmlspecialchars($current->getUserText())
+                       ));
                }
 
                # Get the last edit not by this guy
                $user = intval( $current->getUser() );
                $user_text = $dbw->addQuotes( $current->getUserText() );
                $s = $dbw->selectRow( 'revision',
-                       array( 'rev_id', 'rev_timestamp' ),
-                       array(
-                               'rev_page' => $current->getPage(),
+                       array( 'rev_id', 'rev_timestamp', 'rev_deleted' ),
+                       array(  'rev_page' => $current->getPage(),
                                "rev_user <> {$user} OR rev_user_text <> {$user_text}"
                        ), __METHOD__,
-                       array(
-                               'USE INDEX' => 'page_timestamp',
+                       array(  'USE INDEX' => 'page_timestamp',
                                'ORDER BY'  => 'rev_timestamp DESC' )
                        );
                if( $s === false ) {
-                       # Something wrong
-                       return self::ONLY_AUTHOR;
+                       # No one else ever edited this page
+                       return array(array('cantrollback'));
+               } else if( $s->rev_deleted & REVISION::DELETED_TEXT || $s->rev_deleted & REVISION::DELETED_USER ) {
+                       # Only admins can see this text
+                       return array(array('notvisiblerev'));
                }
-       
+
                $set = array();
                if ( $bot && $wgUser->isAllowed('markbotedits') ) {
                        # Mark all reverted edits as bot
@@ -2359,10 +2510,19 @@ class Article {
                                );
                }
 
-               # Get the edit summary
+               # Generate the edit summary if necessary
                $target = Revision::newFromId( $s->rev_id );
-               if( empty( $summary ) )
-                       $summary = wfMsgForContent( 'revertpage', $target->getUserText(), $from );
+               if( empty( $summary ) ){
+                       $summary = wfMsgForContent( 'revertpage' );
+               }
+               
+               # Allow the custom summary to use the same args as the default message
+               $args = array(
+                       $target->getUserText(), $from, $s->rev_id,
+                       $wgLang->timeanddate(wfTimestamp(TS_MW, $s->rev_timestamp), true),
+                       $current->getId(), $wgLang->timeanddate($current->getTimestamp())
+               );
+               $summary = wfMsgReplaceArgs( $summary, $args ); 
 
                # Save
                $flags = EDIT_UPDATE;
@@ -2370,9 +2530,9 @@ class Article {
                if ($wgUser->isAllowed('minoredit'))
                        $flags |= EDIT_MINOR;
 
-               if( $bot )
+               if( $bot && ($wgUser->isAllowed('markbotedits') || $wgUser->isAllowed('bot')) )
                        $flags |= EDIT_FORCE_BOT;
-               $this->doEdit( $target->getText(), $summary, $flags );
+               $this->doEdit( $target->getText(), $summary, $flags, $target->getId() );
 
                wfRunHooks( 'ArticleRollbackComplete', array( $this, $wgUser, $target ) );
 
@@ -2381,7 +2541,7 @@ class Article {
                        'current' => $current,
                        'target' => $target,
                );
-               return self::SUCCESS;
+               return array();
        }
 
        /**
@@ -2389,19 +2549,8 @@ class Article {
         */
        function rollback() {
                global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol;
-
                $details = null;
 
-               # Skip the permissions-checking in doRollback() itself, by checking permissions here.
-
-               $perm_errors = array_merge( $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser ),
-                                               $this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser ) );
-
-               if (count($perm_errors)) {
-                       $wgOut->showPermissionsErrorPage( $perm_errors );
-                       return;
-               }
-
                $result = $this->doRollback(
                        $wgRequest->getVal( 'from' ),
                        $wgRequest->getText( 'summary' ),
@@ -2410,61 +2559,57 @@ class Article {
                        $details
                );
 
-               switch( $result ) {
-                       case self::BLOCKED:
-                               $wgOut->blockedPage();
-                               break;
-                       case self::PERM_DENIED:
-                               $wgOut->permissionRequired( 'rollback' );
-                               break;
-                       case self::READONLY:
-                               $wgOut->readOnlyPage( $this->getContent() );
-                               break;
-                       case self::BAD_TOKEN:
-                               $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) );
-                               $wgOut->addWikiText( wfMsg( 'sessionfailure' ) );
-                               break;
-                       case self::BAD_TITLE:
-                               $wgOut->addHtml( wfMsg( 'notanarticle' ) );
-                               break;
-                       case self::ALREADY_ROLLED:
+               if( in_array( array( 'blocked' ), $result ) ) {
+                       $wgOut->blockedPage();
+                       return;
+               }
+               if( in_array( array( 'actionthrottledtext' ), $result ) ) {
+                       $wgOut->rateLimited();
+                       return;
+               }
+               if( isset( $result[0][0] ) && ( $result[0][0] == 'alreadyrolled' || $result[0][0] == 'cantrollback' ) ){
+                       $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) );
+                       $errArray = $result[0];
+                       $errMsg = array_shift( $errArray );
+                       $wgOut->addWikiMsgArray( $errMsg, $errArray );
+                       if( isset( $details['current'] ) ){
                                $current = $details['current'];
-                               $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) );
-                               $wgOut->addWikiText(
-                                       wfMsg( 'alreadyrolled',
-                                               htmlspecialchars( $this->mTitle->getPrefixedText() ),
-                                               htmlspecialchars( $wgRequest->getVal( 'from' ) ),
-                                               htmlspecialchars( $current->getUserText() )
-                                       )
-                               );
                                if( $current->getComment() != '' ) {
-                                       $wgOut->addHtml( wfMsg( 'editcomment',
-                                               $wgUser->getSkin()->formatComment( $current->getComment() ) ) );
+                                       $wgOut->addWikiMsgArray( 'editcomment', array( $wgUser->getSkin()->formatComment( $current->getComment() ) ), array( 'replaceafter' ) );
                                }
-                               break;
-                       case self::ONLY_AUTHOR:
-                               $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) );
-                               $wgOut->addHtml( wfMsg( 'cantrollback' ) );
-                               break;
-                       case self::RATE_LIMITED:
-                               $wgOut->rateLimited();
-                               break;
-                       case self::SUCCESS:
-                               $current = $details['current'];
-                               $target = $details['target'];
-                               $wgOut->setPageTitle( wfMsg( 'actioncomplete' ) );
-                               $wgOut->setRobotPolicy( 'noindex,nofollow' );
-                               $old = $wgUser->getSkin()->userLink( $current->getUser(), $current->getUserText() )
-                                       . $wgUser->getSkin()->userToolLinks( $current->getUser(), $current->getUserText() );
-                               $new = $wgUser->getSkin()->userLink( $target->getUser(), $target->getUserText() )
-                                       . $wgUser->getSkin()->userToolLinks( $target->getUser(), $target->getUserText() );
-                               $wgOut->addHtml( wfMsgExt( 'rollback-success', array( 'parse', 'replaceafter' ), $old, $new ) );
-                               $wgOut->returnToMain( false, $this->mTitle );
-                               break;
-                       default:
-                               throw new MWException( __METHOD__ . ": Unknown return value `{$result}`" );
+                       }
+                       return;
+               }
+               # Display permissions errors before read-only message -- there's no
+               # point in misleading the user into thinking the inability to rollback
+               # is only temporary.
+               if( !empty($result) && $result !== array( array('readonlytext') ) ) {
+                       # array_diff is completely broken for arrays of arrays, sigh.  Re-
+                       # move any 'readonlytext' error manually.
+                       $out = array();
+                       foreach( $result as $error ) {
+                               if( $error != array( 'readonlytext' ) ) {
+                                       $out []= $error;
+                               }
+                       }
+                       $wgOut->showPermissionsErrorPage( $out );
+                       return;
+               }
+               if( $result == array( array('readonlytext') ) ) {
+                       $wgOut->readOnlyPage();
+                       return;
                }
 
+               $current = $details['current'];
+               $target = $details['target'];
+               $wgOut->setPageTitle( wfMsg( 'actioncomplete' ) );
+               $wgOut->setRobotPolicy( 'noindex,nofollow' );
+               $old = $wgUser->getSkin()->userLink( $current->getUser(), $current->getUserText() )
+                       . $wgUser->getSkin()->userToolLinks( $current->getUser(), $current->getUserText() );
+               $new = $wgUser->getSkin()->userLink( $target->getUser(), $target->getUserText() )
+                       . $wgUser->getSkin()->userToolLinks( $target->getUser(), $target->getUserText() );
+               $wgOut->addHtml( wfMsgExt( 'rollback-success', array( 'parse', 'replaceafter' ), $old, $new ) );
+               $wgOut->returnToMain( false, $this->mTitle );
        }
 
 
@@ -2473,11 +2618,12 @@ class Article {
         * @private
         */
        function viewUpdates() {
-               global $wgDeferredUpdateList;
+               global $wgDeferredUpdateList, $wgUser;
 
                if ( 0 != $this->getID() ) {
+                       # Don't update page view counters on views from bot users (bug 14044)
                        global $wgDisableCounters;
-                       if( !$wgDisableCounters ) {
+                       if( !$wgDisableCounters && !$wgUser->isAllowed( 'bot' ) ) {
                                Article::incViewCount( $this->getID() );
                                $u = new SiteStatsUpdate( 1, 0, 0 );
                                array_push( $wgDeferredUpdateList, $u );
@@ -2485,7 +2631,6 @@ class Article {
                }
 
                # Update newtalk / watchlist notification status
-               global $wgUser;
                $wgUser->clearNotification( $this->mTitle );
        }
 
@@ -2526,7 +2671,7 @@ class Article {
         * @param $changed Whether or not the content actually changed
         */
        function editUpdates( $text, $summary, $minoredit, $timestamp_of_pagechange, $newid, $changed = true ) {
-               global $wgDeferredUpdateList, $wgMessageCache, $wgUser, $wgParser;
+               global $wgDeferredUpdateList, $wgMessageCache, $wgUser, $wgParser, $wgEnableParserCache;
 
                wfProfileIn( __METHOD__ );
 
@@ -2541,8 +2686,10 @@ class Article {
                }
 
                # Save it to the parser cache
-               $parserCache =& ParserCache::singleton();
-               $parserCache->save( $editInfo->output, $this, $wgUser );
+               if ( $wgEnableParserCache ) {
+                       $parserCache = ParserCache::singleton();
+                       $parserCache->save( $editInfo->output, $this, $wgUser );
+               }
 
                # Update the links tables
                $u = new LinksUpdate( $this->mTitle, $editInfo->output );
@@ -2640,7 +2787,7 @@ class Article {
                $sk = $wgUser->getSkin();
                $lnk = $current
                        ? wfMsg( 'currentrevisionlink' )
-                       : $lnk = $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'currentrevisionlink' ) );
+                       : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'currentrevisionlink' ) );
                $curdiff = $current
                        ? wfMsg( 'diff' )
                        : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=cur&oldid='.$oldid );
@@ -2658,16 +2805,38 @@ class Article {
                        ? wfMsg( 'diff' )
                        : $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'diff' ), 'diff=next&oldid='.$oldid );
 
-               $userlinks = $sk->userLink( $revision->getUser(), $revision->getUserText() )
-                                               . $sk->userToolLinks( $revision->getUser(), $revision->getUserText() );
+               $cdel='';
+               if( $wgUser->isAllowed( 'deleterevision' ) ) {
+                       $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+                       if( $revision->isCurrent() ) {
+                       // We don't handle top deleted edits too well
+                               $cdel = wfMsgHtml('rev-delundel');
+                       } else if( !$revision->userCan( Revision::DELETED_RESTRICTED ) ) {
+                       // If revision was hidden from sysops
+                               $cdel = wfMsgHtml('rev-delundel');
+                       } else {
+                               $cdel = $sk->makeKnownLinkObj( $revdel,
+                                       wfMsgHtml('rev-delundel'),
+                                       'target=' . urlencode( $this->mTitle->getPrefixedDbkey() ) .
+                                       '&oldid=' . urlencode( $oldid ) );
+                               // Bolden oversighted content
+                               if( $revision->isDeleted( Revision::DELETED_RESTRICTED ) )
+                                       $cdel = "<strong>$cdel</strong>";
+                       }
+                       $cdel = "(<small>$cdel</small>) ";
+               }
+               # Show user links if allowed to see them. Normally they
+               # are hidden regardless, but since we can already see the text here...
+               $userlinks = $sk->revUserTools( $revision, false );
 
                $m = wfMsg( 'revision-info-current' );
                $infomsg = $current && !wfEmptyMsg( 'revision-info-current', $m ) && $m != '-'
                        ? 'revision-info-current'
                        : 'revision-info';
-                       
+
                $r = "\n\t\t\t\t<div id=\"mw-{$infomsg}\">" . wfMsg( $infomsg, $td, $userlinks ) . "</div>\n" .
-                    "\n\t\t\t\t<div id=\"mw-revision-nav\">" . wfMsg( 'revision-nav', $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t";
+
+                    "\n\t\t\t\t<div id=\"mw-revision-nav\">" . $cdel . wfMsg( 'revision-nav', $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t";
                $wgOut->setSubtitle( $r );
        }
 
@@ -2725,9 +2894,9 @@ class Article {
                $printable = $wgRequest->getVal( 'printable' );
                $page      = $wgRequest->getVal( 'page' );
 
-               //check for non-standard user language; this covers uselang, 
+               //check for non-standard user language; this covers uselang,
                //and extensions for auto-detecting user language.
-               $ulang     = $wgLang->getCode(); 
+               $ulang     = $wgLang->getCode();
                $clang     = $wgContLang->getCode();
 
                $cacheable = $wgUseFileCache
@@ -2808,6 +2977,8 @@ class Article {
                $revision->insertOn( $dbw );
                $this->updateRevisionOn( $dbw, $revision );
                $dbw->commit();
+               
+               wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, false) );
 
                wfProfileOut( __METHOD__ );
        }
@@ -2905,6 +3076,15 @@ class Article {
        static function onArticleDelete( $title ) {
                global $wgUseFileCache, $wgMessageCache;
 
+               // Update existence markers on article/talk tabs...
+               if( $title->isTalkPage() ) {
+                       $other = $title->getSubjectPage();
+               } else {
+                       $other = $title->getTalkPage();
+               }
+               $other->invalidateCache();
+               $other->purgeSquid();
+
                $title->touchLinks();
                $title->purgeSquid();
 
@@ -2914,9 +3094,20 @@ class Article {
                        @unlink( $cm->fileCacheName() );
                }
 
-               if( $title->getNamespace() == NS_MEDIAWIKI) {
+               # Messages
+               if( $title->getNamespace() == NS_MEDIAWIKI ) {
                        $wgMessageCache->replace( $title->getDBkey(), false );
                }
+               # Images
+               if( $title->getNamespace() == NS_IMAGE ) {
+                       $update = new HTMLCacheUpdate( $title, 'imagelinks' );
+                       $update->doUpdate();
+               }
+               # User talk pages
+               if( $title->getNamespace() == NS_USER_TALK ) {
+                       $user = User::newFromName( $title->getText(), false );
+                       $user->setNewtalk( false );
+               }
        }
 
        /**
@@ -2926,8 +3117,10 @@ class Article {
                global $wgDeferredUpdateList, $wgUseFileCache;
 
                // Invalidate caches of articles which include this page
-               $update = new HTMLCacheUpdate( $title, 'templatelinks' );
-               $wgDeferredUpdateList[] = $update;
+               $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'templatelinks' );
+
+               // Invalidate the caches of all pages which redirect here
+               $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'redirect' );
 
                # Purge squid for this page only
                $title->purgeSquid();
@@ -2941,6 +3134,15 @@ class Article {
 
        /**#@-*/
 
+       /**
+        * Overriden by ImagePage class, only present here to avoid a fatal error
+        * Called for ?action=revert
+        */
+       public function revert(){
+               global $wgOut;
+               $wgOut->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
+       }
+
        /**
         * Info about this page
         * Called for ?action=info when $wgAllowPageInfo is on.
@@ -3067,6 +3269,36 @@ class Article {
                return $result;
        }
 
+       /**
+        * Returns a list of hidden categories this page is a member of.
+        * Uses the page_props and categorylinks tables.
+        *
+        * @return array Array of Title objects
+        */
+       function getHiddenCategories() {
+               $result = array();
+               $id = $this->mTitle->getArticleID();
+               if( $id == 0 ) {
+                       return array();
+               }
+
+               $dbr = wfGetDB( DB_SLAVE );
+               $res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ),
+                       array( 'cl_to' ),
+                       array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
+                               'page_namespace' => NS_CATEGORY, 'page_title=cl_to'),
+                       'Article:getHiddenCategories' );
+               if ( false !== $res ) {
+                       if ( $dbr->numRows( $res ) ) {
+                               while ( $row = $dbr->fetchObject( $res ) ) {
+                                       $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
+                               }
+                       }
+               }
+               $dbr->freeResult( $res );
+               return $result;
+       }
+
        /**
         * Return an auto-generated summary if the text provided is a redirect.
         *
@@ -3110,6 +3342,8 @@ class Article {
        * @return string An appropriate autosummary, or an empty string.
        */
        public static function getAutosummary( $oldtext, $newtext, $flags ) {
+               global $wgUseAutomaticEditSummaries;
+               if ( !$wgUseAutomaticEditSummaries ) return '';
 
                # This code is UGLY UGLY UGLY.
                # Somebody PLEASE come up with a more elegant way to do it.
@@ -3153,7 +3387,7 @@ class Article {
         * @param bool    $cache
         */
        public function outputWikiText( $text, $cache = true ) {
-               global $wgParser, $wgUser, $wgOut;
+               global $wgParser, $wgUser, $wgOut, $wgEnableParserCache;
 
                $popts = $wgOut->parserOptions();
                $popts->setTidy(true);
@@ -3162,8 +3396,8 @@ class Article {
                        $popts, true, true, $this->getRevIdFetched() );
                $popts->setTidy(false);
                $popts->enableLimitReport( false );
-               if ( $cache && $this && $parserOutput->getCacheTime() != -1 ) {
-                       $parserCache =& ParserCache::singleton();
+               if ( $wgEnableParserCache && $cache && $this && $parserOutput->getCacheTime() != -1 ) {
+                       $parserCache = ParserCache::singleton();
                        $parserCache->save( $parserOutput, $this, $wgUser );
                }
 
@@ -3223,4 +3457,60 @@ class Article {
                $wgOut->addParserOutput( $parserOutput );
        }
 
+       /**
+        * Update all the appropriate counts in the category table, given that
+        * we've added the categories $added and deleted the categories $deleted.
+        *
+        * @param $added array   The names of categories that were added
+        * @param $deleted array The names of categories that were deleted
+        * @return null
+        */
+       public function updateCategoryCounts( $added, $deleted ) {
+               $ns = $this->mTitle->getNamespace();
+               $dbw = wfGetDB( DB_MASTER );
+
+               # First make sure the rows exist.  If one of the "deleted" ones didn't
+               # exist, we might legitimately not create it, but it's simpler to just
+               # create it and then give it a negative value, since the value is bogus
+               # anyway.
+               #
+               # Sometimes I wish we had INSERT ... ON DUPLICATE KEY UPDATE.
+               $insertCats = array_merge( $added, $deleted );
+               if( !$insertCats ) {
+                       # Okay, nothing to do
+                       return;
+               }
+               $insertRows = array();
+               foreach( $insertCats as $cat ) {
+                       $insertRows[] = array( 'cat_title' => $cat );
+               }
+               $dbw->insert( 'category', $insertRows, __METHOD__, 'IGNORE' );
+
+               $addFields    = array( 'cat_pages = cat_pages + 1' );
+               $removeFields = array( 'cat_pages = cat_pages - 1' );
+               if( $ns == NS_CATEGORY ) {
+                       $addFields[]    = 'cat_subcats = cat_subcats + 1';
+                       $removeFields[] = 'cat_subcats = cat_subcats - 1';
+               } elseif( $ns == NS_IMAGE ) {
+                       $addFields[]    = 'cat_files = cat_files + 1';
+                       $removeFields[] = 'cat_files = cat_files - 1';
+               }
+
+               if ( $added ) {
+                       $dbw->update(
+                               'category',
+                               $addFields,
+                               array( 'cat_title' => $added ),
+                               __METHOD__
+                       );
+               }
+               if ( $deleted ) {
+                       $dbw->update(
+                               'category',
+                               $removeFields,
+                               array( 'cat_title' => $deleted ),
+                               __METHOD__
+                       );
+               }
+       }
 }