*Clean up deletion of revisions and remove some gaps
authorAaron Schulz <aaron@users.mediawiki.org>
Mon, 1 Oct 2007 19:38:28 +0000 (19:38 +0000)
committerAaron Schulz <aaron@users.mediawiki.org>
Mon, 1 Oct 2007 19:38:28 +0000 (19:38 +0000)
*Allow blocking of users to hide names
*Implement revision deletion for images/deleted files/deleted revs
*Log deletion set off for now
*Add 'hidden' file dir
*Dissallow merging via undelete (which was inefficient and hard to reverse)
*Use restore points and diffs to special:undelete
*Add a special page to merge pages
*Get changeslist to use tables to avoid ugly formatting
*Add logs into RC for rebuildrecentchanges.php
*Add private logs
*List private logs at specialpages
*Tweak/add some deletion and merge messages

34 files changed:
includes/Article.php
includes/ChangesList.php
includes/DefaultSettings.php
includes/DifferenceEngine.php
includes/EditPage.php
includes/FileDeleteForm.php
includes/ImagePage.php
includes/Linker.php
includes/LogPage.php
includes/PageHistory.php
includes/RecentChange.php
includes/Revision.php
includes/Setup.php
includes/SpecialBlockip.php
includes/SpecialContributions.php
includes/SpecialIpblocklist.php
includes/SpecialLog.php
includes/SpecialMergeHistory.php [new file with mode: 0644]
includes/SpecialPage.php
includes/SpecialRecentchanges.php
includes/SpecialRevisiondelete.php
includes/SpecialSpecialpages.php
includes/SpecialUndelete.php
includes/SpecialWatchlist.php
includes/Title.php
includes/filerepo/ArchivedFile.php
includes/filerepo/FSRepo.php
includes/filerepo/File.php
includes/filerepo/FileRepo.php
includes/filerepo/LocalFile.php
includes/filerepo/OldLocalFile.php
languages/messages/MessagesEn.php
maintenance/rebuildrecentchanges.inc
maintenance/rebuildrecentchanges.php

index c0484e7..cf1594f 100644 (file)
@@ -390,6 +390,7 @@ class Article {
                // 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();
@@ -1069,7 +1070,6 @@ class Article {
                $result = $dbw->affectedRows() != 0;
 
                if ($result) {
-                       // FIXME: Should the result from updateRedirectOn() be returned instead?
                        $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
                }
 
@@ -1494,6 +1494,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;
@@ -1689,7 +1690,7 @@ class Article {
         * @return bool true on success
         */
        function updateRestrictions( $limit = array(), $reason = '', $cascade = 0, $expiry = null ) {
-               global $wgUser, $wgRestrictionTypes, $wgContLang;
+               global $wgUser, $wgRestrictionTypes, $wgContLang, $wgGroupPermissions;
 
                $id = $this->mTitle->getArticleID();
                if( !$wgUser->isAllowed( 'protect' ) || wfReadOnly() || $id == 0 ) {
@@ -1719,7 +1720,6 @@ class Article {
 
                # If nothing's changed, do nothing
                if( $changed ) {
-                       global $wgGroupPermissions;
                        if( wfRunHooks( 'ArticleProtect', array( &$this, &$wgUser, $limit, $reason ) ) ) {
 
                                $dbw = wfGetDB( DB_MASTER );
@@ -1832,6 +1832,8 @@ class Article {
                $confirm = $wgRequest->wasPosted() &&
                        $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) );
                $reason = $wgRequest->getText( 'wpReason' );
+               # Flag to hide all contents of the archived revisions
+               $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed('deleterevision');
 
                # This code desperately needs to be totally rewritten
 
@@ -1856,7 +1858,7 @@ class Article {
                }
 
                if( $confirm ) {
-                       $this->doDelete( $reason );
+                       $this->doDelete( $reason, $suppress );
                        if( $wgRequest->getCheck( 'wpWatch' ) ) {
                                $this->doWatch();
                        } elseif( $this->mTitle->userIsWatching() ) {
@@ -2002,7 +2004,14 @@ class Article {
                $delcom = htmlspecialchars( wfMsg( 'deletecomment' ) );
                $token = htmlspecialchars( $wgUser->editToken() );
                $watch = Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching(), array( 'tabindex' => '2' ) );
-
+               if ( $wgUser->isAllowed( 'deleterevision' ) ) {
+                       $supress = "<tr><td>&nbsp;</td><td>";
+                       $supress .= Xml::checkLabel( wfMsg( 'revdelete-suppress' ), 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '2' ) );
+                       $supress .= "</td></tr>";
+               } else {
+                       $supress = '';
+               }
+               
                $wgOut->addHTML( "
 <form id='deleteconfirm' method='post' action=\"{$formaction}\">
        <table border='0'>
@@ -2014,6 +2023,7 @@ class Article {
                                <input type='text' size='60' name='wpReason' id='wpReason' value=\"" . htmlspecialchars( $reason ) . "\" tabindex=\"1\" />
                        </td>
                </tr>
+               $supress
                <tr>
                        <td>&nbsp;</td>
                        <td>$watch</td>
@@ -2051,12 +2061,12 @@ class Article {
        /**
         * Perform a deletion and output success or failure messages
         */
-       function doDelete( $reason ) {
+       function doDelete( $reason, $suppress = false ) {
                global $wgOut, $wgUser;
                wfDebug( __METHOD__."\n" );
 
                if (wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason))) {
-                       if ( $this->doDeleteArticle( $reason ) ) {
+                       if ( $this->doDeleteArticle( $reason, $suppress ) ) {
                                $deleted = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
 
                                $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
@@ -2069,7 +2079,7 @@ class Article {
                                $wgOut->returnToMain( false );
                                wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason));
                        } else {
-                               $wgOut->showFatalError( wfMsg( 'cannotdelete' ) );
+                               $wgOut->showFatalError( wfMsg( 'cannotdelete' ).'<br/>'.wfMsg('cannotdelete-merge') );
                        }
                }
        }
@@ -2079,7 +2089,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;
 
@@ -2093,10 +2103,30 @@ class Article {
                if ( $t == '' || $id == 0 ) {
                        return false;
                }
+               // Do not fuck up histories by merging them in annoying, unrevertable ways
+               // This page id should match any deleted ones (excepting NULL values)
+               $otherpages = $dbw->selectField( 'archive', 'COUNT(*)',
+                       array('ar_namespace' => $ns, 'ar_title' => $t, 
+                               'ar_page_id IS NOT NULL', "ar_page_id != $id" ),
+                       __METHOD__ );
+               if( $otherpages )
+                       return false;
 
                $u = new SiteStatsUpdate( 0, 1, -(int)$this->isCountable( $this->getContent() ), -1 );
                array_push( $wgDeferredUpdateList, $u );
 
+               // Bitfields to further supress 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';
+               }
+               
                // 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
@@ -2122,6 +2152,7 @@ class Article {
                                'ar_flags'      => '\'\'', // MySQL's "strict mode"...
                                'ar_len'                => 'rev_len',
                                'ar_page_id'    => 'page_id',
+                               'ar_deleted'    => $bitfield
                        ), array(
                                'page_id' => $id,
                                'page_id = rev_page'
@@ -2162,8 +2193,9 @@ class Article {
                # Clear caches
                Article::onArticleDelete( $this->mTitle );
 
-               # Log the deletion
-               $log = new LogPage( 'delete' );
+               # Log the deletion, if the page was suppressed, log it at Oversight instead
+               $logtype = ($suppress) ? 'oversight' : 'delete';
+               $log = new LogPage( $logtype );
                $log->addEntry( 'delete', $this->mTitle, $reason );
 
                # Clear the cached article id so the interface doesn't act like we exist
@@ -2262,8 +2294,13 @@ class Article {
                                );
                }
 
-               # Get the edit summary
                $target = Revision::newFromId( $s->rev_id );
+               # Revision *must* be public and we don't well handle deleted edits on top
+               if ( $target->isDeleted(REVISION::DELETED_TEXT) ) {
+                       $wgOut->setPageTitle( wfMsg('rollbackfailed') );
+                       $wgOut->addHTML( wfMsg( 'missingarticle' ) );
+               }
+               # Get the edit summary
                if( empty( $summary ) )
                        $summary = wfMsgForContent( 'revertpage', $target->getUserText(), $from );
 
@@ -2524,8 +2561,28 @@ 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>) ";
+               }
+               
+               $userlinks = $sk->revUserTools( $revision, true );
 
                $m = wfMsg( 'revision-info-current' );
                $infomsg = $current && !wfEmptyMsg( 'revision-info-current', $m ) && $m != '-'
@@ -2533,7 +2590,8 @@ class Article {
                        : '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 );
        }
 
index 8d0f950..71df7dd 100644 (file)
@@ -75,7 +75,7 @@ class ChangesList {
                                : $nothing;
                $f .= $bot ? '<span class="bot">' . $this->message['boteditletter'] . '</span>' : $nothing;
                $f .= $patrolled ? '<span class="unpatrolled">!</span>' : $nothing;
-               return $f;
+               return "<tt>$f</tt>";
        }
 
        /**
@@ -101,6 +101,32 @@ class ChangesList {
                }
        }
 
+       /**
+        * int $field one of DELETED_* bitfield constants
+        * @return bool
+        */
+       function isDeleted( $rc, $field ) {
+               return ($rc->mAttribs['rc_deleted'] & $field) == $field;
+       }
+       
+       /**
+        * Determine if the current user is allowed to view a particular
+        * field of this revision, if it's marked as deleted.
+        * @param int $field
+        * @return bool
+        */
+       function userCan( $rc, $field ) {
+               if( ( $rc->mAttribs['rc_deleted'] & $field ) == $field ) {
+                       global $wgUser;
+                       $permission = ( $rc->mAttribs['rc_deleted'] & Revision::DELETED_RESTRICTED ) == Revision::DELETED_RESTRICTED
+                               ? 'hiderevision'
+                               : 'deleterevision';
+                       wfDebug( "Checking for $permission due to $field match on $rc->mAttribs['rc_deleted']\n" );
+                       return $wgUser->isAllowed( $permission );
+               } else {
+                       return true;
+               }
+       }
 
        function insertMove( &$s, $rc ) {
                # Diff
@@ -136,10 +162,11 @@ class ChangesList {
                $s .= '(' . $this->skin->makeKnownLinkObj($title, $logname ) . ')';
        }
 
-
        function insertDiffHist(&$s, &$rc, $unpatrolled) {
                # Diff link
-               if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) {
+               if( !$this->userCan($rc,Revision::DELETED_TEXT) ) {
+                       $diffLink = $this->message['diff'];
+               } else if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) {
                        $diffLink = $this->message['diff'];
                } else {
                        $rcidparam = $unpatrolled
@@ -170,7 +197,12 @@ class ChangesList {
                $params = ( $unpatrolled && $rc->mAttribs['rc_type'] == RC_NEW )
                        ? 'rcid='.$rc->mAttribs['rc_id']
                        : '';
-               $articlelink = ' '. $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params );
+               if( $this->isDeleted($rc,Revision::DELETED_TEXT) ) {
+                       $articlelink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params );
+                       $articlelink = '<span class="history-deleted">'.$articlelink.'</span>';
+               } else {
+                   $articlelink = ' '. $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params );
+               }
                if( $watched )
                        $articlelink = "<strong class=\"mw-watched\">{$articlelink}</strong>";
                global $wgContLang;
@@ -187,15 +219,38 @@ class ChangesList {
 
        /** Insert links to user page, user talk page and eventually a blocking link */
        function insertUserRelatedLinks(&$s, &$rc) {
-               $s .= $this->skin->userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
-               $s .= $this->skin->userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
+               if ( $this->isDeleted($rc,Revision::DELETED_USER) ) {
+                  $s .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-user') . '</span>';   
+               } else {
+                 $s .= $this->skin->userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
+                 $s .= $this->skin->userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
+               }
+       }
+
+       /** insert a formatted action */
+       function insertAction(&$s, &$rc) {
+               # Add comment
+               if( $rc->mAttribs['rc_type'] == RC_LOG ) {
+                       // log action
+                       if ( $this->isDeleted($rc,LogViewer::DELETED_ACTION) ) {
+                               $s .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
+                       } else {
+                               $s .= ' ' . LogPage::actionText( $rc->mAttribs['rc_log_type'], $rc->mAttribs['rc_log_action'], 
+                                       $rc->getTitle(), $this->skin, LogPage::extractParams($rc->mAttribs['rc_params']), true, true );
+                       }
+               }
        }
 
        /** insert a formatted comment */
        function insertComment(&$s, &$rc) {
                # Add comment
                if( $rc->mAttribs['rc_type'] != RC_MOVE && $rc->mAttribs['rc_type'] != RC_MOVE_OVER_REDIRECT ) {
-                       $s .= $this->skin->commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() );
+                       // log comment
+                       if ( $this->isDeleted($rc,Revision::DELETED_COMMENT) ) {
+                               $s .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-comment') . '</span>';
+                       } else {
+                               $s .= $this->skin->commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() );
+                       }
                }
        }
 
@@ -251,18 +306,22 @@ class OldChangesList extends ChangesList {
 
                $s .= '<li>';
 
-               // moved pages
+               // Moved pages
                if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
                        $this->insertMove( $s, $rc );
-               // log entries
-               } elseif ( $rc_namespace == NS_SPECIAL ) {
+               // Log entries
+               } elseif( $rc_log_type !='' ) {
+                       $logtitle = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL );
+                       $this->insertLog( $s, $logtitle, $rc_log_type );
+               // Log entries (old format) or log targets, and special pages
+               } elseif( $rc_namespace == NS_SPECIAL ) {
                        list( $specialName, $specialSubpage ) = SpecialPage::resolveAliasWithSubpage( $rc_title );
                        if ( $specialName == 'Log' ) {
                                $this->insertLog( $s, $rc->getTitle(), $specialSubpage );
                        } else {
                                wfDebug( "Unexpected special page in recentchanges\n" );
                        }
-               // all other stuff
+               // Log entries
                } else {
                        wfProfileIn($fname.'-page');
 
@@ -284,9 +343,15 @@ class OldChangesList extends ChangesList {
                }
 
                $this->insertUserRelatedLinks($s,$rc);
+               $this->insertAction($s, $rc);
                $this->insertComment($s, $rc);
-
-               $s .=  rtrim(' ' . $this->numberofWatchingusers($rc->numberofWatchingusers));
+               
+               # Mark revision as deleted
+               if ( !$rc_log_type && $this->isDeleted($rc,Revision::DELETED_TEXT) )
+                  $s .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
+               if($rc->numberofWatchingusers > 0) {
+                       $s .= ' ' . wfMsg('number_of_watching_users_RCview',  $wgContLang->formatNum($rc->numberofWatchingusers));
+               }
 
                $s .= "</li>\n";
 
@@ -334,12 +399,14 @@ class EnhancedChangesList extends ChangesList {
                        $rc->unpatrolled = false;
                }
 
+               $showrev=true;
                # Make article link
                if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
                        $msg = ( $rc_type == RC_MOVE ) ? "1movedto2" : "1movedto2_redir";
                        $clink = wfMsg( $msg, $this->skin->makeKnownLinkObj( $rc->getTitle(), '', 'redirect=no' ),
                          $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), '' ) );
-               } elseif( $rc_namespace == NS_SPECIAL ) {
+               } else if( $rc_namespace == NS_SPECIAL ) {
+               // Log entries (old format) and special pages
                        list( $specialName, $logtype ) = SpecialPage::resolveAliasWithSubpage( $rc_title );
                        if ( $specialName == 'Log' ) {
                                # Log updates, etc
@@ -349,7 +416,16 @@ class EnhancedChangesList extends ChangesList {
                                wfDebug( "Unexpected special page in recentchanges\n" );
                                $clink = '';
                        }
-               } elseif( $rc->unpatrolled && $rc_type == RC_NEW ) {
+               } elseif ( $rc_log_type !='' ) {
+               // Log entries
+                       $logtitle = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL );
+                       $logname = LogPage::logName( $rc_log_type );
+                       $clink = '(' . $this->skin->makeKnownLinkObj($logtitle, $logname ) . ')';
+               } if ( $this->isDeleted($rc,Revision::DELETED_TEXT) ) {
+                   $clink = '<span class="history-deleted">' . $this->skin->makeKnownLinkObj( $rc->getTitle(), '' ) . '</span>';
+                   if ( !ChangesList::userCan($rc,Revision::DELETED_TEXT) )
+                      $showrev=false;
+               } else if( $rc->unpatrolled && $rc_type == RC_NEW ) {
                        # Unpatrolled new page, give rc_id in query
                        $clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', "rcid={$rc_id}" );
                } else {
@@ -372,7 +448,10 @@ class EnhancedChangesList extends ChangesList {
                $querydiff = $curIdEq."&diff=$rc_this_oldid&oldid=$rc_last_oldid$rcIdQuery";
                $aprops = ' tabindex="'.$baseRC->counter.'"';
                $curLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['cur'], $querycur, '' ,'', $aprops );
-               if( $rc_type == RC_NEW || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
+               if ( !$showrev ) {
+                  $curLink = $this->message['cur'];
+                  $diffLink = $this->message['diff'];
+               } else if( $rc_type == RC_NEW || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
                        if( $rc_type != RC_NEW ) {
                                $curLink = $this->message['cur'];
                        }
@@ -382,21 +461,27 @@ class EnhancedChangesList extends ChangesList {
                }
 
                # Make "last" link
-               if( $rc_last_oldid == 0 || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
+               if ( !$showrev ) {
+                   $lastLink = $this->message['last'];
+               } else if( $rc_last_oldid == 0 || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
                        $lastLink = $this->message['last'];
                } else {
                        $lastLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['last'],
-                         $curIdEq.'&diff='.$rc_this_oldid.'&oldid='.$rc_last_oldid . $rcIdQuery );
+                       $curIdEq.'&diff='.$rc_this_oldid.'&oldid='.$rc_last_oldid . $rcIdQuery );
+               }
+               
+               # Make user links
+               if ( $this->isDeleted($rc,Revision::DELETED_USER) ) {
+                       $rc->userlink = ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-user') . '</span>';
+               } else {
+                       $rc->userlink = $this->skin->userLink( $rc_user, $rc_user_text );
+                       $rc->usertalklink = $this->skin->userToolLinks( $rc_user, $rc_user_text );
                }
-
-               $rc->userlink = $this->skin->userLink( $rc_user, $rc_user_text );
 
                $rc->lastlink = $lastLink;
                $rc->curlink  = $curLink;
                $rc->difflink = $diffLink;
 
-               $rc->usertalklink = $this->skin->userToolLinks( $rc_user, $rc_user_text );
-
                # Put accumulated information into the cache, for later display
                # Page moves go on their own line
                $title = $rc->getTitle();
@@ -418,10 +503,11 @@ class EnhancedChangesList extends ChangesList {
         */
        function recentChangesBlockGroup( $block ) {
                global $wgLang, $wgContLang, $wgRCShowChangedSize;
-               $r = '';
+               $r = '<table cellpadding="0" cellspacing="0"><tr>';
 
                # Collate list of users
                $isnew = false;
+               $namehidden = true;
                $unpatrolled = false;
                $userlinks = array();
                foreach( $block as $rcObj ) {
@@ -429,6 +515,11 @@ class EnhancedChangesList extends ChangesList {
                        if( $rcObj->mAttribs['rc_new'] ) {
                                $isnew = true;
                        }
+                       // if all log actions to this page were hidden, then don't
+                       // give the name of the affected page for this block
+                       if( !($rcObj->mAttribs['rc_deleted'] & LogViewer::DELETED_ACTION) ) {
+                               $namehidden = false;
+                       }
                        $u = $rcObj->userlink;
                        if( !isset( $userlinks[$u] ) ) {
                                $userlinks[$u] = 0;
@@ -462,24 +553,25 @@ class EnhancedChangesList extends ChangesList {
                $toggleLink = "javascript:toggleVisibility('$rci','$rcm','$rcl')";
                $tl  = '<span id="'.$rcm.'"><a href="'.$toggleLink.'">' . $this->sideArrow() . '</a></span>';
                $tl .= '<span id="'.$rcl.'" style="display:none"><a href="'.$toggleLink.'">' . $this->downArrow() . '</a></span>';
-               $r .= $tl;
+               $r .= '<td valign="top">'.$tl;
 
                # Main line
-               $r .= '<tt>';
-               $r .= $this->recentChangesFlags( $isnew, false, $unpatrolled, '&nbsp;', $bot );
+               $r .= ' '.$this->recentChangesFlags( $isnew, false, $unpatrolled, '&nbsp;', $bot );
 
                # Timestamp
-               $r .= ' '.$block[0]->timestamp.' </tt>';
+               $r .= '&nbsp;'.$block[0]->timestamp.'&nbsp;&nbsp;</td><td>';
 
                # Article link
-               $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched );
+               if ( $namehidden )
+                       $r .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
+               else
+                       $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched );
                $r .= $wgContLang->getDirMark();
 
                $curIdEq = 'curid=' . $block[0]->mAttribs['rc_cur_id'];
                $currentRevision = $block[0]->mAttribs['rc_this_oldid'];
                if( $block[0]->mAttribs['rc_type'] != RC_LOG ) {
                        # Changes
-
                        $n = count($block);
                        static $nchanges = array();
                        if ( !isset( $nchanges[$n] ) ) {
@@ -489,25 +581,25 @@ class EnhancedChangesList extends ChangesList {
 
                        $r .= ' (';
 
-                       if( $isnew ) {
+                       if( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) {
+                           $r .= $nchanges[$n];
+                       } else if( $isnew ) {
                                $r .= $nchanges[$n];
                        } else {
                                $r .= $this->skin->makeKnownLinkObj( $block[0]->getTitle(),
                                        $nchanges[$n], $curIdEq."&diff=$currentRevision&oldid=$oldid" );
                        }
 
-                       $r .= ') . . ';
-
                        if( $wgRCShowChangedSize ) {
                                # Character difference
                                $chardiff = $rcObj->getCharacterDifference( $block[ count( $block ) - 1 ]->mAttribs['rc_old_len'],
                                                $block[0]->mAttribs['rc_new_len'] );
                                if( $chardiff == '' ) {
-                                       $r .= ' (';
+                                       $r .= '';
                                } else {
                                        $r .= ' ' . $chardiff. ' . . ';
                                }
-                       }       
+                       }
 
                        # History
                        $r .= '(' . $this->skin->makeKnownLinkObj( $block[0]->getTitle(),
@@ -516,51 +608,70 @@ class EnhancedChangesList extends ChangesList {
                }
 
                $r .= $users;
-
-               $r .= $this->numberofWatchingusers($block[0]->numberofWatchingusers);
-               $r .= "<br />\n";
+               $r .=$this->numberofWatchingusers($block[0]->numberofWatchingusers);
+               
+               $r .= "</td></tr></table>\n";
 
                # Sub-entries
-               $r .= '<div id="'.$rci.'" style="display:none">';
+               $r .= '<div id="'.$rci.'" style="display:none; font-size:95%;"><table cellpadding="0" cellspacing="0">';
                foreach( $block as $rcObj ) {
                        # Get rc_xxxx variables
                        // FIXME: Would be good to replace this extract() call with something that explicitly initializes local variables.
                        extract( $rcObj->mAttribs );
 
-                       $r .= $this->spacerArrow();
-                       $r .= '<tt>&nbsp; &nbsp; &nbsp; &nbsp;';
+                       #$r .= '<tr><td valign="top">'.$this->spacerArrow();
+                       $r .= '<tr><td valign="top">'.$this->spacerIndent();
+                       $r .= '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
                        $r .= $this->recentChangesFlags( $rc_new, $rc_minor, $rcObj->unpatrolled, '&nbsp;', $rc_bot );
-                       $r .= '&nbsp;</tt>';
+                       $r .= '&nbsp;&nbsp;</td><td valign="top">';
 
                        $o = '';
                        if( $rc_this_oldid != 0 ) {
                                $o = 'oldid='.$rc_this_oldid;
                        }
+                       # Revision link
                        if( $rc_type == RC_LOG ) {
-                               $link = $rcObj->timestamp;
+                               $link = $rcObj->timestamp.' ';
+                       } else if( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) {
+                               $link = '<span class="history-deleted">'.$rcObj->timestamp.'</span> ';
                        } else {
                                $link = $this->skin->makeKnownLinkObj( $rcObj->getTitle(), $rcObj->timestamp, $curIdEq.'&'.$o );
+                               if( $this->isDeleted($rcObj,Revision::DELETED_TEXT) )
+                                       $link = '<span class="history-deleted">'.$link.'</span> ';
                        }
-                       $link = '<tt>'.$link.'</tt>';
-
                        $r .= $link;
-                       $r .= ' (';
-                       $r .= $rcObj->curlink;
-                       $r .= '; ';
-                       $r .= $rcObj->lastlink;
-                       $r .= ') . . ';
+                       
+                       if ( !$rc_log_type ) {
+                               $r .= ' (';
+                               $r .= $rcObj->curlink;
+                               $r .= '; ';
+                               $r .= $rcObj->lastlink;
+                               $r .= ')';
+                       } else {
+                               $logname = LogPage::logName( $rc_log_type );
+                               $logtitle = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL );
+                               $r .= '(' . $this->skin->makeKnownLinkObj($logtitle, $logname ) . ')';
+                       }
+                       $r .= ' . . ';
 
                        # Character diff
                        if( $wgRCShowChangedSize ) {
                                $r .= ( $rcObj->getCharacterDifference() == '' ? '' : $rcObj->getCharacterDifference() . ' . . ' ) ;
                        }
-
+                       # User links
                        $r .= $rcObj->userlink;
                        $r .= $rcObj->usertalklink;
-                       $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() );
-                       $r .= "<br />\n";
+                       // log action
+                       parent::insertAction($r, $rcObj);
+                       // log comment
+                       parent::insertComment($r, $rcObj);
+                       # Mark revision as deleted
+                       if ( !$rc_log_type && $this->isDeleted($rcObj,Revision::DELETED_TEXT) )
+                               $s .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
+
+                       $r .= "</td></tr>\n";
                }
-               $r .= "</div>\n";
+               $r .= "</table></div>\n";
 
                $this->rcCacheIndex++;
                return $r;
@@ -617,8 +728,23 @@ class EnhancedChangesList extends ChangesList {
         * @access private
         */
        function spacerArrow() {
+       //FIXME: problems with FF 1.5x
                return $this->arrow( '', ' ' );
        }
+       
+       /**
+        * Generate HTML for the equivilant of a spacer image for tables
+        * @return string HTML <td> tag
+        * @access private
+        */     
+       function spacerColumn() {
+               return '<td width="12"></td>';
+       }       
+       
+       // Adds a few spaces
+       function spacerIndent() {
+               return '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
+       }       
 
        /**
         * Enhanced RC ungrouped line.
@@ -632,46 +758,64 @@ class EnhancedChangesList extends ChangesList {
                extract( $rcObj->mAttribs );
                $curIdEq = 'curid='.$rc_cur_id;
 
-               $r = '';
-
-               # Spacer image
-               $r .= $this->spacerArrow();
+               $r = '<table cellspacing="0" cellpadding="0"><tr><td>';
 
+               # spacerArrow() causes issues in FF
+               $r .= $this->spacerColumn();
+               $r .= '<td valign="top">';
+               
                # Flag and Timestamp
-               $r .= '<tt>';
-
                if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
-                       $r .= '&nbsp;&nbsp;&nbsp;';
+                       $r .= '&nbsp;&nbsp;&nbsp;&nbsp;';
                } else {
-                       $r .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $rcObj->unpatrolled, '&nbsp;', $rc_bot );
+                       $r .= '&nbsp;'.$this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $rcObj->unpatrolled, '&nbsp;', $rc_bot );
                }
-               $r .= ' '.$rcObj->timestamp.' </tt>';
-
+               $r .= '&nbsp;'.$rcObj->timestamp.'&nbsp;&nbsp;</td><td>';
+               
                # Article link
-               $r .= $this->maybeWatchedLink( $rcObj->link, $rcObj->watched );
-
-               # Diff
-               $r .= ' ('. $rcObj->difflink .'; ';
-
-               # Hist
-               $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), $curIdEq.'&action=history' ) . ') . . ';
-
+               if ( $rc_log_type !='' ) {
+                       $logtitle = Title::newFromText( "Log/$rc_log_type", NS_SPECIAL );
+                       $logname = LogPage::logName( $rc_log_type );
+                       $r .= '(' . $this->skin->makeKnownLinkObj($logtitle, $logname ) . ')';
+               // All other stuff
+               } else {
+                       $r .= $this->maybeWatchedLink( $rcObj->link, $rcObj->watched );
+               }
+               if ( $rc_type != RC_LOG ) {
+                  # Diff
+                  $r .= ' ('. $rcObj->difflink .'; ';
+                  # Hist
+                  $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), $curIdEq.'&action=history' ) . ')';
+               }
+               $r .= ' . . ';
+               
                # Character diff
                if( $wgRCShowChangedSize ) {
                        $r .= ( $rcObj->getCharacterDifference() == '' ? '' : '&nbsp;' . $rcObj->getCharacterDifference() . ' . . ' ) ;
                }
 
                # User/talk
-               $r .= $rcObj->userlink . $rcObj->usertalklink;
+               $r .= ' '.$rcObj->userlink . $rcObj->usertalklink;
 
                # Comment
                if( $rc_type != RC_MOVE && $rc_type != RC_MOVE_OVER_REDIRECT ) {
-                       $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() );
+                       // log action
+                       if ( $this->isDeleted($rcObj,LogViewer::DELETED_ACTION) ) {
+                          $r .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
+                       } else {
+                               $r .= ' ' . LogPage::actionText( $rc_log_type, $rc_log_action, $rcObj->getTitle(), $this->skin, LogPage::extractParams($rc_params), true, true );
+                       } 
+                       // log comment
+                       if ( $this->isDeleted($rcObj,LogViewer::DELETED_COMMENT) ) {
+                          $r .= ' <span class="history-deleted">' . wfMsg('rev-deleted-comment') . '</span>';
+                       } else {
+                         $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() );
+                       }
                }
 
                $r .= $this->numberofWatchingusers($rcObj->numberofWatchingusers);
 
-               $r .= "<br />\n";
+               $r .= "</td></tr></table>\n";
                return $r;
        }
 
index 17c31ae..2b93afb 100644 (file)
@@ -170,10 +170,16 @@ $wgUploadBaseUrl    = "";
  *   $wgFileStore['deleted']['directory'] = '/var/wiki/private/deleted';
  *
  */
+// For deleted images, gererally were all versions of the image are discarded
 $wgFileStore = array();
 $wgFileStore['deleted']['directory'] = false;// Defaults to $wgUploadDirectory/deleted
 $wgFileStore['deleted']['url'] = null;       // Private
 $wgFileStore['deleted']['hash'] = 3;         // 3-level subdirectory split
+// For revisions of images marked as hidden
+// These are kept even if $wgSaveDeletedFiles is set to false
+$wgFileStore['hidden']['directory'] = false;// Defaults to $wgUploadDirectory/hidden
+$wgFileStore['hidden']['url'] = null;       // Private
+$wgFileStore['hidden']['hash'] = 3;         // 3-level subdirectory split
 
 /**#@+
  * File repository structures
@@ -1060,6 +1066,7 @@ $wgGroupPermissions['bot'  ]['autopatrol']      = true;
 $wgGroupPermissions['sysop']['block']           = true;
 $wgGroupPermissions['sysop']['createaccount']   = true;
 $wgGroupPermissions['sysop']['delete']          = true;
+$wgGroupPermissions['sysop']['browsearchive']  = true; // can see the deleted page list
 $wgGroupPermissions['sysop']['deletedhistory']         = true; // can view deleted history entries, but not see or restore the text
 $wgGroupPermissions['sysop']['editinterface']   = true;
 $wgGroupPermissions['sysop']['editusercssjs']   = true;
@@ -1079,14 +1086,21 @@ $wgGroupPermissions['sysop']['unwatchedpages']  = true;
 $wgGroupPermissions['sysop']['autoconfirmed']   = true;
 $wgGroupPermissions['sysop']['upload_by_url']   = true;
 $wgGroupPermissions['sysop']['ipblock-exempt'] = true;
+$wgGroupPermissions['sysop']['deleterevision']  = true;
 $wgGroupPermissions['sysop']['blockemail']      = true;
+$wgGroupPermissions['sysop']['mergehistory']    = true;
 
 // Permission to change users' group assignments
 $wgGroupPermissions['bureaucrat']['userrights'] = true;
 
-// Experimental permissions, not ready for production use
-//$wgGroupPermissions['sysop']['deleterevision'] = true;
-//$wgGroupPermissions['bureaucrat']['hiderevision'] = true;
+// To hide usernames
+$wgGroupPermissions['oversight']['hideuser'] = true;
+// To see hidden revs and unhide revs hidden from Sysops
+$wgGroupPermissions['oversight']['hiderevision'] = true;
+// For private log access
+$wgGroupPermissions['oversight']['oversight'] = true;
+
+$wgAllowLogDeletion = false;
 
 /**
  * The developer group is deprecated, but can be activated if need be
@@ -2243,6 +2257,18 @@ $wgLogTypes = array( '',
        'move',
        'import',
        'patrol',
+       'merge',
+       'oversight',
+);
+
+/**
+ * This restricts log access to those who have a certain right
+ * Users without this will not see it in the option menu and can not view it
+ * Restricted logs are not added to recent changes
+ * Logs should remain non-transcludable
+ */
+$wgLogRestrictions = array(
+       'oversight' => 'oversight'
 );
 
 /**
@@ -2261,6 +2287,8 @@ $wgLogNames = array(
        'move'    => 'movelogpage',
        'import'  => 'importlogpage',
        'patrol'  => 'patrol-log-page',
+       'merge'   => 'mergelog',
+       'oversight' => 'oversightlog',
 );
 
 /**
@@ -2279,6 +2307,8 @@ $wgLogHeaders = array(
        'move'    => 'movelogpagetext',
        'import'  => 'importlogpagetext',
        'patrol'  => 'patrol-log-header',
+       'merge'   => 'mergelogpagetext',
+       'oversight' => 'overlogpagetext',
 );
 
 /**
@@ -2288,22 +2318,29 @@ $wgLogHeaders = array(
  * Extensions with custom log types may add to this array.
  */
 $wgLogActions = array(
-       'block/block'       => 'blocklogentry',
-       'block/unblock'     => 'unblocklogentry',
-       'protect/protect'   => 'protectedarticle',
-       'protect/modify'    => 'modifiedarticleprotection',
-       'protect/unprotect' => 'unprotectedarticle',
-       'rights/rights'     => 'rightslogentry',
-       'delete/delete'     => 'deletedarticle',
-       'delete/restore'    => 'undeletedarticle',
-       'delete/revision'   => 'revdelete-logentry',
-       'upload/upload'     => 'uploadedimage',
-       'upload/overwrite'      => 'overwroteimage',
-       'upload/revert'     => 'uploadedimage',
-       'move/move'         => '1movedto2',
-       'move/move_redir'   => '1movedto2_redir',
-       'import/upload'     => 'import-logentry-upload',
-       'import/interwiki'  => 'import-logentry-interwiki',
+       'block/block'        => 'blocklogentry',
+       'block/unblock'      => 'unblocklogentry',
+       'protect/protect'    => 'protectedarticle',
+       'protect/modify'     => 'modifiedarticleprotection',
+       'protect/unprotect'  => 'unprotectedarticle',
+       'rights/rights'      => 'rightslogentry',
+       'delete/delete'      => 'deletedarticle',
+       'delete/restore'     => 'undeletedarticle',
+       'delete/revision'    => 'revdelete-logentry',
+       'delete/event'           => 'logdelete-logentry',
+       'upload/upload'      => 'uploadedimage',
+       'upload/overwrite'       => 'overwroteimage',
+       'upload/revert'      => 'uploadedimage',
+       'move/move'          => '1movedto2',
+       'move/move_redir'    => '1movedto2_redir',
+       'import/upload'      => 'import-logentry-upload',
+       'import/interwiki'   => 'import-logentry-interwiki',
+       'merge/merge'        => 'pagemerge-logentry',
+       'oversight/revision' => 'revdelete-logentry',
+       'oversight/file'     => 'revdelete-logentry',
+       'oversight/event'    => 'logdelete-logentry',
+       'oversight/delete'   => 'suppressedarticle',
+       'oversight/block'        => 'blocklogentry',
 );
 
 /**
index e0647ee..8e186c4 100644 (file)
@@ -48,7 +48,7 @@ class DifferenceEngine {
                        # Show diff between revision $old and the previous one.
                        # Get previous one from DB.
                        #
-                       $this->mNewid = intval($old);
+                       $this->mNewid = intval($old); 
 
                        $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid );
 
@@ -64,6 +64,13 @@ class DifferenceEngine {
                                $this->mNewid = 0;
                        }
 
+               } else if( 'cur' === $new ) {
+                       # Show diff between revision $old and the current one.
+                       # Get previous one from DB.
+                       #
+                       $this->mNewid = $this->mTitle->getLatestRevID();
+
+                       $this->mOldid = intval($old);
                } else {
                        $this->mOldid = intval($old);
                        $this->mNewid = intval($new);
@@ -220,14 +227,49 @@ CONTROL;
                        $newminor = wfElement( 'span', array( 'class' => 'minor' ),
                        wfMsg( 'minoreditletter') ) . ' ';
                }
+               
+               $rdel = ''; $ldel = '';
+               if( $wgUser->isAllowed( 'deleterevision' ) ) {
+                       $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+                       if( !$this->mOldRev->userCan( Revision::DELETED_RESTRICTED ) ) {
+                       // If revision was hidden from sysops
+                               $ldel = wfMsgHtml('rev-delundel');      
+                       } else {
+                               $ldel = $sk->makeKnownLinkObj( $revdel,
+                                       wfMsgHtml('rev-delundel'),
+                                       'target=' . urlencode( $this->mOldRev->mTitle->getPrefixedDbkey() ) .
+                                       '&oldid=' . urlencode( $this->mOldRev->getId() ) );
+                               // Bolden oversighted content
+                               if( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) )
+                                       $ldel = "<strong>$ldel</strong>";
+                       }
+                       $ldel = "&nbsp;&nbsp;&nbsp;<tt>(<small>$ldel</small>)</tt> ";
+                       // We don't currently handle well changing the top revision's settings
+                       if( $this->mNewRev->isCurrent() ) {
+                       // If revision was hidden from sysops
+                               $rdel = wfMsgHtml('rev-delundel');      
+                       } else if( !$this->mNewRev->userCan( Revision::DELETED_RESTRICTED ) ) {
+                       // If revision was hidden from sysops
+                               $rdel = wfMsgHtml('rev-delundel');      
+                       } else {
+                               $rdel = $sk->makeKnownLinkObj( $revdel,
+                                       wfMsgHtml('rev-delundel'),
+                                       'target=' . urlencode( $this->mNewRev->mTitle->getPrefixedDbkey() ) .
+                                       '&oldid=' . urlencode( $this->mNewRev->getId() ) );
+                               // Bolden oversighted content
+                               if( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) )
+                                       $rdel = "<strong>$rdel</strong>";
+                       }
+                       $rdel = "&nbsp;&nbsp;&nbsp;<tt>(<small>$rdel</small>)</tt> ";
+               }
 
-               $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $this->mOldtitle . '</strong></div>' .
-                       '<div id="mw-diff-otitle2">' . $sk->revUserTools( $this->mOldRev ) . "</div>" .
-                       '<div id="mw-diff-otitle3">' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly ) . "</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 ) . " $rollback</div>" .
-                       '<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly ) . "</div>" .
+               $oldHeader = '<div id="mw-diff-otitle1"><strong>'.$this->mOldtitle.'</strong></div>' .
+                       '<div id="mw-diff-otitle2">' . $sk->revUserTools( $this->mOldRev, true ) . "</div>" .
+                       '<div id="mw-diff-otitle3">' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly, true ) . $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, true ) . " $rollback</div>" .
+                       '<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly, true ) . $rdel . "</div>" .
                        '<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>';
 
                $this->showDiff( $oldHeader, $newHeader );
@@ -248,8 +290,10 @@ CONTROL;
 
                $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
                #add deleted rev tag if needed
-               if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
+               if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
                        $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) );
+               } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
+                       $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) );
                }
 
                if( !$this->mNewRev->isCurrent() ) {
@@ -394,20 +438,25 @@ CONTROL;
                        } // don't try to load but save the result
                }
 
-               #loadtext is permission safe, this just clears out the diff
+               // Loadtext is permission safe, this just clears out the diff
                if ( !$this->loadText() ) {
                        wfProfileOut( $fname );
                        return false;
                } else if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
-                 return '';
+                       return '';
                } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
-                 return '';
+                       return '';
                }
 
                $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
                
                // Save to cache for 7 days
-               if ( $key !== false && $difftext !== false ) {
+               // Only do this for public revs, otherwise an admin can view the diff and a non-admin can nab it!
+               if ( $this->mOldRev && $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) {
+                       wfIncrStats( 'diff_uncacheable' );
+               } else if ( $this->mNewRev && $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
+                       wfIncrStats( 'diff_uncacheable' );
+               } else if ( $key !== false && $difftext !== false ) {
                        wfIncrStats( 'diff_cache_miss' );
                        $wgMemc->set( $key, $difftext, 7*86400 );
                } else {
@@ -539,15 +588,9 @@ CONTROL;
        /**
         * Add the header to a diff body
         */
-       function addHeader( $diff, $otitle, $ntitle, $multi = '' ) {
+       static function addHeader( $diff, $otitle, $ntitle, $multi = '' ) {
                global $wgOut;
-       
-               if ( $this->mOldRev && $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) {
-                  $otitle = '<span class="history-deleted">'.$otitle.'</span>';
-               }
-               if ( $this->mNewRev && $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
-                  $ntitle = '<span class="history-deleted">'.$ntitle.'</span>';
-               }
+
                $header = "
                        <table class='diff'>
                        <col class='diff-marker' />
@@ -600,10 +643,10 @@ CONTROL;
                        : Revision::newFromTitle( $this->mTitle );
                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();
-               
+
                // Set assorted variables
                $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true );
                $this->mNewPage = $this->mNewRev->getTitle();
@@ -623,6 +666,11 @@ CONTROL;
                        $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>"
                                . " (<a href='$newEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</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;
@@ -650,12 +698,20 @@ CONTROL;
                        $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true );
                        $oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid );
                        $oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid );
-                       $this->mOldtitle = "<a href='$oldLink'>" . htmlspecialchars( wfMsg( 'revisionasof', $t ) )
-                               . "</a> (<a href='$oldEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)";
+                       $this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t ) );
                        
+                       $this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>"
+                               . "(<a href='$oldEdit'>" . htmlspecialchars( wfMsg( 'editold' ) ) . "</a>)";
                        // Add an "undo" link
                        $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid);
-                       $this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)";
+                       if ( $this->mNewRev->userCan(Revision::DELETED_TEXT) )
+                               $this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</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;
@@ -676,7 +732,6 @@ CONTROL;
                        return false;
                }
                if ( $this->mOldRev ) {
-                       // FIXME: permission tests
                        $this->mOldtext = $this->mOldRev->revText();
                        if ( $this->mOldtext === false ) {
                                return false;
index 3a19c45..7952f4f 100644 (file)
@@ -982,9 +982,13 @@ class EditPage {
                        }
                        if ( isset( $this->mArticle ) && isset( $this->mArticle->mRevision ) ) {
                        // Let sysop know that this will make private content public if saved
-                               if( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) {
+                               
+                               if( !$this->mArticle->mRevision->userCan( Revision::DELETED_TEXT ) ) {
+                                       $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) );
+                               } else if( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) {
                                        $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) );
                                }
+                               
                                if( !$this->mArticle->mRevision->isCurrent() ) {
                                        $this->mArticle->setOldSubtitle( $this->mArticle->mRevision->getId() );
                                        $wgOut->addWikiText( wfMsg( 'editingold' ) );
index 813d72b..060ced1 100644 (file)
@@ -46,8 +46,14 @@ class FileDeleteForm {
                        return;
                }
                
-               $this->oldimage = $wgRequest->getText( 'oldimage', false );
+               # Use revision delete
+               # $this->oldimage = $wgRequest->getText( 'oldimage', false );
+               
+               $this->oldimage = false;
                $token = $wgRequest->getText( 'wpEditToken' );
+               # Flag to hide all contents of the archived revisions
+               $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed('deleterevision');
+               
                if( $this->oldimage && !$this->isValidOldSpec() ) {
                        $wgOut->showUnexpectedValueError( 'oldimage', htmlspecialchars( $this->oldimage ) );
                        return;
@@ -65,7 +71,7 @@ class FileDeleteForm {
                if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $token, $this->oldimage ) ) {
                        $comment = $wgRequest->getText( 'wpReason' );
                        if( $this->oldimage ) {
-                               $status = $this->file->deleteOld( $this->oldimage, $comment );
+                               $status = $this->file->deleteOld( $this->oldimage, $comment, $suppress );
                                if( $status->ok ) {
                                        // Need to do a log item
                                        $log = new LogPage( 'delete' );
@@ -75,7 +81,7 @@ class FileDeleteForm {
                                        $log->addEntry( 'delete', $this->title, $logComment );
                                }
                        } else {
-                               $status = $this->file->delete( $comment );
+                               $status = $this->file->delete( $comment, $suppress );
                                if( $status->ok ) {
                                        // Need to delete the associated article
                                        $article = new Article( $this->title );
index 3cf6d0a..531c59e 100644 (file)
@@ -416,22 +416,23 @@ EOT
 
                if ( $line ) {
                        $list = new ImageHistoryList( $sk, $this->img );
+                       // Our top image
                        $file = $this->repo->newFileFromRow( $line );
                        $dims = $file->getDimensionsString();
                        $s = $list->beginImageHistoryList() .
                                $list->imageHistoryLine( true, wfTimestamp(TS_MW, $line->img_timestamp),
-                                       $this->mTitle->getDBkey(),  $line->img_user,
-                                       $line->img_user_text, $line->img_size, $line->img_description,
-                                       $dims
+                                       $this->mTitle->getDBkey(), $line->img_user,
+                                       $line->img_user_text, $line->img_size, $line->img_description, $dims, 
+                                       $line->oi_deleted, $line->img_sha1
                                );
-
+                       // old image versions
                        while ( $line = $this->img->nextHistoryLine() ) {
                                $file = $this->repo->newFileFromRow( $line );
                                $dims = $file->getDimensionsString();
                                $s .= $list->imageHistoryLine( false, $line->oi_timestamp,
                                        $line->oi_archive_name, $line->oi_user,
                                        $line->oi_user_text, $line->oi_size, $line->oi_description,
-                                       $dims
+                                       $dims, $line->oi_deleted, $line->oi_sha1
                                );
                        }
                        $s .= $list->endImageHistoryList();
@@ -550,7 +551,7 @@ class ImageHistoryList {
                        . $wgOut->parse( wfMsgNoTrans( 'filehist-help' ) )
                        . Xml::openElement( 'table', array( 'class' => 'filehistory' ) ) . "\n"
                        . '<tr><td></td>'
-                       . ( $this->img->isLocal() && $wgUser->isAllowed( 'delete' ) ? '<td></td>' : '' )
+                       . ( $this->img->isLocal() && $wgUser->isAllowed( 'deleterevision' ) ? '<td></td>' : '' )
                        . '<th>' . wfMsgHtml( 'filehist-datetime' ) . '</th>'
                        . '<th>' . wfMsgHtml( 'filehist-user' ) . '</th>'
                        . '<th>' . wfMsgHtml( 'filehist-dimensions' ) . '</th>'
@@ -563,30 +564,48 @@ class ImageHistoryList {
                return "</table>\n";
        }
 
-       public function imageHistoryLine( $iscur, $timestamp, $img, $user, $usertext, $size, $description, $dims ) {
-               global $wgUser, $wgLang, $wgContLang;
+       public function imageHistoryLine( $iscur, $timestamp, $img, $user, $usertext, $size, $description, $dims,
+               $deleted, $sha1 ) {
+               global $wgUser, $wgLang, $wgContLang, $wgTitle;
                $local = $this->img->isLocal();
-               $row = '';
+               $row = '<td>';
 
                // Deletion link
-               if( $local && $wgUser->isAllowed( 'delete' ) ) {
-                       $row .= '<td>';
+               if( $iscur && $local && $wgUser->isAllowed( 'delete' ) ) {
                        $q = array();
                        $q[] = 'action=delete';
-                       if( !$iscur )
-                               $q[] = 'oldimage=' . urlencode( $img );
+                       $q[] = 'image=' . $this->title->getPartialUrl();
                        $row .= '(' . $this->skin->makeKnownLinkObj(
                                $this->title,
                                wfMsgHtml( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' ),
                                implode( '&', $q )
                        ) . ')';
-                       $row .= '</td>';
+                       $row .= '</td><td>';
                }
 
+               if( !$iscur && $local && $wgUser->isAllowed( 'deleterevision' ) ) {
+                       $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+                       if( !$this->userCan($deleted,Image::DELETED_RESTRICTED) ) {
+                               // If file was hidden from sysops
+                               $del = wfMsgHtml( 'rev-delundel' );                     
+                       } else {
+                               // If the file was hidden, link to sha-1
+                               list($ts,$name) = explode('!',$img,2);
+                               $del = $this->skin->makeKnownLinkObj( $revdel,  wfMsg( 'rev-delundel' ),
+                                       'target=' . urlencode( $wgTitle->getPrefixedText() ) .
+                                       '&oldimage=' . urlencode( $ts ) );
+                               // Bolden oversighted content
+                               if( $this->isDeleted($deleted,Image::DELETED_RESTRICTED) )
+                                       $del = "<strong>$del</strong>";
+                       }
+                       $row .= "<tt>(<small>$del</small>)</tt></td><td> ";
+               }
+               
                // Reversion link/current indicator
-               $row .= '<td>';
                if( $iscur ) {
-                       $row .= '(' . wfMsgHtml( 'filehist-current' ) . ')';
+                       $row .= ' (' . wfMsgHtml( 'filehist-current' ) . ')';
+               } elseif( $this->isDeleted($deleted,Image::DELETED_FILE) ) {
+                       $row .= '(' . wfMsgHtml('filehist-revert') . ')';
                } elseif( $local && $wgUser->isLoggedIn() && $this->title->userCan( 'edit' ) ) {
                        $q = array();
                        $q[] = 'action=revert';
@@ -602,18 +621,32 @@ class ImageHistoryList {
 
                // Date/time and image link
                $row .= '<td>';
-               $url = $iscur ? $this->img->getUrl() : $this->img->getArchiveUrl( $img );
-               $row .= Xml::element(
-                       'a',
-                       array( 'href' => $url ),
-                       $wgLang->timeAndDate( $timestamp, true )
-               );
+               if( !$this->userCan($deleted,Image::DELETED_FILE) ) {
+                       # Don't link to unviewable files
+                       $row .= '<span class="history-deleted">' . $wgLang->timeAndDate( $timestamp, true ) . '</span>';
+               } else if( $this->isDeleted($deleted,Image::DELETED_FILE) ) {
+                       $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+                       # Make a link to review the image
+                       $url = $this->skin->makeKnownLinkObj( $revdel, $wgLang->timeAndDate( $timestamp, true ), 
+                               "target=".$wgTitle->getPrefixedText()."&file=$sha1.".$this->img->getExtension() );
+                       $row .= '<span class="history-deleted">'.$url.'</span>';
+               } else {
+                       $url = $iscur ? $this->img->getUrl() : $this->img->getArchiveUrl( $img );
+                       $row .= Xml::element( 'a',
+                               array( 'href' => $url ),
+                               $wgLang->timeAndDate( $timestamp, true ) );
+               }
+
                $row .= '</td>';
 
                // Uploading user
                $row .= '<td>';
                if( $local ) {
-                       $row .= $this->skin->userLink( $user, $usertext ) . $this->skin->userToolLinks( $user, $usertext );
+                       // Hide deleted usernames
+                       if( $this->isDeleted($deleted,Image::DELETED_USER) )
+                               $row .= '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
+                       else
+                               $row .= $this->skin->userLink( $user, $usertext ) . $this->skin->userToolLinks( $user, $usertext );
                } else {
                        $row .= htmlspecialchars( $usertext );
                }
@@ -625,10 +658,45 @@ class ImageHistoryList {
                // File size
                $row .= '<td class="mw-imagepage-filesize">' . $this->skin->formatSize( $size ) . '</td>';
 
-               // Comment
-               $row .= '<td>' . $this->skin->formatComment( $description, $this->title ) . '</td>';
+               // Don't show deleted descriptions
+               if ( $this->isDeleted($deleted,Image::DELETED_COMMENT) )
+                       $row .= '<td><span class="history-deleted">' . wfMsgHtml('rev-deleted-comment') . '</span></td>';
+               else
+                       $row .= '<td>' . $this->skin->commentBlock( $description, $this->title ) . '</td>';
 
                return "<tr>{$row}</tr>\n";
        }
+       
+       /**
+        * int $field one of DELETED_* bitfield constants
+        * for file or revision rows
+        * @param int $bitfield
+        * @param int $field
+        * @return bool
+        */
+       function isDeleted( $bitfield, $field ) {
+               return ($bitfield & $field) == $field;
+       }
+       
+       /**
+        * Determine if the current user is allowed to view a particular
+        * field of this FileStore image file, if it's marked as deleted.
+        * @param int $bitfield
+        * @param int $field
+        * @return bool
+        */
+       function userCan( $bitfield, $field ) {
+               if( ($bitfield & $field) == $field ) {
+               // images
+                       global $wgUser;
+                       $permission = ( $bitfield & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED
+                               ? 'hiderevision'
+                               : 'deleterevision';
+                       wfDebug( "Checking for $permission due to $field match on $bitfield\n" );
+                       return $wgUser->isAllowed( $permission );
+               } else {
+                       return true;
+               }
+       }
 
 }
index 1d187b8..59a146b 100644 (file)
@@ -442,6 +442,7 @@ class Linker {
         * @param boolean $thumb shows image as thumbnail in a frame
         * @param string $manualthumb image name for the manual thumbnail
         * @param string $valign vertical alignment: baseline, sub, super, top, text-top, middle, bottom, text-bottom
+        * @param string $time, timestamp (for image versioning)
         * @return string
         */
        function makeImageLinkObj( $title, $label, $alt, $align = '', $handlerParams = array(), $framed = false,
@@ -871,10 +872,13 @@ class Linker {
        /**
         * Generate a user link if the current user is allowed to view it
         * @param $rev Revision object.
+        * @param $isPublic, bool, show only if all users can see it
         * @return string HTML
         */
-       function revUserLink( $rev ) {
-               if( $rev->userCan( Revision::DELETED_USER ) ) {
+       function revUserLink( $rev, $isPublic = false ) {
+               if( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
+                       $link = wfMsgHtml( 'rev-deleted-user' );
+               } else if( $rev->userCan( Revision::DELETED_USER ) ) {
                        $link = $this->userLink( $rev->getRawUser(), $rev->getRawUserText() );
                } else {
                        $link = wfMsgHtml( 'rev-deleted-user' );
@@ -884,21 +888,74 @@ class Linker {
                }
                return $link;
        }
+       
+       /**
+        * Generate a user link if the current user is allowed to view it
+        * @param $event, log row item.
+        * @param $isPublic, bool, show only if all users can see it
+        * @return string HTML
+        */
+       function logUserLink( $event, $isPublic = false ) {
+               if( LogViewer::isDeleted( $event, LogViewer::DELETED_USER ) && $isPublic ) {
+                       $link = wfMsgHtml( 'rev-deleted-user' );
+               } else if( LogViewer::userCan( $event, LogViewer::DELETED_USER ) ) {
+                       if ( isset($event->user_name) ) {
+                               $link = $this->userLink( $event->log_user, $event->user_name );
+                       } else {
+                               $user = $event->log_user;
+                               $link = $this->userLink( $event->log_user, User::whoIs( $user ) );
+                       }
+               } else {
+                       $link = wfMsgHtml( 'rev-deleted-user' );
+               }
+               if( LogViewer::isDeleted( $event, LogViewer::DELETED_USER ) ) {
+                       return '<span class="history-deleted">' . $link . '</span>';
+               }
+               return $link;
+       }
 
        /**
         * Generate a user tool link cluster if the current user is allowed to view it
         * @param $rev Revision object.
+        * @param $isPublic, bool, show only if all users can see it
         * @return string HTML
         */
-       function revUserTools( $rev ) {
-               if( $rev->userCan( Revision::DELETED_USER ) ) {
+       function revUserTools( $rev, $isPublic = false ) {
+               if( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
+                       $link = wfMsgHtml( 'rev-deleted-user' );
+               } else if( $rev->userCan( Revision::DELETED_USER ) ) {
                        $link = $this->userLink( $rev->getRawUser(), $rev->getRawUserText() ) .
-                               ' ' .
-                               $this->userToolLinks( $rev->getRawUser(), $rev->getRawUserText() );
+                       ' ' . $this->userToolLinks( $rev->getRawUser(), $rev->getRawUserText() );
                } else {
                        $link = wfMsgHtml( 'rev-deleted-user' );
                }
                if( $rev->isDeleted( Revision::DELETED_USER ) ) {
+                       return ' <span class="history-deleted">' . $link . '</span>';
+               }
+               return " $link";
+       }
+
+       /**
+        * Generate a user tool link cluster if the current user is allowed to view it
+        * @param $event, log item.
+        * @param $isPublic, bool, show only if all users can see it
+        * @return string HTML
+        */
+       function logUserTools( $event, $isPublic = false ) {
+               if( LogViewer::isDeleted( $event, LogViewer::DELETED_USER ) && $isPublic ) {
+                       $link = wfMsgHtml( 'rev-deleted-user' );
+               } else if( LogViewer::userCan( $event, LogViewer::DELETED_USER ) ) {
+                       if( isset($event->user_name) ) {
+                               $link = $this->userLink( $event->log_user, $event->user_name ) . 
+                                       $this->userToolLinksRedContribs( $event->log_user, $event->user_name );
+                       } else {
+                               $link = $this->userLink( $event->log_user, $event->user_name ) . 
+                                       $this->userToolLinksRedContribs( $event->log_user, User::whoIs($event->log_user) );
+                       }
+               } else {
+                       $link = wfMsgHtml( 'rev-deleted-user' );
+               }
+               if( LogViewer::isDeleted( $event, LogViewer::DELETED_USER ) ) {
                        return '<span class="history-deleted">' . $link . '</span>';
                }
                return $link;
@@ -1056,20 +1113,43 @@ class Linker {
         *
         * @param Revision $rev
         * @param bool $local Whether section links should refer to local page
+        * @param $isPublic, show only if all users can see it
         * @return string HTML
         */
-       function revComment( Revision $rev, $local = false ) {
-               if( $rev->userCan( Revision::DELETED_COMMENT ) ) {
+       function revComment( Revision $rev, $local = false, $isPublic = false ) {
+               if( $rev->isDeleted( Revision::DELETED_COMMENT ) && $isPublic ) {
+                       $block = " <span class=\"comment\">" . wfMsgHtml( 'rev-deleted-comment' ) . "</span>";
+               } else if( $rev->userCan( Revision::DELETED_COMMENT ) ) {
                        $block = $this->commentBlock( $rev->getRawComment(), $rev->getTitle(), $local );
                } else {
-                       $block = " <span class=\"comment\">" .
-                               wfMsgHtml( 'rev-deleted-comment' ) . "</span>";
+                       $block = " <span class=\"comment\">" . wfMsgHtml( 'rev-deleted-comment' ) . "</span>";
                }
                if( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
                        return " <span class=\"history-deleted\">$block</span>";
                }
                return $block;
        }
+               
+       /**
+        * Wrap and format the given event's comment block, if the current
+        * user is allowed to view it.
+        *
+        * @param Revision $rev
+        * @return string HTML
+        */
+       function logComment( $event, $isPublic = false ) {
+               if( LogViewer::isDeleted( $event, LogViewer::DELETED_COMMENT ) && $isPublic ) {
+                       $block = ' ' . wfMsgHtml( 'rev-deleted-comment' );
+               } else if( LogViewer::userCan( $event, LogViewer::DELETED_COMMENT ) ) {
+                       $block = $this->commentBlock( LogViewer::getRawComment( $event ) );
+               } else {
+                       $block = ' ' . wfMsgHtml( 'rev-deleted-comment' );
+               }
+               if( LogViewer::isDeleted( $event, LogViewer::DELETED_COMMENT ) ) {
+                       return "<span class=\"history-deleted\">$block</span>";
+               }
+               return $block;
+       }
 
        /** @todo document */
        function tocIndent() {
index e58f65f..cc9727f 100644 (file)
@@ -50,7 +50,7 @@ class LogPage {
        function saveContent() {
                if( wfReadOnly() ) return false;
 
-               global $wgUser;
+               global $wgUser, $wgLogRestrictions;
                $fname = 'LogPage::saveContent';
 
                $dbw = wfGetDB( DB_MASTER );
@@ -68,20 +68,18 @@ class LogPage {
                        'log_comment' => $this->comment,
                        'log_params' => $this->params
                );
-
-               # log_id doesn't exist on Wikimedia servers yet, and it's a tricky 
-               # schema update to do. Hack it for now to ignore the field on MySQL.
-               if ( !is_null( $log_id ) ) {
-                       $data['log_id'] = $log_id;
-               }
                $dbw->insert( 'logging', $data, $fname );
+               $newId = $dbw->insertId();
 
                # And update recentchanges
-               if ( $this->updateRecentChanges ) {
-                       $titleObj = SpecialPage::getTitleFor( 'Log', $this->type );
-                       $rcComment = $this->getRcComment();
-                       RecentChange::notifyLog( $now, $titleObj, $wgUser, $rcComment, '',
-                               $this->type, $this->action, $this->target, $this->comment, $this->params );
+               if( $this->updateRecentChanges ) {
+                       # Don't add private logs to RC!
+                       if( !isset($wgLogRestrictions[$this->type]) || $wgLogRestrictions[$this->type]=='*' ) {
+                               $titleObj = SpecialPage::getTitleFor( 'Log', $this->type );
+                               $rcComment = $this->getRcComment();
+                               RecentChange::notifyLog( $now, $titleObj, $wgUser, $rcComment, '',
+                                       $this->type, $this->action, $this->target, $this->comment, $this->params, $newId );
+                       }
                }
                return true;
        }
@@ -133,7 +131,7 @@ class LogPage {
         */
        static function logHeader( $type ) {
                global $wgLogHeaders;
-               return wfMsg( $wgLogHeaders[$type] );
+               return wfMsgHtml( $wgLogHeaders[$type] );
        }
 
        /**
@@ -173,6 +171,11 @@ class LogPage {
                                                        $text = $wgContLang->ucfirst( $title->getText() );
                                                        $titleLink = $skin->makeLinkObj( Title::makeTitle( NS_USER, $text ) );
                                                        break;
+                                               case 'merge':
+                                                       $titleLink = $skin->makeLinkObj( $title, $title->getPrefixedText(), 'redirect=no' );
+                                                       $params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), htmlspecialchars( $params[0] ) );
+                                                       $params[1] = $wgLang->timeanddate( $params[1] );
+                                                       break;
                                                default:
                                                        $titleLink = $skin->makeLinkObj( $title );
                                        }
@@ -199,7 +202,7 @@ class LogPage {
                                        }
                                } else {
                                        array_unshift( $params, $titleLink );
-                                       if ( $key == 'block/block' ) {
+                                       if ( $key == 'block/block' || $key == 'oversight/block' ) {
                                                if ( $skin ) {
                                                        $params[1] = '<span title="' . htmlspecialchars( $params[1] ). '">' . $wgLang->translateBlockExpiry( $params[1] ) . '</span>';
                                                } else {
index df711c8..932ab61 100644 (file)
@@ -37,6 +37,20 @@ class PageHistory {
                $this->mTitle =& $article->mTitle;
                $this->mNotificationTimestamp = NULL;
                $this->mSkin = $wgUser->getSkin();
+               $this->preCacheMessages();
+       }
+       
+       /**
+        * As we use the same small set of messages in various methods and that
+        * they are called often, we call them once and save them in $this->message
+        */
+       function preCacheMessages() {
+               // Precache various messages
+               if( !isset( $this->message ) ) {
+                       foreach( explode(' ', 'cur last rev-delundel' ) as $msg ) {
+                               $this->message[$msg] = wfMsgExt( $msg, array( 'escape') );
+                       }
+               }
        }
 
        /**
@@ -189,35 +203,31 @@ class PageHistory {
                $arbitrary = $this->diffButtons( $rev, $firstInList, $counter );
                $link = $this->revLink( $rev );
                
-               $user = $this->mSkin->userLink( $rev->getUser(), $rev->getUserText() )
-                               . $this->mSkin->userToolLinks( $rev->getUser(), $rev->getUserText() );
-               
                $s .= "($curlink) ($lastlink) $arbitrary";
                
                if( $wgUser->isAllowed( 'deleterevision' ) ) {
                        $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
                        if( $firstInList ) {
-                               // We don't currently handle well changing the top revision's settings
-                               $del = wfMsgHtml( 'rev-delundel' );
+                       // We don't currently handle well changing the top revision's settings
+                               $del = $this->message['rev-delundel'];
                        } else if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
                        // If revision was hidden from sysops
-                               $del = wfMsgHtml( 'rev-delundel' );                     
+                               $del = $this->message['rev-delundel'];  
                        } else {
                                $del = $this->mSkin->makeKnownLinkObj( $revdel,
-                                       wfMsg( 'rev-delundel' ),
+                                       $this->message['rev-delundel'],
                                        'target=' . urlencode( $this->mTitle->getPrefixedDbkey() ) .
                                        '&oldid=' . urlencode( $rev->getId() ) );
+                               // Bolden oversighted content
+                               if( $rev->isDeleted( Revision::DELETED_RESTRICTED ) )
+                                       $del = "<strong>$del</strong>";
                        }
-                       $s .= " (<small>$del</small>) ";
+                       $s .= " <tt>(<small>$del</small>)</tt> ";
                }
                
                $s .= " $link";
-               #getUser is safe, but this avoids making the invalid untargeted contribs links
-               if( $row->rev_deleted & Revision::DELETED_USER ) {
-                       $user = '<span class="history-deleted">' . wfMsg('rev-deleted-user') . '</span>';
-               }
-               $s .= " <span class='history-user'>$user</span>";
-
+               $s .= ' '.$this->mSkin->revUserTools( $rev, true);
+               
                if( $row->rev_minor_edit ) {
                        $s .= ' ' . wfElement( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') );
                }
@@ -243,7 +253,7 @@ class PageHistory {
                }
                #add blurb about text having been deleted
                if( $row->rev_deleted & Revision::DELETED_TEXT ) {
-                       $s .= ' ' . wfMsgHtml( 'deletedrev' );
+                       $s .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
                }
                
                $tools = array();
@@ -292,7 +302,7 @@ class PageHistory {
 
        /** @todo document */
        function curLink( $rev, $latest ) {
-               $cur = wfMsgExt( 'cur', array( 'escape') );
+               $cur = $this->message['cur'];
                if( $latest || !$rev->userCan( Revision::DELETED_TEXT ) ) {
                        return $cur;
                } else {
@@ -305,7 +315,7 @@ class PageHistory {
 
        /** @todo document */
        function lastLink( $rev, $next, $counter ) {
-               $last = wfMsgExt( 'last', array( 'escape' ) );
+               $last = $this->message['last'];
                if ( is_null( $next ) ) {
                        # Probably no next row
                        return $last;
index 79f32d0..e31cab2 100644 (file)
  *     rc_patrolled    boolean whether or not someone has marked this edit as patrolled
  *     rc_old_len      integer byte length of the text before the edit
  *     rc_new_len      the same after the edit
+ *     rc_deleted              partial deletion
+ *     rc_logid                the log_id value for this log entry (or zero)
+ *  rc_log_type                the log type (or null)
+ *     rc_log_action   the log action (or null)
+ *  rc_params          log params
  *
  * mExtra:
  *     prefixedDBkey   prefixed db key, used by external app via msg queue
@@ -295,7 +300,12 @@ class RecentChange
                        'rc_patrolled'  => 0,
                        'rc_new'        => 0,  # obsolete
                        'rc_old_len'    => $oldSize,
-                       'rc_new_len'    => $newSize
+                       'rc_new_len'    => $newSize,
+                       'rc_deleted'    => 0,
+                       'rc_logid'              => 0,
+                       'rc_log_type'   => null,
+                       'rc_log_action' => '',
+                       'rc_params'             => ''
                );
 
                $rc->mExtra =  array(
@@ -316,11 +326,9 @@ class RecentChange
        public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot = 'default',
          $ip='', $size = 0, $newId = 0 )
        {
-               if ( !$ip ) {
+               if( !$ip ) {
                        $ip = wfGetIP();
-                       if ( !$ip ) {
-                               $ip = '';
-                       }
+                       if( !$ip ) $ip = '';
                }
                if ( $bot === 'default' ) {
                        $bot = $user->isAllowed( 'bot' );
@@ -345,9 +353,14 @@ class RecentChange
                        'rc_moved_to_title' => '',
                        'rc_ip'             => $ip,
                        'rc_patrolled'      => 0,
-                       'rc_new'            => 1, # obsolete
+                       'rc_new'                => 1, # obsolete
                        'rc_old_len'        => 0,
-                       'rc_new_len'        => $size
+                       'rc_new_len'        => $size,
+                       'rc_deleted'            => 0,
+                       'rc_logid'                      => 0,
+                       'rc_log_type'           => null,
+                       'rc_log_action'         => '',
+                       'rc_params'                     => ''
                );
 
                $rc->mExtra =  array(
@@ -363,11 +376,9 @@ class RecentChange
        # Makes an entry in the database corresponding to a rename
        public static function notifyMove( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='', $overRedir = false )
        {
-               if ( !$ip ) {
+               if( !$ip ) {
                        $ip = wfGetIP();
-                       if ( !$ip ) {
-                               $ip = '';
-                       }
+                       if( !$ip ) $ip = '';
                }
 
                $rc = new RecentChange;
@@ -392,6 +403,11 @@ class RecentChange
                        'rc_patrolled'  => 1,
                        'rc_old_len'    => NULL,
                        'rc_new_len'    => NULL,
+                       'rc_deleted'    => 0,
+                       'rc_logid'              => 0, # notifyMove not used anymore
+                       'rc_log_type'   => null,
+                       'rc_log_action' => '',
+                       'rc_params'             => ''
                );
 
                $rc->mExtra = array(
@@ -410,30 +426,27 @@ class RecentChange
                RecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip, true );
        }
 
-       # A log entry is different to an edit in that previous revisions are
-       # not kept
+       # A log entry is different to an edit in that previous revisions are not kept
        public static function notifyLog( $timestamp, &$title, &$user, $comment, $ip='',
-          $type, $action, $target, $logComment, $params )
+          $type, $action, $target, $logComment, $params, $newId=0 )
        {
-               if ( !$ip ) {
+               if( !$ip ) {
                        $ip = wfGetIP();
-                       if ( !$ip ) {
-                               $ip = '';
-                       }
+                       if( !$ip ) $ip = '';
                }
 
                $rc = new RecentChange;
                $rc->mAttribs = array(
                        'rc_timestamp'  => $timestamp,
                        'rc_cur_time'   => $timestamp,
-                       'rc_namespace'  => $title->getNamespace(),
-                       'rc_title'      => $title->getDBkey(),
+                       'rc_namespace'  => $target->getNamespace(),
+                       'rc_title'      => $target->getDBkey(),
                        'rc_type'       => RC_LOG,
                        'rc_minor'      => 0,
-                       'rc_cur_id'     => $title->getArticleID(),
+                       'rc_cur_id'     => $target->getArticleID(),
                        'rc_user'       => $user->getID(),
                        'rc_user_text'  => $user->getName(),
-                       'rc_comment'    => $comment,
+                       'rc_comment'    => $logComment,
                        'rc_this_oldid' => 0,
                        'rc_last_oldid' => 0,
                        'rc_bot'        => $user->isAllowed( 'bot' ) ? 1 : 0,
@@ -444,6 +457,11 @@ class RecentChange
                        'rc_new'        => 0, # obsolete
                        'rc_old_len'    => NULL,
                        'rc_new_len'    => NULL,
+                       'rc_deleted'    => 0,
+                       'rc_logid'              => $newId,
+                       'rc_log_type'   => $type,
+                       'rc_log_action' => $action,
+                       'rc_params'             => $params
                );
                $rc->mExtra =  array(
                        'prefixedDBkey' => $title->getPrefixedDBkey(),
@@ -490,6 +508,11 @@ class RecentChange
                        'rc_new' => $row->page_is_new, # obsolete
                        'rc_old_len' => $row->rc_old_len,
                        'rc_new_len' => $row->rc_new_len,
+                       'rc_deleted'  => $row->rc_deleted,
+                       'rc_logid'      => $row->rc_logid,
+                       'rc_log_type'   => $row->rc_log_type,
+                       'rc_log_action' => $row->rc_log_action,
+                       'rc_params'     => $row->rc_params
                );
 
                $this->mExtra = array();
index 3947092..27ccd45 100644 (file)
@@ -794,7 +794,7 @@ class Revision {
         * @param bool     $minor
         * @return Revision
         */
-       function newNullRevision( &$dbw, $pageId, $summary, $minor ) {
+       static function newNullRevision( &$dbw, $pageId, $summary, $minor ) {
                $fname = 'Revision::newNullRevision';
                wfProfileIn( $fname );
 
index 81436bb..c62f610 100644 (file)
@@ -57,7 +57,9 @@ if( $wgFileCacheDirectory === false ) $wgFileCacheDirectory = "{$wgUploadDirecto
 if ( empty( $wgFileStore['deleted']['directory'] ) ) {
        $wgFileStore['deleted']['directory'] = "{$wgUploadDirectory}/deleted";
 }
-
+if ( empty( $wgFileStore['hidden']['directory'] ) ) {
+       $wgFileStore['hidden']['directory'] = "{$wgUploadDirectory}/hidden";
+}
 
 /**
  * Initialise $wgLocalFileRepo from backwards-compatible settings
@@ -73,7 +75,9 @@ if ( !$wgLocalFileRepo ) {
                'transformVia404' => !$wgGenerateThumbnailOnParse,
                'initialCapital' => $wgCapitalLinks,
                'deletedDir' => $wgFileStore['deleted']['directory'],
-               'deletedHashLevels' => $wgFileStore['deleted']['hash']
+               'deletedHashLevels' => $wgFileStore['deleted']['hash'],
+               'hiddenDir' => $wgFileStore['hidden']['directory'],
+               'hiddenHashLevels' => $wgFileStore['hidden']['hash']
        );
 }
 /**
index 942ebe8..35b54f1 100644 (file)
@@ -227,34 +227,35 @@ class IPBlockForm {
                        </td>
                </tr>
                ");
-               // Allow some users to hide name from block log, blocklist and listusers
-               if ( $wgUser->isAllowed( 'hideuser' ) ) {
+
+               global $wgSysopEmailBans;
+               if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) {
                        $wgOut->addHTML("
                        <tr>
                        <td>&nbsp;</td>
                                <td>
-                                       " . wfCheckLabel( wfMsgHtml( 'ipbhidename' ),
-                                                       'wpHideName', 'wpHideName', $this->BlockHideName,
-                                                               array( 'tabindex' => '9' ) ) . "
+                                       " . wfCheckLabel( wfMsgHtml( 'ipbemailban' ),
+                                                       'wpEmailBan', 'wpEmailBan', $this->BlockEmail,
+                                                               array( 'tabindex' => '10' )) . "
                                </td>
                        </tr>
                        ");
                }
 
-               global $wgSysopEmailBans;
-
-               if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) {
+               // Allow some users to hide name from block log, blocklist and listusers
+               if ( $wgUser->isAllowed( 'hideuser' ) ) {
                        $wgOut->addHTML("
                        <tr id='wpEnableEmailBan'>
                        <td>&nbsp;</td>
                                <td>
-                                       " . wfCheckLabel( wfMsgHtml( 'ipbemailban' ),
-                                                       'wpEmailBan', 'wpEmailBan', $this->BlockEmail,
-                                                               array( 'tabindex' => '10' )) . "
+                                       " . wfCheckLabel( wfMsgHtml( 'ipbhidename' ),
+                                                       'wpHideName', 'wpHideName', $this->BlockHideName,
+                                                               array( 'tabindex' => '9' ) ) . "
                                </td>
                        </tr>
                        ");
                }
+               
                $wgOut->addHTML("
                <tr>
                        <td style='padding-top: 1em'>&nbsp;</td>
index e85ec43..4ffc3b4 100644 (file)
@@ -171,7 +171,7 @@ class ContribsPager extends IndexPager {
                }
                $histlink='('.$sk->makeKnownLinkObj( $page, $this->messages['hist'], 'action=history' ) . ')';
 
-               $comment = $wgContLang->getDirMark() . $sk->revComment( $rev );
+               $comment = $wgContLang->getDirMark() . $sk->revComment( $rev, false, true );
                $d = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->rev_timestamp ), true );
                
                if( $this->target == 'newbies' ) {
index 015da0d..b3c3a21 100644 (file)
@@ -322,13 +322,13 @@ class IPUnblockForm {
                        $titleObj = SpecialPage::getTitleFor( "Ipblocklist" );
                        $unblocklink = ' (' . $sk->makeKnownLinkObj($titleObj, $msg['unblocklink'], 'action=unblock&id=' . urlencode( $block->mId ) ) . ')';
                }
-               
+
                $comment = $sk->commentBlock( $block->mReason );
-               
+
                $s = "{$line} $comment";        
                if ( $block->mHideName )
                        $s = '<span class="history-deleted">' . $s . '</span>';
-                               
+       
                wfProfileOut( __METHOD__ );
                return "<li>$s $unblocklink</li>\n";
        }
index 09353d8..2b93b9b 100644 (file)
@@ -50,6 +50,23 @@ class LogReader {
                $this->db = wfGetDB( DB_SLAVE );
                $this->setupQuery( $request );
        }
+       
+       /**
+        * Returns a row of log data
+        * @param Title $title
+        * @param integer $logid, optional
+        * @private
+        */     
+       function newRowFromID( $logid ) {
+               $fname = 'LogReader::newFromTitle';
+
+               $dbr = wfGetDB( DB_SLAVE );
+               $row = $dbr->selectRow( 'logging', array('*'),
+                       array('log_id' => intval($logid) ), 
+                       $fname );
+
+               return $row;
+       }
 
        /**
         * Basic setup and applies the limiting factors from the WebRequest object.
@@ -80,10 +97,31 @@ class LogReader {
 
        /**
         * Set the log reader to return only entries of the given type.
+        * Type restrictions enforced here
         * @param string $type A log type ('upload', 'delete', etc)
         * @private
         */
        function limitType( $type ) {
+               global $wgLogRestrictions, $wgUser;
+               // Reset the array, clears extra "where" clauses when $par is used
+               $this->whereClauses = $hiddenLogs = array();
+               // Exclude logs this user can't see
+               if( isset($wgLogRestrictions) ) {
+                       if( isset($wgLogRestrictions[$type]) && !$wgUser->isAllowed( $wgLogRestrictions[$type] ) )
+                               return false;
+                       // Don't show private logs to unpriviledged users or
+                       // when not specifically requested.
+                       foreach( $wgLogRestrictions as $logtype => $right ) {
+                               if( !$wgUser->isAllowed( $right ) || empty($type) ) {
+                                       $safetype = $this->db->strencode( $logtype );
+                                       $hiddenLogs[] = "'$safetype'";
+                               }
+                       }
+                       if( !empty($hiddenLogs) ) {
+                               $this->whereClauses[] = 'log_type NOT IN('.implode(',',$hiddenLogs).')';
+                       }
+               }
+               
                if( empty( $type ) ) {
                        return false;
                }
@@ -161,11 +199,16 @@ class LogReader {
         * @private
         */
        function getQuery() {
+               global $wgAllowLogDeletion;
+       
                $logging = $this->db->tableName( "logging" );
                $sql = "SELECT /*! STRAIGHT_JOIN */ log_type, log_action, log_timestamp,
-                       log_user, user_name,
-                       log_namespace, log_title, page_id,
-                       log_comment, log_params FROM $logging ";
+                       log_user, user_name, log_namespace, log_title, page_id,
+                       log_comment, log_params, log_deleted ";
+               if( $wgAllowLogDeletion )       
+                       $sql .= ", log_id ";
+               
+               $sql .= "FROM $logging ";
                if( !empty( $this->joinClauses ) ) {
                        $sql .= implode( ' ', $this->joinClauses );
                }
@@ -233,7 +276,7 @@ class LogReader {
                $this->db->freeResult( $res );
                return $ret;
        }
-       
+
 }
 
 /**
@@ -241,8 +284,12 @@ class LogReader {
  * @addtogroup SpecialPage
  */
 class LogViewer {
+       const DELETED_ACTION = 1;
+       const DELETED_COMMENT = 2;
+       const DELETED_USER = 4;
+    const DELETED_RESTRICTED = 8;
+    
        const NO_ACTION_LINK = 1;
-       
        /**
         * @var LogReader $reader
         */
@@ -259,8 +306,22 @@ class LogViewer {
                global $wgUser;
                $this->skin = $wgUser->getSkin();
                $this->reader =& $reader;
+               $this->preCacheMessages();
                $this->flags = $flags;
        }
+       
+       /**
+        * As we use the same small set of messages in various methods and that
+        * they are called often, we call them once and save them in $this->message
+        */
+       function preCacheMessages() {
+               // Precache various messages
+               if( !isset( $this->message ) ) {
+                       foreach( explode(' ', 'viewpagelogs revhistory filehist rev-delundel' ) as $msg ) {
+                               $this->message[$msg] = wfMsgExt( $msg, array( 'escape') );
+                       }
+               }
+       }
 
        /**
         * Take over the whole output page in $wgOut with the log display.
@@ -278,6 +339,82 @@ class LogViewer {
                        $this->showError( $wgOut );
                }
        }
+       
+               /**
+        * Fetch event's user id if it's available to all users
+        * @return int
+        */
+       static function getUser( $event ) {
+               if( $this->isDeleted( $event, Revision::DELETED_USER ) ) {
+                       return 0;
+               } else {
+                       return $event->log_user;
+               }
+       }
+
+       /**
+        * Fetch event's user id without regard for the current user's permissions
+        * @return string
+        */
+       static function getRawUser( $event ) {
+               return $event->log_user;
+       }
+
+       /**
+        * Fetch event's username if it's available to all users
+        * @return string
+        */
+       static function getUserText( $event ) {
+               if( $this->isDeleted( $event, Revision::DELETED_USER ) ) {
+                       return "";
+               } else {
+                       if ( isset($event->user_name) ) {
+                          return $event->user_name;
+                       } else {
+                         return User::whoIs( $event->log_user );
+                       }
+               }
+       }
+
+       /**
+        * Fetch event's username without regard for view restrictions
+        * @return string
+        */
+       static function getRawUserText( $event ) {
+               if ( isset($event->user_name) ) {
+                       return $event->user_name;
+               } else {
+                       return User::whoIs( $event->log_user );
+               }
+       }
+       
+       /**
+        * Fetch event comment if it's available to all users
+        * @return string
+        */
+       static function getComment( $event ) {
+               if( $this->isDeleted( $event, Revision::DELETED_COMMENT ) ) {
+                       return "";
+               } else {
+                       return $event->log_comment;
+               }
+       }
+
+       /**
+        * Fetch event comment without regard for the current user's permissions
+        * @return string
+        */
+       static function getRawComment( $event ) {
+               return $event->log_comment;
+       }
+       
+       /**
+        * Returns the title of the page associated with this entry.
+        * @return Title
+        */
+       static function getTitle( $event ) {
+               return Title::makeTitle( $event->log_namespace, $event->log_title );
+       }
 
        /**
         * Load the data from the linked LogReader
@@ -363,54 +500,154 @@ class LogViewer {
                } else {
                        $linkCache->addBadLinkObj( $title );
                }
-
-               $userLink = $this->skin->userLink( $s->log_user, $s->user_name ) . $this->skin->userToolLinksRedContribs( $s->log_user, $s->user_name );
-               $comment = $wgContLang->getDirMark() . $this->skin->commentBlock( $s->log_comment );
+               // User links
+               $userLink = $this->skin->logUserTools( $s, true );
+               // Comment
+               if( $s->log_action == 'create2' ) {
+                       $comment = ''; // Suppress from old account creations, useless and can contain incorrect links
+               } else if( $s->log_deleted & self::DELETED_COMMENT ) {
+                       $comment = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-comment') . '</span>';
+               } else {
+                       $comment = $wgContLang->getDirMark() . $this->skin->commentBlock( $s->log_comment );
+               }
+               
                $paramArray = LogPage::extractParams( $s->log_params );
+               $revert = ''; $del = '';
+               
+               // Some user can hide log items and have review links
+               if( $wgUser->isAllowed( 'deleterevision' ) ) {
+                       $del = $this->showhideLinks( $s, $title );
+               }
+               
+               // Show restore/unprotect/unblock
+               $revert = $this->showReviewLinks( $s, $title, $paramArray );
+               
+               // Event description
+               if ( $s->log_deleted & self::DELETED_ACTION )
+                       $action = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
+               else
+                       $action = LogPage::actionText( $s->log_type, $s->log_action, $title, $this->skin, $paramArray, true );
+               
+               return "<li><tt>$del</tt> $time $userLink $action $comment $revert</li>";
+       }
+
+       /**
+        * @param $s, row object
+        * @param $s, title object
+        * @private
+        */
+       function showhideLinks( $s, $title ) {
+               global $wgAllowLogDeletion;
+               
+               if( !$wgAllowLogDeletion )
+                       return "";
+       
+               $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+               // If event was hidden from sysops
+               if( !self::userCan( $s, Revision::DELETED_RESTRICTED ) ) {
+                       $del = $this->message['rev-delundel'];
+               } else if( $s->log_type == 'oversight' ) {
+                       return ''; // No one should be hiding from the oversight log
+               } else {
+                       $del = $this->skin->makeKnownLinkObj( $revdel, $this->message['rev-delundel'], 'logid='.$s->log_id );
+                       // Bolden oversighted content
+                       if( self::isDeleted( $s, Revision::DELETED_RESTRICTED ) )
+                               $del = "<strong>$del</strong>";
+               }
+               return "(<small>$del</small>)";
+       }
+
+       /**
+        * @param $s, row object
+        * @param $title, title object
+        * @param $s, param array
+        * @private
+        */
+       function showReviewLinks( $s, $title, $paramArray ) {
+               global $wgUser;
+               
                $revert = '';
-               // show revertmove link
-               if ( !( $this->flags & self::NO_ACTION_LINK ) ) {
-                       if ( $s->log_type == 'move' && isset( $paramArray[0] ) ) {
-                               $destTitle = Title::newFromText( $paramArray[0] );
-                               if ( $destTitle ) {
-                                       $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ),
-                                               wfMsg( 'revertmove' ),
-                                               'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) .
-                                               '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) .
-                                               '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) .
-                                               '&wpMovetalk=0' ) . ')';
-                               }
-                       // show undelete link
-                       } elseif ( $s->log_action == 'delete' && $wgUser->isAllowed( 'delete' ) ) {
-                               $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ),
-                                       wfMsg( 'undeletebtn' ) ,
-                                       'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')';
+               // Don't give away the page name
+               if( self::isDeleted($s,self::DELETED_ACTION) )
+                       return $revert;
                        
-                       // show unblock link
-                       } elseif ( $s->log_action == 'block' && $wgUser->isAllowed( 'block' ) ) {
-                               $revert = '(' .  $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ),
-                                       wfMsg( 'unblocklink' ),
-                                       'action=unblock&ip=' . urlencode( $s->log_title ) ) . ')';
-                       // show change protection link
-                       } elseif ( ( $s->log_action == 'protect' || $s->log_action == 'modify' ) && $wgUser->isAllowed( 'protect' ) ) {
-                               $revert = '(' .  $skin->makeKnownLinkObj( $title, wfMsg( 'protect_change' ), 'action=unprotect' ) . ')';
-                       // show user tool links for self created users
-                       // TODO: The extension should be handling this, get it out of core!
-                       } elseif ( $s->log_action == 'create2' ) {
-                               if( isset( $paramArray[0] ) ) {
-                                       $revert = $this->skin->userToolLinks( $paramArray[0], $s->log_title, true );
-                               } else {
-                                       # Fall back to a blue contributions link
-                                       $revert = $this->skin->userToolLinks( 1, $s->log_title );
+               if( $this->flags & self::NO_ACTION_LINK ) {
+                       return $revert;
+               }
+               // Show revertmove link
+               if( $s->log_type == 'move' && isset( $paramArray[0] ) ) {
+                       $destTitle = Title::newFromText( $paramArray[0] );
+                       if ( $destTitle ) {
+                               $revert = $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ),
+                                       wfMsg( 'revertmove' ),
+                                       'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) .
+                                       '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) .
+                                       '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) .
+                                       '&wpMovetalk=0' );
+                       }
+               // show undelete link
+               } else if( $s->log_action == 'delete' && $wgUser->isAllowed( 'delete' ) ) {
+                       $revert = $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ),
+                               wfMsg( 'undeletebtn' ) ,
+                               'target='. urlencode( $title->getPrefixedDBkey() ) );
+               // show unblock link
+               } elseif( $s->log_action == 'block' && $wgUser->isAllowed( 'block' ) ) {
+                       $revert = $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ),
+                               wfMsg( 'unblocklink' ),
+                               'action=unblock&ip=' . urlencode( $s->log_title ) );
+               // show change protection link
+               } elseif ( ( $s->log_action == 'protect' || $s->log_action == 'modify' ) && $wgUser->isAllowed( 'protect' ) ) {
+                       $revert = $this->skin->makeKnownLinkObj( $title, wfMsg( 'protect_change' ), 'action=unprotect' );
+               // show user tool links for self created users
+               // TODO: The extension should be handling this, get it out of core!
+               } elseif ( $s->log_action == 'create2' ) {
+                       if( isset( $paramArray[0] ) ) {
+                               $revert = $this->skin->userToolLinks( $paramArray[0], $s->log_title, true );
+                       } else {
+                               # Fall back to a blue contributions link
+                               $revert = $this->skin->userToolLinks( 1, $s->log_title );
+                       }
+               // If an edit was hidden from a page give a review link to the history
+               } elseif ( $s->log_action == 'merge' ) {
+                       $merge = SpecialPage::getTitleFor( 'Mergehistory' );
+                       $revert = $this->skin->makeKnownLinkObj( $merge, wfMsg('revertmerge'),
+                                       wfArrayToCGI( array('target' => $paramArray[0], 'dest' => $title->getPrefixedText() ) ) );
+               // If an edit was hidden from a page give a review link to the history
+               } else if( ($s->log_action=='revision') && $wgUser->isAllowed( 'deleterevision' ) ) {
+                       $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+                       // Different revision types use different URL params...
+                       $subtype = isset($paramArray[2]) ? $paramArray[1] : '';
+                       // Link to each hidden object ID, $paramArray[1] is the url param.
+                       // Don't number by IDs because of their size.
+                       // We may often just have one, in which case it's easier to not...
+                       $Ids = explode( ',', $paramArray[2] );
+                       if( count($Ids) == 1 ) {
+                               $revert = $this->skin->makeKnownLinkObj( $revdel, wfMsgHtml('revdel-restore'),
+                                       wfArrayToCGI( array('target' => $paramArray[0], $paramArray[1] => $Ids[0] ) ) );
+                       } else {
+                               $revert .= wfMsgHtml('revdel-restore').':';
+                               foreach( $Ids as $n => $id ) {
+                                       $revert .= ' '.$this->skin->makeKnownLinkObj( $revdel, '#'.($n+1),
+                                               wfArrayToCGI( array('target' => $paramArray[0], $paramArray[1] => $id ) ) );
+                               }
+                       }
+               // Hidden log items, give review link
+               } else if( ($s->log_action=='event') && $wgUser->isAllowed( 'deleterevision' ) ) {
+                       $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+                       $revert .= wfMsgHtml('revdel-restore');
+                       $Ids = explode( ',', $paramArray[0] );
+                       if( count($Ids) == 1 ) {
+                               $revert = $this->skin->makeKnownLinkObj( $revdel, wfMsgHtml('revdel-restore'),
+                                       wfArrayToCGI( array('logid' => $Ids[0] ) ) );
+                       } else {
+                               foreach( $Ids as $n => $id ) {
+                                       $revert .= $this->skin->makeKnownLinkObj( $revdel, '#'.($n+1),
+                                               wfArrayToCGI( array('logid' => $id ) ) );
                                }
-                               # Suppress $comment from old entries, not needed and can contain incorrect links
-                               $comment = '';
                        }
                }
-
-               $action = LogPage::actionText( $s->log_type, $s->log_action, $title, $this->skin, $paramArray, true );
-               $out = "<li>$time $userLink $action $comment $revert</li>\n";
-               return $out;
+               $revert = ($revert == '') ? "" : "&nbsp;&nbsp;&nbsp;($revert) ";
+               return $revert;
        }
 
        /**
@@ -451,6 +688,8 @@ class LogViewer {
         * @private
         */
        function getTypeMenu() {
+               global $wgLogRestrictions, $wgUser;
+       
                $out = "<select name='type'>\n";
 
                $validTypes = LogPage::validTypes();
@@ -468,7 +707,14 @@ class LogViewer {
                // Third pass generates sorted XHTML content
                foreach( $m as $text => $type ) {
                        $selected = ($type == $this->reader->queryType());
-                       $out .= Xml::option( $text, $type, $selected ) . "\n";
+                       // Restricted types
+                       if ( isset($wgLogRestrictions[$type]) ) {
+                               if ( $wgUser->isAllowed( $wgLogRestrictions[$type] ) ) {
+                                       $out .= Xml::option( $text, $type, $selected ) . "\n";
+                               }
+                       } else {
+                               $out .= Xml::option( $text, $type, $selected ) . "\n";
+                       }
                }
 
                $out .= '</select>';
@@ -524,7 +770,41 @@ class LogViewer {
                        $this->numResults < $limit);
                $out->addHTML( '<p>' . $html . '</p>' );
        }
+
+       /**
+        * Determine if the current user is allowed to view a particular
+        * field of this event, if it's marked as deleted.
+        * @param int $field
+        * @return bool
+        */
+       public static function userCan( $event, $field ) {
+               if( ( $event->log_deleted & $field ) == $field ) {
+                       global $wgUser;
+                       $permission = ( $event->log_deleted & Revision::DELETED_RESTRICTED ) == Revision::DELETED_RESTRICTED
+                               ? 'hiderevision'
+                               : 'deleterevision';
+                       wfDebug( "Checking for $permission due to $field match on $event->log_deleted\n" );
+                       return $wgUser->isAllowed( $permission );
+               } else {
+                       return true;
+               }
+       }
+
+       /**
+        * int $field one of DELETED_* bitfield constants
+        * @return bool
+        */
+       public static function isDeleted( $event, $field ) {
+               return ($event->log_deleted & $field) == $field;
+       }
 }
 
+/**
+ * Aliases for backwards compatibility with 1.6
+ */
+define( 'MW_LOG_DELETED_ACTION', LogViewer::DELETED_ACTION );
+define( 'MW_LOG_DELETED_USER', LogViewer::DELETED_USER );
+define( 'MW_LOG_DELETED_COMMENT', LogViewer::DELETED_COMMENT );
+define( 'MW_LOG_DELETED_RESTRICTED', LogViewer::DELETED_RESTRICTED );
 
 
diff --git a/includes/SpecialMergeHistory.php b/includes/SpecialMergeHistory.php
new file mode 100644 (file)
index 0000000..5e11eb9
--- /dev/null
@@ -0,0 +1,418 @@
+<?php\r
+\r
+/**\r
+ * Special page allowing users with the appropriate permissions to \r
+ * merge article histories, with some restrictions\r
+ *\r
+ * @addtogroup SpecialPage\r
+ */\r
+\r
+/**\r
+ * Constructor\r
+ */\r
+function wfSpecialMergehistory( $par ) {\r
+       global $wgRequest;\r
+\r
+       $form = new MergehistoryForm( $wgRequest, $par );\r
+       $form->execute();\r
+}\r
+\r
+/**\r
+ * The HTML form for Special:MergeHistory, which allows users with the appropriate\r
+ * permissions to view and restore deleted content.\r
+ * @addtogroup SpecialPage\r
+ */\r
+class MergehistoryForm {\r
+       var $mAction, $mTarget, $mDest, $mTimestamp, $mTargetID, $mDestID, $mComment;\r
+       var $mTargetObj, $mDestObj;\r
+\r
+       function MergehistoryForm( $request, $par = "" ) {\r
+               global $wgUser;\r
+               \r
+               $this->mAction = $request->getVal( 'action' );\r
+               $this->mTarget = $request->getVal( 'target' );\r
+               $this->mDest = $request->getVal( 'dest' );\r
+               \r
+               $this->mTargetID = intval( $request->getVal( 'targetID' ) );\r
+               $this->mDestID = intval( $request->getVal( 'destID' ) );\r
+               $this->mTimestamp = $request->getVal( 'mergepoint' );\r
+               $this->mComment = $request->getText( 'wpComment' );\r
+               \r
+               $this->mMerge = $request->wasPosted() && $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );\r
+               // target page\r
+               if( $this->mTarget !== "" ) {\r
+                       $this->mTargetObj = Title::newFromURL( $this->mTarget );\r
+               } else {\r
+                       $this->mTargetObj = NULL;\r
+               }\r
+               # Destination\r
+               if( $this->mDest !== "" ) {\r
+                       $this->mDestObj = Title::newFromURL( $this->mDest );\r
+               } else {\r
+                       $this->mDestObj = NULL;\r
+               }               \r
+               \r
+               $this->preCacheMessages();\r
+       }\r
+       \r
+       /**\r
+        * As we use the same small set of messages in various methods and that\r
+        * they are called often, we call them once and save them in $this->message\r
+        */\r
+       function preCacheMessages() {\r
+               // Precache various messages\r
+               if( !isset( $this->message ) ) {\r
+                       $this->message['last'] = wfMsgExt( 'last', array( 'escape') );\r
+               }\r
+       }\r
+\r
+       function execute() {\r
+               global $wgOut, $wgUser;\r
+               \r
+               $wgOut->setPagetitle( wfMsgHtml( "mergehistory" ) );\r
+               \r
+               if( $this->mTargetID && $this->mDestID && $this->mAction=="submit" && $this->mMerge ) {\r
+                       return $this->merge();\r
+               }\r
+               \r
+               if( is_object($this->mTargetObj) && is_object($this->mDestObj) ) {\r
+                       return $this->showHistory();\r
+               }\r
+               \r
+               return $this->showMergeForm();\r
+       }\r
+\r
+       function showMergeForm() {\r
+               global $wgOut, $wgScript;\r
+               \r
+               $wgOut->addWikiText( wfMsg( 'mergehistory-header' ) );\r
+               \r
+               $wgOut->addHtml(\r
+                       Xml::openElement( 'form', array(\r
+                               'method' => 'get',\r
+                               'action' => $wgScript ) ) .\r
+                       '<fieldset>' .\r
+                       Xml::element( 'legend', array(),\r
+                               wfMsg( 'mergehistory-box' ) ) .\r
+                       Xml::hidden( 'title',\r
+                               SpecialPage::getTitleFor( 'Mergehistory' )->getPrefixedDbKey() ) .\r
+                       Xml::openElement( 'table' ) .\r
+                       "<tr>\r
+                               <td>".Xml::Label( wfMsg( 'mergehistory-from' ), 'target' )."</td>\r
+                               <td>".Xml::input( 'target', 30, $this->mTarget, array('id'=>'target') )."</td>\r
+                       </tr><tr>\r
+                               <td>".Xml::Label( wfMsg( 'mergehistory-into' ), 'dest' )."</td>\r
+                               <td>".Xml::input( 'dest', 30, $this->mDest, array('id'=>'dest') )."</td>\r
+                       </tr><tr><td>" .\r
+                       Xml::submitButton( wfMsg( 'mergehistory-go' ) ) .\r
+                       "</td></tr>" .\r
+                       Xml::closeElement( 'table' ) .\r
+                       '</fieldset>' .\r
+                       '</form>' );\r
+       }\r
+\r
+       private function showHistory() {\r
+               global $wgLang, $wgContLang, $wgUser, $wgOut;\r
+\r
+               $this->sk = $wgUser->getSkin();\r
+               \r
+               $wgOut->setPagetitle( wfMsg( "mergehistory" ) );\r
+               \r
+               $this->showMergeForm();\r
+               \r
+               # List all stored revisions\r
+               $revisions = new MergeHistoryPager( $this, array(), $this->mTargetObj, $this->mDestObj );\r
+               $haveRevisions = $revisions && $revisions->getNumRows() > 0;\r
+\r
+               $titleObj = SpecialPage::getTitleFor( "Mergehistory" );\r
+               $action = $titleObj->getLocalURL( "action=submit" );\r
+               # Start the form here\r
+               $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'merge' ) );\r
+               $wgOut->addHtml( $top );\r
+\r
+               if( $haveRevisions ) {\r
+                       # Format the user-visible controls (comment field, submission button)\r
+                       # in a nice little table\r
+                       $align = $wgContLang->isRtl() ? 'left' : 'right';\r
+                       $table =\r
+                               Xml::openElement( 'fieldset' ) .\r
+                               Xml::openElement( 'table' ) .\r
+                                       "<tr>\r
+                                               <td colspan='2'>" .\r
+                                                       wfMsgExt( 'mergehistory-merge', array('parseinline'),\r
+                                                               $this->mTargetObj->getPrefixedText(), $this->mDestObj->getPrefixedText() ) .\r
+                                               "</td>\r
+                                       </tr>\r
+                                       <tr>\r
+                                               <td align='$align'>" .\r
+                                                       Xml::label( wfMsg( 'undeletecomment' ), 'wpComment' ) .\r
+                                               "</td>\r
+                                               <td>" .\r
+                                                       Xml::input( 'wpComment', 50, $this->mComment ) .\r
+                                               "</td>\r
+                                       </tr>\r
+                                       <tr>\r
+                                               <td>&nbsp;</td>\r
+                                               <td>" .\r
+                                                       Xml::submitButton( wfMsg( 'mergehistory-submit' ), array( 'name' => 'merge', 'id' => 'mw-merge-submit' ) ) .\r
+                                               "</td>\r
+                                       </tr>" .\r
+                               Xml::closeElement( 'table' ) .\r
+                               Xml::closeElement( 'fieldset' );\r
+\r
+                       $wgOut->addHtml( $table );\r
+               }\r
+\r
+               $wgOut->addHTML( "<h2 id=\"mergehistory\">" . wfMsgHtml( "mergehistory-list" ) . "</h2>\n" );\r
+\r
+               if( $haveRevisions ) {\r
+                       $wgOut->addHTML( $revisions->getNavigationBar() );\r
+                       $wgOut->addHTML( "<ul>" );\r
+                       $wgOut->addHTML( $revisions->getBody() );\r
+                       $wgOut->addHTML( "</ul>" );\r
+                       $wgOut->addHTML( $revisions->getNavigationBar() );\r
+               } else {\r
+                       $wgOut->addWikiText( wfMsg( "mergehistory-empty" ) );\r
+               }\r
+\r
+               # Show relevant lines from the deletion log:\r
+               $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'merge' ) ) . "</h2>\n" );\r
+               $logViewer = new LogViewer(\r
+                       new LogReader(\r
+                               new FauxRequest(\r
+                                       array( 'page' => $this->mTargetObj->getPrefixedText(),\r
+                                                  'type' => 'merge' ) ) ) );\r
+               $logViewer->showList( $wgOut );\r
+               \r
+               # Slip in the hidden controls here\r
+               # When we submit, go by page ID to avoid some nasty but unlikely collisions.\r
+               # Such would happen if a page was renamed after the form loaded, but before submit\r
+               $misc = Xml::hidden( 'targetID', $this->mTargetObj->getArticleID() );\r
+               $misc .= Xml::hidden( 'destID', $this->mDestObj->getArticleID() );\r
+               $misc .= Xml::hidden( 'target', $this->mTarget );\r
+               $misc .= Xml::hidden( 'dest', $this->mDest );\r
+               $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() );\r
+               $misc .= Xml::closeElement( 'form' );\r
+               $wgOut->addHtml( $misc );\r
+\r
+               return true;\r
+       }\r
+       \r
+       function formatRevisionRow( $row ) {\r
+               global $wgUser, $wgLang;\r
+               \r
+               $rev = new Revision( $row );\r
+               \r
+               $stxt = ''; \r
+               $last = $this->message['last'];\r
+               \r
+               $ts = wfTimestamp( TS_MW, $row->rev_timestamp );\r
+               $checkBox = wfRadio( "mergepoint", $ts, false );\r
+               \r
+               $pageLink = $this->sk->makeKnownLinkObj( $rev->getTitle(), $wgLang->timeanddate( $ts ), 'oldid=' . $rev->getID() );\r
+               if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {\r
+                       $pageLink = '<span class="history-deleted">' . $pageLink . '</span>';\r
+               }\r
+               \r
+               # Last link\r
+               if( !$rev->userCan( Revision::DELETED_TEXT ) )\r
+                       $last = $this->message['last'];\r
+               else if( isset($this->prevId[$row->rev_id]) )\r
+                       $last = $this->sk->makeKnownLinkObj( $rev->getTitle(), $this->message['last'], \r
+                               "&diff=" . $row->rev_id . "&oldid=" . $this->prevId[$row->rev_id] );\r
+               \r
+               $userLink = $this->sk->userLink( $rev->getUser(), $rev->getUserText() )\r
+                       . $this->sk->userToolLinks( $rev->getUser(), $rev->getUserText() );\r
+               \r
+               if(!is_null($size = $row->rev_len)) {\r
+                       if($size == 0)\r
+                               $stxt = wfMsgHtml('historyempty');\r
+                       else\r
+                               $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) );\r
+               }\r
+               $comment = $this->sk->revComment( $rev );\r
+               \r
+               return "<li>$checkBox ($last) $pageLink . . $userLink $stxt $comment</li>";\r
+       }\r
+\r
+       /**\r
+        * Fetch revision text link if it's available to all users\r
+        * @return string\r
+        */\r
+       function getPageLink( $row, $titleObj, $ts, $target ) {\r
+               global $wgLang;\r
+               \r
+               if( !$this->userCan($row, Revision::DELETED_TEXT) ) {\r
+                       return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';\r
+               } else {\r
+                       $link = $this->sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ), "target=$target&timestamp=$ts" );\r
+                       if( $this->isDeleted($row, Revision::DELETED_TEXT) )\r
+                               $link = '<span class="history-deleted">' . $link . '</span>';\r
+                       return $link;\r
+               }\r
+       }\r
+\r
+       /**\r
+        * Fetch revision's user id if it's available to this user\r
+        * @return string\r
+        */\r
+       function getUser( $row ) {      \r
+               if( !$this->userCan($row, Revision::DELETED_USER) ) {\r
+                       return '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';\r
+               } else {\r
+                       $link = $this->sk->userLink( $row->rev_user, $row->rev_user_text ) . $this->sk->userToolLinks( $row->rev_user, $row->rev_user_text );\r
+                       if( $this->isDeleted($row, Revision::DELETED_USER) )\r
+                               $link = '<span class="history-deleted">' . $link . '</span>';\r
+                       return $link;\r
+               }\r
+       }\r
+\r
+       /**\r
+        * Fetch revision comment if it's available to this user\r
+        * @return string\r
+        */\r
+       function getComment( $row ) {\r
+               if( !$this->userCan($row, Revision::DELETED_COMMENT) ) {\r
+                       return '<span class="history-deleted"><span class="comment">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span></span>';\r
+               } else {\r
+                       $link = $this->sk->commentBlock( $row->rev_comment );\r
+                       if( $this->isDeleted($row, Revision::DELETED_COMMENT) )\r
+                               $link = '<span class="history-deleted">' . $link . '</span>';\r
+                       return $link;\r
+               }\r
+       }\r
+\r
+       function merge() {\r
+               global $wgOut, $wgUser;\r
+               # Get the titles directly from the IDs, in case the target page params \r
+               # were spoofed. The queries are done based on the IDs, so it's best to \r
+               # keep it consistent...\r
+               $targetTitle = Title::newFromID( $this->mTargetID );\r
+               $destTitle = Title::newFromID( $this->mDestID );\r
+               if( is_null($targetTitle) || is_null($destTitle) )\r
+                       return false; // validate these\r
+               # Verify that this timestamp is valid\r
+               # Must be older than the destination page\r
+               $dbw = wfGetDB( DB_MASTER );\r
+               $maxtimestamp = $dbw->selectField( 'revision', 'MIN(rev_timestamp)',\r
+                       array('rev_page' => $this->mDestID ),\r
+                       __METHOD__ );\r
+               # Destination page must exist with revisions\r
+               if( !$maxtimestamp ) {\r
+                       $wgOut->addHtml( wfMsg('mergehistory-fail') );\r
+                       return false;\r
+               }\r
+               # Leave the latest version no matter what\r
+               $lasttime = $dbw->selectField( array('page','revision'), \r
+                       'rev_timestamp',\r
+                       array('page_id' => $this->mTargetID, 'page_latest = rev_id' ),\r
+                       __METHOD__ );\r
+               # Take the most restrictive of the twain\r
+               $maxtimestamp = ($lasttime < $maxtimestamp) ? $lasttime : $maxtimestamp;\r
+               if( $this->mTimestamp && $this->mTimestamp >= $maxtimestamp ) {\r
+                       $wgOut->addHtml( wfMsg('mergehistory-fail') );\r
+                       return false;\r
+               }\r
+               # Update the revisions\r
+               if( $this->mTimestamp )\r
+                       $timewhere = "rev_timestamp <= {$this->mTimestamp}";\r
+               else\r
+                       $timewhere = '1 = 1';\r
+               \r
+               $dbw->update( 'revision',\r
+                       array( 'rev_page' => $this->mDestID ),\r
+                       array( 'rev_page' => $this->mTargetID, \r
+                               "rev_timestamp < {$maxtimestamp}",\r
+                               $timewhere ),\r
+                       __METHOD__ );\r
+               # Check if this did anything\r
+               $count = $dbw->affectedRows();\r
+               if( !$count ) {\r
+                       $wgOut->addHtml( wfMsg('mergehistory-fail') );\r
+                       return false;\r
+               }\r
+               # Update our logs\r
+               $log = new LogPage( 'merge' );\r
+               $log->addEntry( 'merge', $targetTitle, $this->mComment, \r
+                       array($destTitle->getPrefixedText(),$this->mTimestamp) );\r
+               \r
+               $wgOut->addHtml( wfMsgExt( 'mergehistory-success', array('parseinline'),\r
+                       $targetTitle->getPrefixedText(), $destTitle->getPrefixedText(), $count ) );\r
+               \r
+               wfRunHooks( 'ArticleMergeComplete', array( $targetTitle, $destTitle ) );\r
+               \r
+               return true;\r
+       }\r
+}\r
+\r
+class MergeHistoryPager extends ReverseChronologicalPager {\r
+       public $mForm, $mConds;\r
+\r
+       function __construct( $form, $conds = array(), $title, $title2 ) {\r
+               $this->mForm = $form;\r
+               $this->mConds = $conds;\r
+               $this->title = $title;\r
+               $this->articleID = $title->getArticleID();\r
+               \r
+               $dbw = wfGetDB( DB_SLAVE );\r
+               $maxtimestamp = $dbw->selectField('revision', 'MIN(rev_timestamp)',\r
+                       array('rev_page' => $title2->getArticleID() ),\r
+                       __METHOD__ );\r
+               $maxtimestamp = $maxtimestamp ? $maxtimestamp : 0;\r
+               $this->maxTimestamp = $maxtimestamp;\r
+               \r
+               parent::__construct();\r
+       }\r
+       \r
+       function getStartBody() {\r
+               wfProfileIn( __METHOD__ );\r
+               # Do a link batch query\r
+               $this->mResult->seek( 0 );\r
+               $batch = new LinkBatch();\r
+               # Give some pointers to make (last) links\r
+               $this->mForm->prevId = array();\r
+               while( $row = $this->mResult->fetchObject() ) {\r
+                       $batch->addObj( Title::makeTitleSafe( NS_USER, $row->rev_user_text ) );\r
+                       $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->rev_user_text ) );\r
+                       \r
+                       $rev_id = isset($rev_id) ? $rev_id : $row->rev_id;\r
+                       if( $rev_id > $row->rev_id )\r
+                               $this->mForm->prevId[$rev_id] = $row->rev_id;\r
+                       else if( $rev_id < $row->rev_id )\r
+                               $this->mForm->prevId[$row->rev_id] = $rev_id;\r
+                       \r
+                       $rev_id = $row->rev_id;\r
+               }\r
+               \r
+               $batch->execute();\r
+               $this->mResult->seek( 0 );\r
+\r
+               wfProfileOut( __METHOD__ );\r
+               return '';\r
+       }\r
+       \r
+       function formatRow( $row ) {\r
+               $block = new Block;\r
+               return $this->mForm->formatRevisionRow( $row );\r
+       }\r
+\r
+       function getQueryInfo() {\r
+               $conds = $this->mConds;\r
+               $conds['rev_page'] = $this->articleID;\r
+               $conds[] = "rev_timestamp < {$this->maxTimestamp}";\r
+               # Skip the latest one, as that could cause problems\r
+               if( $page = $this->title->getLatestRevID() )\r
+                       $conds[] = "rev_id != {$page}";\r
+               \r
+               return array(\r
+                       'tables' => array('revision'),\r
+                       'fields' => array( 'rev_minor_edit', 'rev_timestamp', 'rev_user', 'rev_user_text', 'rev_comment', \r
+                                'rev_id', 'rev_page', 'rev_text_id', 'rev_len', 'rev_deleted' ),\r
+                       'conds' => $conds\r
+               );\r
+       }\r
+\r
+       function getIndexField() {\r
+               return 'rev_timestamp';\r
+       }\r
+}\r
index ded0124..98d5067 100644 (file)
@@ -131,7 +131,7 @@ class SpecialPage
                'Log'                       => array( 'SpecialPage', 'Log' ),
                'Blockip'                   => array( 'SpecialPage', 'Blockip', 'block' ),
                'Undelete'                  => array( 'SpecialPage', 'Undelete', 'deletedhistory' ),
-               'Import'                    => array( 'SpecialPage', "Import", 'import' ),
+               'Import'                    => array( 'SpecialPage', 'Import', 'import' ),
                'Lockdb'                    => array( 'SpecialPage', 'Lockdb', 'siteadmin' ),
                'Unlockdb'                  => array( 'SpecialPage', 'Unlockdb', 'siteadmin' ),
                'Userrights'                => array( 'SpecialPage', 'Userrights', 'userrights' ),
@@ -147,6 +147,7 @@ class SpecialPage
                'Mytalk'                    => array( 'SpecialMytalk' ),
                'Mycontributions'           => array( 'SpecialMycontributions' ),
                'Listadmins'                => array( 'SpecialRedirectToSpecial', 'Listadmins', 'Listusers', 'sysop' ),
+               'MergeHistory'              => array( 'SpecialPage', 'Mergehistory', 'mergehistory' ),
        );
 
        static public $mAliases;
@@ -379,6 +380,28 @@ class SpecialPage
                }
                return $pages;
        }
+       
+       /**
+        * Return categorised listable log pages which are available
+        * for the current user, but not for everyone
+        * @static
+        */
+       static function getRestrictedLogs() {
+               global $wgUser, $wgLogRestrictions, $wgLogNames;
+       
+               $pages = array();
+       
+               if ( isset($wgLogRestrictions) ) {
+                       foreach ( $wgLogRestrictions as $type => $restriction ) {
+                               $page = SpecialPage::getTitleFor( 'Log', $type );
+                               if ( $restriction !='' && $restriction !='*' && $wgUser->isAllowed( $restriction ) ) {
+                                       $name = wfMsgHtml( $wgLogNames[$type] );
+                                       $pages[$name] = $page;
+                               }
+                       }
+               }
+               return $pages;
+       }
 
        /**
         * Execute a special page path.
index 2744cd8..bd9c853 100644 (file)
@@ -408,7 +408,7 @@ function rcDoOutputFeed( $rows, &$feed ) {
                        rcFormatDiff( $obj ),
                        $title->getFullURL(),
                        $obj->rc_timestamp,
-                       $obj->rc_user_text,
+                       ($obj->rc_deleted & Revision::DELETED_USER) ? wfMsgHtml('rev-deleted-user') : $obj->rc_user_text,
                        $talkpage->getFullURL()
                        );
                $feed->outItem( $item );
@@ -617,15 +617,18 @@ function rcFormatDiff( $row ) {
        return rcFormatDiffRow( $titleObj,
                $row->rc_last_oldid, $row->rc_this_oldid,
                $timestamp,
-               $row->rc_comment );
+               ($row->rc_deleted & Revision::DELETED_COMMENT) ? wfMsgHtml('rev-deleted-comment') : $row->rc_comment,
+               ($row->rc_deleted & LogViewer::DELETED_ACTION) ? wfMsgHtml('rev-deleted-event') : $row->rc_actiontext );
 }
 
-function rcFormatDiffRow( $title, $oldid, $newid, $timestamp, $comment ) {
+function rcFormatDiffRow( $title, $oldid, $newid, $timestamp, $comment, $actiontext='' ) {
        global $wgFeedDiffCutoff, $wgContLang, $wgUser;
        $fname = 'rcFormatDiff';
        wfProfileIn( $fname );
 
        $skin = $wgUser->getSkin();
+       # log enties
+       if( $actiontext ) $comment = "$actiontext $comment";
        $completeText = '<p>' . $skin->formatComment( $comment ) . "</p>\n";
 
        //NOTE: Check permissions for anonymous users, not current user.
index 34e9dfb..bf3f41c 100644 (file)
@@ -1,37 +1,57 @@
 <?php
 
 /**
- * Not quite ready for production use yet; need to fix up the restricted mode,
- * and provide for preservation across delete/undelete of the page.
+ * Special page allowing users with the appropriate permissions to view
+ * and hide revisions. Log items can also be hidden.
  *
- * To try this out, set up extra permissions something like:
- * $wgGroupPermissions['sysop']['deleterevision'] = true;
- * $wgGroupPermissions['bureaucrat']['hiderevision'] = true;
+ * @addtogroup SpecialPage
  */
 
 function wfSpecialRevisiondelete( $par = null ) {
-       global $wgOut, $wgRequest;
-       
-       $target = $wgRequest->getVal( 'target' );
-       $oldid = $wgRequest->getIntArray( 'oldid' );
-       
+       global $wgOut, $wgRequest, $wgUser, $wgAllowLogDeletion;
+       # Handle our many different possible input types
+       $target = $wgRequest->getText( 'target' );
+       $oldid = $wgRequest->getArray( 'oldid' );
+       $artimestamp = $wgRequest->getArray( 'artimestamp' );
+       $logid = $wgAllowLogDeletion ? $wgRequest->getArray( 'logid' ) : '';
+       $image = $wgRequest->getArray( 'oldimage' );
+       $fileid = $wgRequest->getArray( 'fileid' );
+       # For reviewing deleted files...
+       $file = $wgRequest->getVal( 'file' );
+       # If this is a revision, then we need a target page
        $page = Title::newFromUrl( $target );
-       
-       if( is_null( $page ) ) {
-               $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
+       if( is_null($page) && is_null($logid) ) {
+               $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) );
                return;
        }
+       # Only one target set at a time please!
+       $inputs = !empty($file) + !empty($oldid) + !empty($logid) + !empty($artimestamp) + 
+               !empty($fileid) + !empty($image);
        
-       if( is_null( $oldid ) ) {
+       if( $inputs > 1 || $inputs==0 ) {
                $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
                return;
        }
-       
-       $form = new RevisionDeleteForm( $wgRequest );
+       # Either submit or create our form
+       $form = new RevisionDeleteForm( $page, $oldid, $logid, $artimestamp, $fileid, $image, $file );
        if( $wgRequest->wasPosted() ) {
                $form->submit( $wgRequest );
-       } else {
-               $form->show( $wgRequest );
+       } else if( $oldid || $artimestamp ) {
+               $form->showRevs( $wgRequest );
+       } else if( $fileid || $image ) {
+               $form->showImages( $wgRequest );
+       } else if( $logid ) {
+               $form->showEvents( $wgRequest );
+       }
+       # Show relevant lines from the deletion log
+       # This will show even if said ID does not exist...might be helpful
+       if( !is_null($page) ) {
+               $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
+               $logViewer = new LogViewer(
+                       new LogReader(
+                               new FauxRequest(
+                                       array( 'page' => $page->getPrefixedText(), 'type' => 'delete' ) ) ) );
+               $logViewer->showList( $wgOut );
        }
 }
 
@@ -42,54 +62,410 @@ function wfSpecialRevisiondelete( $par = null ) {
 class RevisionDeleteForm {
        /**
         * @param Title $page
-        * @param int $oldid
+        * @param array $oldids
+        * @param array $logids
+        * @param array $artimestamps
+        * @param array $fileids
+        * @param array $oldimages
+     * @param string $file
         */
-       function __construct( $request ) {
+       function __construct( $page, $oldids=null, $logids=null, $artimestamps=null, $fileids=null, $oldimages=null, $file=null ) {
                global $wgUser;
-               
-               $target = $request->getVal( 'target' );
-               $this->page = Title::newFromUrl( $target );
-               
-               $this->revisions = $request->getIntArray( 'oldid', array() );
-               
+
+               $this->page = $page;
                $this->skin = $wgUser->getSkin();
+               
+               // For reviewing deleted files
+               if( $file ) {
+                       $oimage = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $page, $file );
+                       $oimage->load();
+                       // Check if user is allowed to see this file
+                       if( !$oimage->userCan(File::DELETED_FILE) ) {
+                               $wgOut->permissionRequired( 'hiderevision' ); 
+                               return false;
+                       } else {
+                               return $this->showFile( $file );
+                       }
+               }
+               // At this point, we should only have one of these
+               if( $oldids ) {
+                       $this->revisions = $oldids;
+                       $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT );
+                       $this->deletetype='oldid';
+               } else if( $artimestamps ) {
+                       $this->archrevs = $artimestamps;
+                       $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT );
+                       $this->deletetype='artimestamp';
+               } else if( $oldimages ) {
+                       $this->ofiles = $oldimages;
+                       $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE );
+                       $this->deletetype='oldimage';
+               } else if( $fileids ) {
+                       $this->afiles = $fileids;
+                       $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE );
+                       $this->deletetype='fileid';
+               } else if( $logids ) {
+                       $this->events = $logids;
+                       $hide_content_name = array( 'revdelete-hide-name', 'wpHideName', LogViewer::DELETED_ACTION );
+                       $this->deletetype='logid';
+               }
+               // Our checkbox messages depends one what we are doing, 
+               // e.g. we don't hide "text" for logs or images
                $this->checks = array(
-                       array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT ),
+                       $hide_content_name,
                        array( 'revdelete-hide-comment', 'wpHideComment', Revision::DELETED_COMMENT ),
                        array( 'revdelete-hide-user', 'wpHideUser', Revision::DELETED_USER ),
                        array( 'revdelete-hide-restricted', 'wpHideRestricted', Revision::DELETED_RESTRICTED ) );
        }
        
        /**
+        * Show a deleted file version requested by the visitor.
+        */
+       function showFile( $key ) {
+               global $wgOut, $wgRequest;
+               $wgOut->disable();
+               
+               # We mustn't allow the output to be Squid cached, otherwise
+               # if an admin previews a deleted image, and it's cached, then
+               # a user without appropriate permissions can toddle off and
+               # nab the image, and Squid will serve it
+               $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' );
+               $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' );
+               $wgRequest->response()->header( 'Pragma: no-cache' );
+               
+               $store = FileStore::get( 'hidden' );
+               $store->stream( $key );
+       }
+       
+       /**
+        * This lets a user set restrictions for live and archived revisions
+        * @param WebRequest $request
+        */
+       function showRevs( $request ) {
+               global $wgOut, $wgUser, $action;
+
+               $UserAllowed = true;
+               
+               $count = ($this->deletetype=='oldid') ? 
+                       count($this->revisions) : count($this->archrevs);
+               $wgOut->addWikiText( wfMsgExt( 'revdelete-selected', array('parsemag'), 
+                       $this->page->getPrefixedText(), $count ) );
+               
+               $bitfields = 0;
+               $wgOut->addHtml( "<ul>" );
+               
+               $where = $revObjs = array();
+               $dbr = wfGetDB( DB_SLAVE );
+               // Live revisions...
+               if( $this->deletetype=='oldid' ) {
+                       // Run through and pull all our data in one query
+                       foreach( $this->revisions as $revid ) {
+                               $where[] = intval($revid);
+                       }
+                       $whereClause = 'rev_id IN(' . implode(',',$where) . ')';
+                       $result = $dbr->select( 'revision', '*',
+                               array( 'rev_page' => $this->page->getArticleID(), 
+                                       $whereClause ),
+                               __METHOD__ );
+                       while( $row = $dbr->fetchObject( $result ) ) {
+                               $revObjs[$row->rev_id] = new Revision( $row );
+                       }
+                       foreach( $this->revisions as $revid ) {
+                               // Hiding top revisison is bad
+                               if( !is_object($revObjs[$revid]) || $revObjs[$revid]->isCurrent() ) {
+                                       $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+                                       return;
+                               } else if( !$revObjs[$revid]->userCan(Revision::DELETED_RESTRICTED) ) {
+                               // If a rev is hidden from sysops
+                                       if( $action != 'submit') {
+                                               $wgOut->permissionRequired( 'hiderevision' ); 
+                                               return;
+                                       }
+                                       $UserAllowed = false;
+                               }
+                               $wgOut->addHtml( $this->historyLine( $revObjs[$revid] ) );
+                               $bitfields |= $revObjs[$revid]->mDeleted;
+                       }
+               // The archives...
+               } else {
+                       // Run through and pull all our data in one query
+                       foreach( $this->archrevs as $timestamp ) {
+                               $where[] = $dbr->addQuotes( $timestamp );
+                       }
+                       $whereClause = 'ar_timestamp IN(' . implode(',',$where) . ')';
+                       $result = $dbr->select( 'archive', '*',
+                               array( 'ar_namespace' => $this->page->getNamespace(),
+                                       'ar_title' => $this->page->getDBKey(), 
+                                               $whereClause ),
+                               __METHOD__ );
+                       while( $row = $dbr->fetchObject( $result ) ) {
+                               $revObjs[$row->ar_timestamp] = new Revision( array(
+                               'page'       => $this->page->getArticleId(),
+                               'id'         => $row->ar_rev_id,
+                               'text'       => $row->ar_text_id,
+                               'comment'    => $row->ar_comment,
+                               'user'       => $row->ar_user,
+                               'user_text'  => $row->ar_user_text,
+                               'timestamp'  => $row->ar_timestamp,
+                               'minor_edit' => $row->ar_minor_edit,
+                               'text_id'    => $row->ar_text_id,
+                               'deleted'    => $row->ar_deleted,
+                               'len'        => $row->ar_len) );
+                       }
+                       foreach( $this->archrevs as $timestamp ) {
+                               if( !is_object($revObjs[$timestamp]) ) {
+                                       $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+                                       return;
+                               }
+                       }
+                       foreach( $revObjs as $rev ) {
+                               if( !$rev->userCan(Revision::DELETED_RESTRICTED) ) {
+                               //if a rev is hidden from sysops
+                                       if( $action != 'submit') {
+                                               $wgOut->permissionRequired( 'hiderevision' ); 
+                                               return;
+                                       }
+                                       $UserAllowed = false;
+                               }
+                               $wgOut->addHtml( $this->historyLine( $rev ) );
+                               $bitfields |= $rev->mDeleted;
+                       }
+               } 
+               $wgOut->addHtml( "</ul>" );
+               
+               $wgOut->addWikiText( wfMsgHtml( 'revdelete-text' ) );
+               //Normal sysops can always see what they did, but can't always change it
+               if( !$UserAllowed ) return;
+               
+               $items = array(
+                       wfInputLabel( wfMsgHtml( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
+                       wfSubmitButton( wfMsgHtml( 'revdelete-submit' ) ) );
+               $hidden = array(
+                       wfHidden( 'wpEditToken', $wgUser->editToken() ),
+                       wfHidden( 'target', $this->page->getPrefixedText() ),
+                       wfHidden( 'type', $this->deletetype ) );
+               if( $this->deletetype=='oldid' ) {
+                       foreach( $revObjs as $rev )
+                               $hidden[] = wfHidden( 'oldid[]', $rev->getID() );
+               } else {        
+                       foreach( $revObjs as $rev )
+                               $hidden[] = wfHidden( 'artimestamp[]', $rev->getTimestamp() );
+               }
+               $special = SpecialPage::getTitleFor( 'Revisiondelete' );
+               $wgOut->addHtml( wfElement( 'form', array(
+                       'method' => 'post',
+                       'action' => $special->getLocalUrl( 'action=submit' ) ),
+                       null ) );
+               
+               $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'revdelete-legend' ) . '</legend>' );
+               // FIXME: all items checked for just one rev are checked, even if not set for the others
+               foreach( $this->checks as $item ) {
+                       list( $message, $name, $field ) = $item;
+                       $wgOut->addHtml( "<div>" .
+                               wfCheckLabel( wfMsgHtml( $message), $name, $name, $bitfields & $field ) .
+                               "</div>\n" );
+               }
+               $wgOut->addHtml( '</fieldset>' );
+               foreach( $items as $item ) {
+                       $wgOut->addHtml( '<p>' . $item . '</p>' );
+               }
+               foreach( $hidden as $item ) {
+                       $wgOut->addHtml( $item );
+               }
+               
+               $wgOut->addHtml( '</form>' );
+       }
+
+       /**
+        * This lets a user set restrictions for archived images
+        * @param WebRequest $request
+        */
+       function showImages( $request ) {
+               global $wgOut, $wgUser, $action;
+
+               $UserAllowed = true;
+               
+               $count = ($this->deletetype=='oldimage') ? count($this->ofiles) : count($this->afiles);
+               $wgOut->addWikiText( wfMsgExt( 'revdelete-selected', array('parsemag'), $this->page->getPrefixedText(), $count ) );
+               
+               $bitfields = 0;
+               $wgOut->addHtml( "<ul>" );
+               
+               $where = $filesObjs = array();
+               $dbr = wfGetDB( DB_SLAVE );
+               // Live old revisions...
+               if( $this->deletetype=='oldimage' ) {
+                       // Run through and pull all our data in one query
+                       foreach( $this->ofiles as $timestamp ) {
+                               $where[] = $dbr->addQuotes( $timestamp.'!'.$this->page->getDbKey() );
+                       }
+                       $whereClause = 'oi_archive_name IN(' . implode(',',$where) . ')';
+                       $result = $dbr->select( 'oldimage', '*',
+                               array( 'oi_name' => $this->page->getDbKey(),
+                                       $whereClause ),
+                               __METHOD__ );
+                       while( $row = $dbr->fetchObject( $result ) ) {
+                               $filesObjs[$row->oi_archive_name] = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row );
+                               $filesObjs[$row->oi_archive_name]->user = $row->oi_user;
+                               $filesObjs[$row->oi_archive_name]->userText = $row->oi_user_text;
+                       }
+                       // Check through our images
+                       foreach( $this->ofiles as $timestamp ) {
+                               $archivename = $timestamp.'!'.$this->page->getDbKey();
+                               if( !isset($filesObjs[$archivename]) ) {
+                                       $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+                                       return;
+                               }
+                       }
+                       foreach( $filesObjs as $file ) {
+                               if( !isset($file) ) {
+                                       $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+                                       return;
+                               } else if( !$file->userCan(File::DELETED_RESTRICTED) ) {
+                                       // If a rev is hidden from sysops
+                                       if( $action != 'submit' ) {
+                                               $wgOut->permissionRequired( 'hiderevision' );
+                                               return;
+                                       }
+                                       $UserAllowed = false;
+                               }
+                               // Inject history info
+                               $wgOut->addHtml( $this->uploadLine( $file ) );
+                               $bitfields |= $file->deleted;
+                       }       
+               // Archived files...
+               } else {
+                       // Run through and pull all our data in one query
+                       foreach( $this->afiles as $id ) {
+                               $where[] = intval($id);
+                       }
+                       $whereClause = 'fa_id IN(' . implode(',',$where) . ')';
+                       $result = $dbr->select( 'filearchive', '*',
+                               array( 'fa_name' => $this->page->getDbKey(),
+                                       $whereClause ),
+                               __METHOD__ );
+                       while( $row = $dbr->fetchObject( $result ) ) {
+                               $filesObjs[$row->fa_id] = ArchivedFile::newFromRow( $row );
+                       }
+                       
+                       foreach( $this->afiles as $fileid ) {
+                               if( !isset($filesObjs[$fileid]) ) {
+                                       $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
+                                       return;
+                               } else if( !$filesObjs[$fileid]->userCan(File::DELETED_RESTRICTED) ) {
+                                       // If a rev is hidden from sysops
+                                       if( $action != 'submit' ) {
+                                               $wgOut->permissionRequired( 'hiderevision' );
+                                               return;
+                                       }
+                                       $UserAllowed = false;
+                               }
+                               // Inject history info
+                               $wgOut->addHtml( $this->uploadLine( $filesObjs[$fileid] ) );
+                               $bitfields |= $filesObjs[$fileid]->deleted;
+                       }
+               }
+               $wgOut->addHtml( "</ul>" );
+               
+               $wgOut->addWikiText( wfMsgHtml( 'revdelete-text' ) );
+               //Normal sysops can always see what they did, but can't always change it
+               if( !$UserAllowed ) return;
+               
+               $items = array(
+                       wfInputLabel( wfMsgHtml( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
+                       wfSubmitButton( wfMsgHtml( 'revdelete-submit' ) ) );
+               $hidden = array(
+                       wfHidden( 'wpEditToken', $wgUser->editToken() ),
+                       wfHidden( 'target', $this->page->getPrefixedText() ),
+                       wfHidden( 'type', $this->deletetype ) );
+               if( $this->deletetype=='oldimage' ) {
+                       foreach( $this->ofiles as $filename )
+                               $hidden[] = wfHidden( 'oldimage[]', $filename );
+               } else {
+                       foreach( $this->afiles as $fileid )
+                               $hidden[] = wfHidden( 'fileid[]', $fileid );
+               }
+               $special = SpecialPage::getTitleFor( 'Revisiondelete' );
+               $wgOut->addHtml( wfElement( 'form', array(
+                       'method' => 'post',
+                       'action' => $special->getLocalUrl( 'action=submit' ) ),
+                       null ) );
+               
+               $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'revdelete-legend' ) . '</legend>' );
+               // FIXME: all items checked for just one file are checked, even if not set for the others
+               foreach( $this->checks as $item ) {
+                       list( $message, $name, $field ) = $item;
+                       $wgOut->addHtml( '<div>' .
+                               wfCheckLabel( wfMsgHtml( $message), $name, $name, $bitfields & $field ) .
+                               '</div>' );
+               }
+               $wgOut->addHtml( '</fieldset>' );
+               foreach( $items as $item ) {
+                       $wgOut->addHtml( '<p>' . $item . '</p>' );
+               }
+               foreach( $hidden as $item ) {
+                       $wgOut->addHtml( $item );
+               }
+               
+               $wgOut->addHtml( '</form>' );
+       }
+               
+       /**
+        * This lets a user set restrictions for log items
         * @param WebRequest $request
         */
-       function show( $request ) {
-               global $wgOut, $wgUser;
+       function showEvents( $request ) {
+               global $wgOut, $wgUser, $action;
 
-               $wgOut->addWikiText( wfMsg( 'revdelete-selected', $this->page->getPrefixedText() ) );
+               $UserAllowed = true;
+               $wgOut->addWikiText( wfMsgExt( 'logdelete-selected', array('parsemag'), count($this->events) ) );
                
+               $bitfields = 0;
                $wgOut->addHtml( "<ul>" );
-               foreach( $this->revisions as $revid ) {
-                       $rev = Revision::newFromTitle( $this->page, $revid );
-                       if( !isset( $rev ) ) {
+               
+               $where = $logRows = array();
+               $dbr = wfGetDB( DB_SLAVE );
+               // Run through and pull all our data in one query
+               foreach( $this->events as $logid ) {
+                       $where[] = intval($logid);
+               }
+               $whereClause = 'log_id IN(' . implode(',',$where) . ')';
+               $result = $dbr->select( 'logging', '*', 
+                       array( $whereClause ),
+                       __METHOD__ );
+               while( $row = $dbr->fetchObject( $result ) ) {
+                       $logRows[$row->log_id] = $row;
+               }
+               foreach( $this->events as $logid ) {
+                       // Don't hide from oversight log!!!
+                       if( !isset( $logRows[$logid] ) || $logRows[$logid]->log_type=='oversight' ) {
                                $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
                                return;
+                       } else if( !LogViewer::userCan( $logRows[$logid],Revision::DELETED_RESTRICTED) ) {
+                       // If an event is hidden from sysops
+                               if( $action != 'submit') {
+                                       $wgOut->permissionRequired( 'hiderevision' );
+                                       return;
+                               }
+                               $UserAllowed = false;
                        }
-                       $wgOut->addHtml( $this->historyLine( $rev ) );
-                       $bitfields[] = $rev->mDeleted; // FIXME
+                       $wgOut->addHtml( $this->logLine( $logRows[$logid] ) );
+                       $bitfields |= $logRows[$logid]->log_deleted;
                }
                $wgOut->addHtml( "</ul>" );
-       
-               $wgOut->addWikiText( wfMsg( 'revdelete-text' ) );
+
+               $wgOut->addWikiText( wfMsgHtml( 'revdelete-text' ) );
+               //Normal sysops can always see what they did, but can't always change it
+               if( !$UserAllowed ) return;
                
                $items = array(
-                       wfInputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
-                       wfSubmitButton( wfMsg( 'revdelete-submit' ) ) );
+                       wfInputLabel( wfMsgHtml( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
+                       wfSubmitButton( wfMsgHtml( 'revdelete-submit' ) ) );
                $hidden = array(
                        wfHidden( 'wpEditToken', $wgUser->editToken() ),
-                       wfHidden( 'target', $this->page->getPrefixedText() ) );
-               foreach( $this->revisions as $revid ) {
-                       $hidden[] = wfHidden( 'oldid[]', $revid );
+                       wfHidden( 'type', $this->deletetype ) );
+               foreach( $this->events as $logid ) {
+                       $hidden[] = wfHidden( 'logid[]', $logid );
                }
                
                $special = SpecialPage::getTitleFor( 'Revisiondelete' );
@@ -99,10 +475,11 @@ class RevisionDeleteForm {
                        null ) );
                
                $wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'revdelete-legend' ) . '</legend>' );
+               // FIXME: all items checked for just on event are checked, even if not set for the others
                foreach( $this->checks as $item ) {
                        list( $message, $name, $field ) = $item;
                        $wgOut->addHtml( '<div>' .
-                               wfCheckLabel( wfMsg( $message), $name, $name, $rev->isDeleted( $field ) ) .
+                               wfCheckLabel( wfMsgHtml( $message), $name, $name, $bitfields & $field ) .
                                '</div>' );
                }
                $wgOut->addHtml( '</fieldset>' );
@@ -123,14 +500,157 @@ class RevisionDeleteForm {
        function historyLine( $rev ) {
                global $wgContLang;
                $date = $wgContLang->timeanddate( $rev->getTimestamp() );
+               
+               $difflink=''; $del = '';
+               // Live revisions
+               if( $this->deletetype=='oldid' ) {
+                       $difflink = '(' . $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml('diff'), 
+                               'diff=' . $rev->getId() . '&oldid=prev' ) . ')';
+                       $revlink = $this->skin->makeLinkObj( $this->page, $date, 'oldid=' . $rev->getId() );
+               } else {
+               // Archived revisions
+                       $undelete = SpecialPage::getTitleFor( 'Undelete' );
+                       $target = $this->page->getPrefixedText();
+                       $revlink = $this->skin->makeLinkObj( $undelete, $date, "target=$target&timestamp=" . $rev->getTimestamp() );
+               }
+       
+               if( $rev->isDeleted(Revision::DELETED_TEXT) ) {
+                       $revlink = '<span class="history-deleted">'.$revlink.'</span>';
+                       $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
+                       if( !$rev->userCan(Revision::DELETED_TEXT) ) {
+                               $revlink = '<span class="history-deleted">'.$date.'</span>';
+                       }
+               }
+               
                return
-                       "<li>" .
-                       $this->skin->makeLinkObj( $this->page, $date, 'oldid=' . $rev->getId() ) .
-                       " " .
-                       $this->skin->revUserLink( $rev ) .
-                       " " .
-                       $this->skin->revComment( $rev ) .
-                       "</li>";
+                       "<li> $difflink $revlink " . $this->skin->revUserLink( $rev ) . " " . $this->skin->revComment( $rev ) . "$del</li>";
+       }
+       
+       /**
+        * @param File $file
+        * This can work for old or archived revisions
+        * @returns string
+        */     
+       function uploadLine( $file ) {
+               global $wgContLang, $wgTitle;
+               
+               $target = $this->page->getPrefixedText();
+               $date = $wgContLang->timeanddate( $file->timestamp, true  );
+       
+               $del = '';
+               // Special:Undelete for viewing archived images
+               if( $this->deletetype=='fileid' ) {
+                       $undelete = SpecialPage::getTitleFor( 'Undelete' );
+                       $pageLink = $this->skin->makeKnownLinkObj( $undelete, $date, "target=$target&file=$file->key" );
+               // Revisiondelete for viewing images
+               } else {
+                       # Hidden files...
+                       if( $file->isDeleted(File::DELETED_FILE) ) {
+                               $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
+                               if( !$file->userCan(File::DELETED_FILE) ) {
+                                       $pageLink = $date;
+                               } else {
+                                       $pageLink = $this->skin->makeKnownLinkObj( $wgTitle, $date, 
+                                               "target=$target&file=$file->sha1.".$file->getExtension() );
+                               }
+                               $pageLink = '<span class="history-deleted">' . $pageLink . '</span>';
+                       # Regular files...
+                       } else {
+                               $url = $file->getUrlRel();
+                               $pageLink = "<a href=\"{$url}\">{$date}</a>";
+                       }
+               }
+               
+               $data = wfMsgHtml( 'widthheight',
+                                       $wgContLang->formatNum( $file->width ),
+                                       $wgContLang->formatNum( $file->height ) ) .
+                       ' (' . wfMsgHtml( 'nbytes', $wgContLang->formatNum( $file->size ) ) . ')';      
+       
+               return "<li> $pageLink " . $this->fileUserLink( $file ) . " $data " . $this->fileComment( $file ) . "$del</li>";
+       }
+       
+       /**
+        * @param Array $event row
+        * @returns string
+        */
+       function logLine( $event ) {
+               global $wgContLang;
+
+               $date = $wgContLang->timeanddate( $event->log_timestamp );
+               $paramArray = LogPage::extractParams( $event->log_params );
+
+               if( !LogViewer::userCan($event,LogViewer::DELETED_ACTION) ) {
+                       $action = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';        
+               } else {
+                       $title = Title::makeTitle( $event->log_namespace, $event->log_title );
+                       $action = LogPage::actionText( $event->log_type, $event->log_action, $title, $this->skin, $paramArray, true, true );
+                       if( $event->log_deleted & LogViewer::DELETED_ACTION )
+                               $action = '<span class="history-deleted">' . $action . '</span>';
+               }
+               return
+                       "<li>$date" . " " . $this->skin->logUserLink( $event ) . " $action " . $this->skin->logComment( $event ) . "</li>";
+       }
+       
+       /**
+        * Generate a user link if the current user is allowed to view it
+        * @param ArchivedFile $file
+        * @param $isPublic, bool, show only if all users can see it
+        * @return string HTML
+        */
+       function fileUserLink( $file, $isPublic = false ) {
+               if( $file->isDeleted( File::DELETED_USER ) && $isPublic ) {
+                       $link = wfMsgHtml( 'rev-deleted-user' );
+               } else if( $file->userCan( File::DELETED_USER ) ) {
+                       $link = $this->skin->userLink( $file->user, $file->userText );
+               } else {
+                       $link = wfMsgHtml( 'rev-deleted-user' );
+               }
+               if( $file->isDeleted( File::DELETED_USER ) ) {
+                       return '<span class="history-deleted">' . $link . '</span>';
+               }
+               return $link;
+       }
+       
+       /**
+        * Generate a user tool link cluster if the current user is allowed to view it
+        * @param ArchivedFile $file
+        * @param $isPublic, bool, show only if all users can see it
+        * @return string HTML
+        */
+       function fileUserTools( $file, $isPublic = false ) {
+               if( $file->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
+                       $link = wfMsgHtml( 'rev-deleted-user' );
+               } else if( $file->userCan( Revision::DELETED_USER ) ) {
+                       $link = $this->skin->userLink( $file->user, $file->userText ) .
+                       $this->userToolLinks( $file->user, $file->userText );
+               } else {
+                       $link = wfMsgHtml( 'rev-deleted-user' );
+               }
+               if( $file->isDeleted( Revision::DELETED_USER ) ) {
+                       return '<span class="history-deleted">' . $link . '</span>';
+               }
+               return $link;
+       }
+       
+       /**
+        * Wrap and format the given file's comment block, if the current
+        * user is allowed to view it.
+        *
+        * @param ArchivedFile $file
+        * @return string HTML
+        */
+       function fileComment( $file, $isPublic = false ) {
+               if( $file->isDeleted( File::DELETED_COMMENT ) && $isPublic ) {
+                       $block = ' ' . wfMsgHtml( 'rev-deleted-comment' );
+               } else if( $file->userCan( File::DELETED_COMMENT ) ) {
+                       $block = $this->skin->commentBlock( $file->description );
+               } else {
+                       $block = ' ' . wfMsgHtml( 'rev-deleted-comment' );
+               }
+               if( $file->isDeleted( File::DELETED_COMMENT ) ) {
+                       return "<span class=\"history-deleted\">$block</span>";
+               }
+               return $block;
        }
        
        /**
@@ -139,16 +659,52 @@ class RevisionDeleteForm {
        function submit( $request ) {
                $bitfield = $this->extractBitfield( $request );
                $comment = $request->getText( 'wpReason' );
-               if( $this->save( $bitfield, $comment ) ) {
-                       return $this->success( $request );
-               } else {
-                       return $this->show( $request );
+               
+               $this->target = $request->getText( 'target' );
+               $this->title = Title::newFromURL( $this->target );
+               
+               if( $this->save( $bitfield, $comment, $this->title ) ) {
+                       $this->success( $request );
+               } else if( $request->getCheck( 'oldid' ) || $request->getCheck( 'artimestamp' ) ) {
+                       return $this->showRevs( $request );
+               } else if( $request->getCheck( 'logid' ) ) {
+                       return $this->showLogs( $request );
+               } else if( $request->getCheck( 'oldimage' ) || $request->getCheck( 'fileid' ) ) {
+                       return $this->showImages( $request );
                }
        }
        
        function success( $request ) {
                global $wgOut;
-               $wgOut->addWikiText( 'woo' );
+               
+               $wgOut->setPagetitle( wfMsgHtml( 'actioncomplete' ) );
+               # Give a link to the log for this page
+               $logtitle = SpecialPage::getTitleFor( 'Log' );
+        $loglink = $this->skin->makeKnownLinkObj( $logtitle, wfMsgHtml( 'viewpagelogs' ),
+                       wfArrayToCGI( array('page' => $this->target ) ) );
+               # Give a link to the page history       
+               $histlink = $this->skin->makeKnownLinkObj( $this->title, wfMsgHtml( 'revhistory' ),
+                       wfArrayToCGI( array('action' => 'history' ) ) );
+               # Link to deleted edits
+               $undelete = SpecialPage::getTitleFor( 'Undelete' );
+               $dellink = $this->skin->makeKnownLinkObj( $undelete, wfMsgHtml( 'undeleterevs' ),
+                       wfArrayToCGI( array('target' => $this->target) ) );
+               # Logs themselves don't have histories or archived revisions
+               if( !is_null($this->title) && $this->title->getNamespace() > -1)
+                       $wgOut->setSubtitle( '<p>'.$histlink.' / '.$loglink.' / '.$dellink.'</p>' );
+               
+               if( $this->deletetype=='logid' ) {
+                       $wgOut->addWikiText( wfMsgHtml('logdelete-success'), false );
+                       $this->showEvents( $request );
+               } else if( $this->deletetype=='oldid' || $this->deletetype=='artimestamp' ) {
+                       $wgOut->addWikiText( wfMsgHtml('revdelete-success'), false );
+                       $this->showRevs( $request );
+               } else if( $this->deletetype=='fileid' ) {
+                       $wgOut->addWikiText( wfMsgHtml('revdelete-success'), false );
+                       $this->showImages( $request );
+               } else if( $this->deletetype=='oldimage' ) {
+                       $this->showImages( $request );
+               }
        }
        
        /**
@@ -167,10 +723,26 @@ class RevisionDeleteForm {
                return $bitfield;
        }
        
-       function save( $bitfield, $reason ) {
+       function save( $bitfield, $reason, $title ) {
                $dbw = wfGetDB( DB_MASTER );
+               
+               // Don't allow simply locking the interface for no reason
+               if( $bitfield == Revision::DELETED_RESTRICTED )
+                       $bitfield = 0;
+               
                $deleter = new RevisionDeleter( $dbw );
-               $deleter->setVisibility( $this->revisions, $bitfield, $reason );
+               // By this point, only one of the below should be set
+               if( isset($this->revisions) ) {
+                       return $deleter->setRevVisibility( $title, $this->revisions, $bitfield, $reason );
+               } else if( isset($this->archrevs) ) {
+                       return $deleter->setArchiveVisibility( $title, $this->archrevs, $bitfield, $reason );
+               } else if( isset($this->ofiles) ) {
+                       return $deleter->setOldImgVisibility( $title, $this->ofiles, $bitfield, $reason );
+               } else if( isset($this->afiles) ) {
+                       return $deleter->setArchFileVisibility( $title, $this->afiles, $bitfield, $reason );
+               } else if( isset($this->events) ) {
+                       return $deleter->setEventVisibility( $this->events, $bitfield, $reason );
+               }
        }
 }
 
@@ -180,42 +752,510 @@ class RevisionDeleteForm {
  */
 class RevisionDeleter {
        function __construct( $db ) {
-               $this->db = $db;
+               $this->dbw = $db;
        }
        
        /**
+        * @param $title, the page these events apply to
         * @param array $items list of revision ID numbers
         * @param int $bitfield new rev_deleted value
         * @param string $comment Comment for log records
         */
-       function setVisibility( $items, $bitfield, $comment ) {
-               $pages = array();
+       function setRevVisibility( $title, $items, $bitfield, $comment ) {
+               global $wgOut;
                
+               $userAllowedAll = $success = true;
+               $revIDs = array();
+               $revCount = 0;
+               // Run through and pull all our data in one query
+               foreach( $items as $revid ) {
+                       $where[] = intval($revid);
+               }
+               $whereClause = 'rev_id IN(' . implode(',',$where) . ')';
+               $result = $this->dbw->select( 'revision', '*',
+                       array( 'rev_page' => $title->getArticleID(), 
+                               $whereClause ),
+                       __METHOD__ );
+               while( $row = $this->dbw->fetchObject( $result ) ) {
+                       $revObjs[$row->rev_id] = new Revision( $row );
+               }
                // To work!
                foreach( $items as $revid ) {
-                       $rev = Revision::newFromId( $revid );
-                       if( !isset( $rev ) ) {
-                               return false;
+                       if( !isset($revObjs[$revid]) || $revObjs[$revid]->isCurrent() ) {
+                               $success = false;
+                               continue; // Must exist
+                       } else if( !$revObjs[$revid]->userCan(Revision::DELETED_RESTRICTED) ) {
+                       $userAllowedAll=false; 
+                               continue;
+                       }
+                       // For logging, maintain a count of revisions
+                       if( $revObjs[$revid]->mDeleted != $bitfield ) {
+                               $revCount++;
+                               $revIDs[]=$revid;
+                               
+                               $this->updateRevision( $revObjs[$revid], $bitfield );
+                               $this->updateRecentChangesEdits( $revObjs[$revid], $bitfield, false );
+                       }
+               }
+               // Clear caches...
+               // Don't log or touch if nothing changed
+               if( $revCount > 0 ) {
+                       $this->updatePage( $title );
+                       $this->updateLog( $title, $revCount, $bitfield, $revObjs[$revid]->mDeleted, 
+                               $comment, $title, 'oldid', $revIDs );
+               }
+               // Where all revs allowed to be set?
+               if( !$userAllowedAll ) {
+                       //FIXME: still might be confusing???
+                       $wgOut->permissionRequired( 'hiderevision' );
+                       return false;
+               }
+               
+               return $success;
+       }
+       
+        /**
+        * @param $title, the page these events apply to
+        * @param array $items list of revision ID numbers
+        * @param int $bitfield new rev_deleted value
+        * @param string $comment Comment for log records
+        */
+       function setArchiveVisibility( $title, $items, $bitfield, $comment ) {
+               global $wgOut;
+               
+               $userAllowedAll = $success = true;
+               $count = 0; 
+               $Id_set = array();
+               // Run through and pull all our data in one query
+               foreach( $items as $timestamp ) {
+                       $where[] = $this->dbw->addQuotes( $timestamp );
+               }
+               $whereClause = 'ar_timestamp IN(' . implode(',',$where) . ')';
+               $result = $this->dbw->select( 'archive', '*',
+                       array( 'ar_namespace' => $title->getNamespace(),
+                               'ar_title' => $title->getDBKey(), 
+                                       $whereClause ),
+                       __METHOD__ );
+               while( $row = $this->dbw->fetchObject( $result ) ) {
+                       $revObjs[$row->ar_timestamp] = new Revision( array(
+                       'page'       => $title->getArticleId(),
+                       'id'         => $row->ar_rev_id,
+                       'text'       => $row->ar_text_id,
+                       'comment'    => $row->ar_comment,
+                       'user'       => $row->ar_user,
+                       'user_text'  => $row->ar_user_text,
+                       'timestamp'  => $row->ar_timestamp,
+                       'minor_edit' => $row->ar_minor_edit,
+                       'text_id'    => $row->ar_text_id,
+                       'deleted'    => $row->ar_deleted,
+                       'len'        => $row->ar_len) );
+               }
+               // To work!
+               foreach( $items as $timestamp ) {
+                       // This will only select the first revision with this timestamp.
+                       // Since they are all selected/deleted at once, we can just check the 
+                       // permissions of one. UPDATE is done via timestamp, so all revs are set.
+                       if( !is_object($revObjs[$timestamp]) ) {
+                               $success = false;
+                               continue; // Must exist
+                       } else if( !$revObjs[$timestamp]->userCan(Revision::DELETED_RESTRICTED) ) {
+                       $userAllowedAll=false;
+                               continue;
+                       }
+                       // Which revisions did we change anything about?
+                       if( $revObjs[$timestamp]->mDeleted != $bitfield ) {
+                          $Id_set[]=$timestamp;
+                          $count++;
+                          
+                          $this->updateArchive( $revObjs[$timestamp], $bitfield );
+                       }
+               }
+               // For logging, maintain a count of revisions
+               if( $count > 0 ) {
+                       $this->updateLog( $title, $count, $bitfield, $revObjs[$timestamp]->mDeleted, 
+                               $comment, $title, 'artimestamp', $Id_set );
+               }
+               // Where all revs allowed to be set?
+               if( !$userAllowedAll ) {
+                       $wgOut->permissionRequired( 'hiderevision' ); 
+                       return false;
+               }
+               
+               return $success;
+       }
+       
+        /**
+        * @param $title, the page these events apply to
+        * @param array $items list of revision ID numbers
+        * @param int $bitfield new rev_deleted value
+        * @param string $comment Comment for log records
+        */
+       function setOldImgVisibility( $title, $items, $bitfield, $comment ) {
+               global $wgOut;
+               
+               $userAllowedAll = $success = true;
+               $count = 0; 
+               $set = array();
+               // Run through and pull all our data in one query
+               foreach( $items as $timestamp ) {
+                       $where[] = $this->dbw->addQuotes( $timestamp.'!'.$title->getDbKey() );
+               }
+               $whereClause = 'oi_archive_name IN(' . implode(',',$where) . ')';
+               $result = $this->dbw->select( 'oldimage', '*',
+                       array( 'oi_name' => $title->getDbKey(),
+                               $whereClause ),
+                       __METHOD__ );
+               while( $row = $this->dbw->fetchObject( $result ) ) {
+                       $filesObjs[$row->oi_archive_name] = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row );
+                       $filesObjs[$row->oi_archive_name]->user = $row->oi_user;
+                       $filesObjs[$row->oi_archive_name]->userText = $row->oi_user_text;
+               }
+               // To work!
+               foreach( $items as $timestamp ) {
+                       $archivename = $timestamp.'!'.$title->getDbKey();
+                       if( !isset($filesObjs[$archivename]) ) {
+                               $success = false;
+                               continue; // Must exist
+                       } else if( !$filesObjs[$archivename]->userCan(File::DELETED_RESTRICTED) ) {
+                       $userAllowedAll=false;
+                               continue;
                        }
-                       $this->updateRevision( $rev, $bitfield );
-                       $this->updateRecentChanges( $rev, $bitfield );
                        
-                       // For logging, maintain a count of revisions per page
-                       $pageid = $rev->getPage();
-                       if( isset( $pages[$pageid] ) ) {
-                               $pages[$pageid]++;
+                       $transaction = true;
+                       // Which revisions did we change anything about?
+                       if( $filesObjs[$archivename]->deleted != $bitfield ) {
+                               $count++;
+                               
+                               $this->dbw->begin();
+                               $this->updateOldFiles( $filesObjs[$archivename], $bitfield );
+                               // If this image is currently hidden...
+                               if( $filesObjs[$archivename]->deleted & File::DELETED_FILE ) {
+                                       if( $bitfield & File::DELETED_FILE ) {
+                                               # Leave it alone if we are not changing this...
+                                               $set[]=$name;
+                                               $transaction = true;
+                                       } else {
+                                               # We are moving this out
+                                               $transaction = $this->makeOldImagePublic( $filesObjs[$archivename] );
+                                               $set[]=$transaction;
+                                       }
+                               // Is it just now becoming hidden?
+                               } else if( $bitfield & File::DELETED_FILE ) {
+                                       $transaction = $this->makeOldImagePrivate( $filesObjs[$archivename] );
+                                       $set[]=$transaction;
+                               } else {
+                                       $set[]=$name;
+                               }
+                               // If our file operations fail, then revert back the db
+                               if( $transaction==false ) {
+                                       $this->dbw->rollback();
+                                       return false;
+                               }
+                               $this->dbw->commit();
+                               // Purge page/history
+                               $filesObjs[$archivename]->purgeCache();
+                               $filesObjs[$archivename]->purgeHistory();
+                               // Invalidate cache for all pages using this file
+                               $update = new HTMLCacheUpdate( $oimage->getTitle(), 'imagelinks' );
+                               $update->doUpdate();
+                       }
+               }
+               
+               // Log if something was changed
+               if( $count > 0 ) {
+                       $this->updateLog( $title, $count, $bitfield, $filesObjs[$archivename]->deleted, 
+                               $comment, $title, 'oldimage', $set );
+               }
+               // Where all revs allowed to be set?
+               if( !$userAllowedAll ) {
+                       $wgOut->permissionRequired( 'hiderevision' ); 
+                       return false;
+               }
+               
+               return $success;
+       }
+       
+        /**
+        * @param $title, the page these events apply to
+        * @param array $items list of revision ID numbers
+        * @param int $bitfield new rev_deleted value
+        * @param string $comment Comment for log records
+        */
+       function setArchFileVisibility( $title, $items, $bitfield, $comment ) {
+               global $wgOut;
+               
+               $userAllowedAll = $success = true;
+               $count = 0; 
+               $Id_set = array();
+               
+               // Run through and pull all our data in one query
+               foreach( $items as $id ) {
+                       $where[] = intval($id);
+               }
+               $whereClause = 'fa_id IN(' . implode(',',$where) . ')';
+               $result = $this->dbw->select( 'filearchive', '*',
+                       array( 'fa_name' => $title->getDbKey(),
+                               $whereClause ),
+                       __METHOD__ );
+               while( $row = $this->dbw->fetchObject( $result ) ) {
+                       $filesObjs[$row->fa_id] = ArchivedFile::newFromRow( $row );
+               }
+               // To work!
+               foreach( $items as $fileid ) {
+                       if( !isset($filesObjs[$fileid]) ) {
+                               $success = false;
+                               continue; // Must exist
+                       } else if( !$filesObjs[$fileid]->userCan(File::DELETED_RESTRICTED) ) {
+                       $userAllowedAll=false;
+                               continue;
+                       }
+                       // Which revisions did we change anything about?
+                       if( $filesObjs[$fileid]->deleted != $bitfield ) {
+                          $Id_set[]=$fileid;
+                          $count++;
+                          
+                          $this->updateArchFiles( $filesObjs[$fileid], $bitfield );
+                       }
+               }
+               // Log if something was changed
+               if( $count > 0 ) {
+                       $this->updateLog( $title, $count, $bitfield, $comment, 
+                               $filesObjs[$fileid]->deleted, $title, 'fileid', $Id_set );
+               }
+               // Where all revs allowed to be set?
+               if( !$userAllowedAll ) {
+                       $wgOut->permissionRequired( 'hiderevision' );
+                       return false;
+               }
+               
+               return $success;
+       }
+
+       /**
+        * @param $title, the page these events apply to
+        * @param array $items list of log ID numbers
+        * @param int $bitfield new log_deleted value
+        * @param string $comment Comment for log records
+        */
+       function setEventVisibility( $items, $bitfield, $comment ) {
+               global $wgOut;
+               
+               $userAllowedAll = $success = true;
+               $logs_count = array(); 
+               $logs_Ids = array();
+               
+               // Run through and pull all our data in one query
+               foreach( $items as $logid ) {
+                       $where[] = intval($logid);
+               }
+               $whereClause = 'log_id IN(' . implode(',',$where) . ')';
+               $result = $this->dbw->select( 'logging', '*', 
+                       array( $whereClause ),
+                       __METHOD__ );
+               while( $row = $this->dbw->fetchObject( $result ) ) {
+                       $logRows[$row->log_id] = $row;
+               }
+               // To work!
+               foreach( $items as $logid ) {
+                       if( !isset($logRows[$logid]) ) {
+                               $success = false;
+                               continue; // Must exist
+                       } else if( !LogViewer::userCan($logRows[$logid], Revision::DELETED_RESTRICTED)
+                                || $logRows[$logid]->log_type=='oversight' ) {
+                       // Don't hide from oversight log!!!
+                       $userAllowedAll=false;
+                       continue;
+                       }
+                       $logtype = $logRows[$logid]->log_type;
+                       // For logging, maintain a count of events per log type
+                       if( !isset( $logs_count[$logtype] ) ) {
+                               $logs_count[$logtype]=0;
+                               $logs_Ids[$logtype]=array();
+                       }
+                       // Which logs did we change anything about?
+                       if( $logRows[$logid]->log_deleted != $bitfield ) {
+                               $logs_Ids[$logtype][]=$logid;
+                               $logs_count[$logtype]++;
+                          
+                               $this->updateLogs( $logRows[$logid], $bitfield );
+                               $this->updateRecentChangesLog( $logRows[$logid], $bitfield, true );
+                       }
+               }
+               foreach( $logs_count as $logtype => $count ) {
+                       //Don't log or touch if nothing changed
+                       if( $count > 0 ) {
+                               $target = SpecialPage::getTitleFor( 'Log', $logtype );
+                               $this->updateLog( $target, $count, $bitfield, $logRows[$logid]->log_deleted, 
+                               $comment, $target, 'logid', $logs_Ids[$logtype] );
+                       }
+               }
+               // Where all revs allowed to be set?
+               if( !$userAllowedAll ) {
+                       $wgOut->permissionRequired( 'hiderevision' ); 
+                       return false;
+               }
+               
+               return $success;
+       }
+
+       /**
+        * Moves an image to a safe private location
+        * Caller is responsible for clearing caches
+        * @param File $oimage
+        * @returns string, timestamp on success, false on failure
+        */     
+       function makeOldImagePrivate( $oimage ) {
+               global $wgFileStore, $wgUseSquid;
+       
+               $transaction = new FSTransaction();
+               if( !FileStore::lock() ) {
+                       wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" );
+                       return false;
+               }
+               $oldpath = $oimage->getArchivePath() . DIRECTORY_SEPARATOR . $oimage->archive_name;
+               // Dupe the file into the file store
+               if( file_exists( $oldpath ) ) {
+                       // Is our directory configured?
+                       if( $store = FileStore::get( 'hidden' ) ) {
+                               if( !$oimage->sha1 )
+                                       $oimage->upgradeRow();
+                               
+                               $key = $oimage->sha1.'.'.$oimage->getExtension();
+                               $transaction->add( $store->insert( $key, $oldpath, FileStore::DELETE_ORIGINAL ) );
                        } else {
-                               $pages[$pageid] = 1;
+                               $group = null;
+                               $key = null;
+                               $transaction = false; // Return an error and do nothing
                        }
+               } else {
+                       wfDebug( __METHOD__." deleting already-missing '$oldpath'; moving on to database\n" );
+                       $group = null;
+                       $key = '';
+                       $transaction = new FSTransaction(); // empty
+               }
+
+               if( $transaction === false ) {
+                       // Fail to restore?
+                       wfDebug( __METHOD__.": import to file store failed, aborting\n" );
+                       throw new MWException( "Could not archive and delete file $oldpath" );
+                       return false;
                }
                
-               // Clear caches...
-               foreach( $pages as $pageid => $count ) {
-                       $title = Title::newFromId( $pageid );
-                       $this->updatePage( $title );
-                       $this->updateLog( $title, $count, $bitfield, $comment );
+               wfDebug( __METHOD__.": set db items, applying file transactions\n" );
+               $transaction->commit();
+               FileStore::unlock();
+               
+               $m = explode('!',$oimage->archive_name,2);
+               $timestamp = $m[0];
+               
+               return $timestamp;
+       }
+
+       /**
+        * Moves an image from a safe private location
+        * Caller is responsible for clearing caches
+        * @param File $oimage
+        * @returns string, timestamp on success, false on failure
+        */             
+       function makeOldImagePublic( $oimage ) {
+               global $wgFileStore;
+       
+               $transaction = new FSTransaction();
+               if( !FileStore::lock() ) {
+                       wfDebug( __METHOD__." could not acquire filestore lock\n" );
+                       return false;
+               }
+               
+               $store = FileStore::get( 'hidden' );
+               if( !$store ) {
+                       wfDebug( __METHOD__.": skipping row with no file.\n" );
+                       return false;
+               }
+               
+               $key = $oimage->sha1.'.'.$oimage->getExtension();
+               $destDir = $oimage->getArchivePath();
+               if( !is_dir( $destDir ) ) {
+                       wfMkdirParents( $destDir );
+               }
+               $destPath = $destDir . DIRECTORY_SEPARATOR . $oimage->archive_name;
+               // Check if any other stored revisions use this file;
+               // if so, we shouldn't remove the file from the hidden
+               // archives so they will still work.
+               $useCount = $this->dbw->selectField( 'oldimage','COUNT(*)',
+                       array( 'oi_sha1' => $oimage->sha1,
+                               'oi_deleted & '.File::DELETED_FILE => File::DELETED_FILE ),
+                       __METHOD__ );
+                       
+               if( $useCount == 0 ) {
+                       wfDebug( __METHOD__.": nothing else using {$oimage->sha1}, will deleting after\n" );
+                       $flags = FileStore::DELETE_ORIGINAL;
+               } else {
+                       $flags = 0;
+               }
+               $transaction->add( $store->export( $key, $destPath, $flags ) );
+               
+               wfDebug( __METHOD__.": set db items, applying file transactions\n" );
+               $transaction->commit();
+               FileStore::unlock();
+               
+               $m = explode('!',$oimage->archive_name,2);
+               $timestamp = $m[0];
+               
+               return $timestamp;
+       }
+       
+       /**
+        * Moves an image from a safe private location to deleted archives
+        * Groups should be 'deleted' and 'hidden'
+        * @param File $oimage
+        * @param string $group1, old group
+        * @param string $group2, new group
+        * @returns bool, success
+        */     
+       function moveImageFromFileRepos( $oimage, $group1, $group2 ) {
+               global $wgFileStore;
+               
+               $transaction = new FSTransaction();
+               if( !FileStore::lock() ) {
+                       wfDebug( __METHOD__." could not acquire filestore lock\n" );
+                       return false;
                }
                
+               $storeOld = FileStore::get( $group1 );
+               if( !$storeOld ) {
+                       wfDebug( __METHOD__.": skipping row with no file.\n" );
+                       return false;
+               }
+               $key = $oimage->sha1.'.'.$oimage->getExtension();
+               
+               $oldPath = $storeOld->filePath( $key ); 
+               // Check if any other stored revisions use this file;
+               // if so, we shouldn't remove the file from the hidden
+               // archives so they will still work.
+               if( $group1=='hidden' ) {
+                       $useCount = $this->dbw->selectField( 'oldimage','COUNT(*)',
+                               array( 'oi_sha1' => $oimage->sha1 ),
+                               __METHOD__ );
+               } else if( $group1=='deleted' ) {
+                       $useCount = $this->dbw->selectField( 'filearchive','COUNT(*)',
+                               array( 'fa_storage_key' => $key, 'fa_storage_group' => 'deleted' ),
+                               __METHOD__ );
+               }
+                       
+               if( $useCount == 0 ) {
+                       wfDebug( __METHOD__.": nothing else using $key, will deleting after\n" );
+                       $flags = FileStore::DELETE_ORIGINAL;
+               } else {
+                       $flags = 0;
+               }
+               
+               $storeNew = FileStore::get( $group2 );
+               $transaction->add( $storeNew->insert( $key, $oldPath, $flags ) );
+               
+               wfDebug( __METHOD__.": set db items, applying file transactions\n" );
+               $transaction->commit();
+               FileStore::unlock();
+               
                return true;
        }
        
@@ -225,26 +1265,84 @@ class RevisionDeleter {
         * @param int $bitfield new rev_deleted bitfield value
         */
        function updateRevision( $rev, $bitfield ) {
-               $this->db->update( 'revision',
+               $this->dbw->update( 'revision',
                        array( 'rev_deleted' => $bitfield ),
                        array( 'rev_id' => $rev->getId() ),
                        'RevisionDeleter::updateRevision' );
        }
        
+       /**
+        * Update the revision's rev_deleted field
+        * @param Revision $rev
+        * @param int $bitfield new rev_deleted bitfield value
+        */
+       function updateArchive( $rev, $bitfield ) {
+               $this->dbw->update( 'archive',
+                       array( 'ar_deleted' => $bitfield ),
+                       array( 'ar_rev_id' => $rev->getId() ),
+                       'RevisionDeleter::updateArchive' );
+       }
+
+       /**
+        * Update the images's oi_deleted field
+        * @param File $oimage
+        * @param int $bitfield new rev_deleted bitfield value
+        */
+       function updateOldFiles( $oimage, $bitfield ) {
+               $this->dbw->update( 'oldimage',
+                       array( 'oi_deleted' => $bitfield ),
+                       array( 'oi_archive_name' => $oimage->archive_name ),
+                       'RevisionDeleter::updateOldFiles' );
+       }
+       
+       /**
+        * Update the images's fa_deleted field
+        * @param ArchivedFile $file
+        * @param int $bitfield new rev_deleted bitfield value
+        */
+       function updateArchFiles( $file, $bitfield ) {
+               $this->dbw->update( 'filearchive',
+                       array( 'fa_deleted' => $bitfield ),
+                       array( 'fa_id' => $file->id ),
+                       'RevisionDeleter::updateArchFiles' );
+       }       
+       
+       /**
+        * Update the logging log_deleted field
+        * @param Row $event
+        * @param int $bitfield new rev_deleted bitfield value
+        */
+       function updateLogs( $event, $bitfield ) {
+               $this->dbw->update( 'logging',
+                       array( 'log_deleted' => $bitfield ),
+                       array( 'log_id' => $event->log_id ),
+                       'RevisionDeleter::updateLogs' );
+       }       
+       
        /**
         * Update the revision's recentchanges record if fields have been hidden
         * @param Revision $rev
         * @param int $bitfield new rev_deleted bitfield value
         */
-       function updateRecentChanges( $rev, $bitfield ) {
-               $this->db->update( 'recentchanges',
-                       array(
-                               'rc_user' => ($bitfield & Revision::DELETED_USER) ? 0 : $rev->getUser(),
-                               'rc_user_text' => ($bitfield & Revision::DELETED_USER) ? wfMsg( 'rev-deleted-user' ) : $rev->getUserText(),
-                               'rc_comment' => ($bitfield & Revision::DELETED_COMMENT) ? wfMsg( 'rev-deleted-comment' ) : $rev->getComment() ),
-                       array(
-                               'rc_this_oldid' => $rev->getId() ),
-                       'RevisionDeleter::updateRecentChanges' );
+       function updateRecentChangesEdits( $rev, $bitfield ) {
+               $this->dbw->update( 'recentchanges',
+                       array( 'rc_deleted' => $bitfield,
+                                  'rc_patrolled' => 1 ),
+                       array( 'rc_this_oldid' => $rev->getId() ),
+                       'RevisionDeleter::updateRecentChangesEdits' );
+       }
+       
+       /**
+        * Update the revision's recentchanges record if fields have been hidden
+        * @param Row $event
+        * @param int $bitfield new rev_deleted bitfield value
+        */
+       function updateRecentChangesLog( $event, $bitfield ) {
+               $this->dbw->update( 'recentchanges',
+                       array( 'rc_deleted' => $bitfield,
+                                  'rc_patrolled' => 1 ),
+                       array( 'rc_logid' => $event->log_id ),
+                       'RevisionDeleter::updateRecentChangesLog' );
        }
        
        /**
@@ -255,21 +1353,39 @@ class RevisionDeleter {
         */
        function updatePage( $title ) {
                $title->invalidateCache();
+               $title->purgeSquid();
+               
+               // Extensions that require referencing previous revisions may need this
+               wfRunHooks( 'ArticleRevisionVisiblitySet', array( &$title ) );
        }
        
        /**
         * Record a log entry on the action
-        * @param Title $title
+        * @param Title $title, page where item was removed from
         * @param int $count the number of revisions altered for this page
-        * @param int $bitfield the new rev_deleted value
+        * @param int $nbitfield the new _deleted value
+        * @param int $obitfield the old _deleted value
         * @param string $comment
+        * @param Title $target, the relevant page
+        * @param string $param, URL param
+        * @param Array $items
         */
-       function updateLog( $title, $count, $bitfield, $comment ) {
-               $log = new LogPage( 'delete' );
-               $reason = "changed $count revisions to $bitfield";
-               $reason .= ": $comment";
-               $log->addEntry( 'revision', $title, $reason );
+       function updateLog( $title, $count, $nbitfield, $obitfield, $comment, $target, $param, $items = array() ) {
+               // Put things hidden from sysops in the oversight log
+               $logtype = ( ($nbitfield | $obitfield) & Revision::DELETED_RESTRICTED ) ? 'oversight' : 'delete';
+               $log = new LogPage( $logtype );
+               // FIXME: do this better
+               if( $param=='logid' ) {
+                       $params = array( implode( ',', $items) );
+               $reason = wfMsgExt('logdelete-logaction', array('parsemag'), $count, $nbitfield );
+                       if($comment) $reason .= ": $comment";
+                       $log->addEntry( 'event', $title, $reason, $params );
+               } else {
+                       // Add params for effected page and ids
+                       $params = array( $target->getPrefixedText(), $param, implode( ',', $items) );
+               $reason = wfMsgExt('revdelete-logaction', array('parsemag'), $count, $nbitfield );
+                       if($comment) $reason .= ": $comment";
+                       $log->addEntry( 'revision', $title, $reason, $params );
+               }
        }
 }
-
-
index a893966..b23d036 100644 (file)
@@ -16,10 +16,13 @@ function wfSpecialSpecialpages() {
        $sk = $wgUser->getSkin();
 
        /** Pages available to all */
-       wfSpecialSpecialpages_gen( SpecialPage::getRegularPages(), 'spheading', $sk );
+       wfSpecialSpecialpages_gen( SpecialPage::getRegularPages(), 'spheading', $sk, false );
 
        /** Restricted special pages */
-       wfSpecialSpecialpages_gen( SpecialPage::getRestrictedPages(), 'restrictedpheading', $sk );
+       wfSpecialSpecialpages_gen( SpecialPage::getRestrictedPages(), 'restrictedpheading', $sk, false );
+       
+       /** Restricted logs */
+       wfSpecialSpecialpages_gen( SpecialPage::getRestrictedLogs(), 'restrictedlheading', $sk, true );
 }
 
 /**
@@ -27,9 +30,10 @@ function wfSpecialSpecialpages() {
  * @param $pages the list of pages
  * @param $heading header to be used
  * @param $sk skin object ???
+ * @param $islog, is this for a list of log types?
  */
-function wfSpecialSpecialpages_gen($pages,$heading,$sk) {
-       global $wgOut, $wgSortSpecialPages;
+function wfSpecialSpecialpages_gen( $pages, $heading, $sk, $islog=false ) {
+       global $wgOut, $wgUser, $wgSortSpecialPages;
 
        if( count( $pages ) == 0 ) {
                # Yeah, that was pointless. Thanks for coming.
@@ -38,9 +42,13 @@ function wfSpecialSpecialpages_gen($pages,$heading,$sk) {
 
        /** Put them into a sortable array */
        $sortedPages = array();
-       foreach ( $pages as $page ) {
-               if ( $page->isListed() ) {
-                       $sortedPages[$page->getDescription()] = $page->getTitle();
+       if( $islog ) {
+               $sortedPages = $pages;
+       } else {
+               foreach ( $pages as $page ) {
+                       if ( $page->isListed() ) {
+                               $sortedPages[$page->getDescription()] = $page->getTitle();
+                       }
                }
        }
 
index 5678a81..7255682 100644 (file)
@@ -78,7 +78,7 @@ class PageArchive {
                                array(
                                        'ar_namespace',
                                        'ar_title',
-                                       'COUNT(*) AS count',
+                                       'COUNT(*) AS count'
                                ),
                                $condition,
                                __METHOD__,
@@ -91,24 +91,6 @@ class PageArchive {
                );
        }
        
-       /**
-        * List the revisions of the given page. Returns result wrapper with
-        * (ar_minor_edit, ar_timestamp, ar_user, ar_user_text, ar_comment) fields.
-        *
-        * @return ResultWrapper
-        */
-       function listRevisions() {
-               $dbr = wfGetDB( DB_SLAVE );
-               $res = $dbr->select( 'archive',
-                       array( 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text', 'ar_comment', 'ar_len' ),
-                       array( 'ar_namespace' => $this->title->getNamespace(),
-                              'ar_title' => $this->title->getDBkey() ),
-                       'PageArchive::listRevisions',
-                       array( 'ORDER BY' => 'ar_timestamp DESC' ) );
-               $ret = $dbr->resultObject( $res );
-               return $ret;
-       }
-       
        /**
         * List the deleted file revisions for this page, if it's a file page.
         * Returns a result wrapper with various filearchive fields, or null
@@ -124,14 +106,22 @@ class PageArchive {
                                array(
                                        'fa_id',
                                        'fa_name',
+                                       'fa_archive_name',
                                        'fa_storage_key',
+                                       'fa_storage_group',
                                        'fa_size',
                                        'fa_width',
                                        'fa_height',
+                                       'fa_bits',
+                                       'fa_metadata',
+                                       'fa_media_type',
+                                       'fa_major_mime',
+                                       'fa_minor_mime',
                                        'fa_description',
                                        'fa_user',
                                        'fa_user_text',
-                                       'fa_timestamp' ),
+                                       'fa_timestamp',
+                                       'fa_deleted' ),
                                array( 'fa_name' => $this->title->getDbKey() ),
                                __METHOD__,
                                array( 'ORDER BY' => 'fa_timestamp DESC' ) );
@@ -152,14 +142,25 @@ class PageArchive {
                $rev = $this->getRevision( $timestamp );
                return $rev ? $rev->getText() : null;
        }
+       
+       function getRevisionConds( $timestamp, $id ) {
+               if( $id ) {
+                       $id = intval($id);
+                       return "ar_rev_id=$id";
+               } else if( $timestamp ) {
+                       return "ar_timestamp=$timestamp";
+               } else {
+                       return 'ar_rev_id=0';
+               }
+       }
 
        /**
         * Return a Revision object containing data for the deleted revision.
-        * Note that the result *may* or *may not* have a null page ID.
-        * @param string $timestamp
+        * Note that the result *may* have a null page ID.
+        * @param string $timestamp or $id
         * @return Revision
         */
-       function getRevision( $timestamp ) {
+       function getRevision( $timestamp, $id=null ) {
                $dbr = wfGetDB( DB_SLAVE );
                $row = $dbr->selectRow( 'archive',
                        array(
@@ -172,10 +173,11 @@ class PageArchive {
                                'ar_minor_edit',
                                'ar_flags',
                                'ar_text_id',
+                               'ar_deleted',
                                'ar_len' ),
                        array( 'ar_namespace' => $this->title->getNamespace(),
                               'ar_title' => $this->title->getDbkey(),
-                              'ar_timestamp' => $dbr->timestamp( $timestamp ) ),
+                              $this->getRevisionConds( $dbr->timestamp($timestamp), $id ) ),
                        __METHOD__ );
                if( $row ) {
                        return new Revision( array(
@@ -189,7 +191,9 @@ class PageArchive {
                                'user_text'  => $row->ar_user_text,
                                'timestamp'  => $row->ar_timestamp,
                                'minor_edit' => $row->ar_minor_edit,
-                               'text_id'    => $row->ar_text_id ) );
+                               'text_id'    => $row->ar_text_id,
+                               'deleted'    => $row->ar_deleted,
+                               'len'        => $row->ar_len) );
                } else {
                        return null;
                }
@@ -254,48 +258,50 @@ class PageArchive {
         * Restore the given (or all) text and file revisions for the page.
         * Once restored, the items will be removed from the archive tables.
         * The deletion log will be updated with an undeletion notice.
+        * Use -1 for the one of the timestamps to only restore files or text
         *
-        * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
+        * @param string $pagetimestamp, restore all revisions since this time
         * @param string $comment
-        * @param array $fileVersions
+        * @param string $filetimestamp, restore all revision from this time on
+        * @param bool $Unsuppress
         *
         * @return true on success.
         */
-       function undelete( $timestamps, $comment = '', $fileVersions = array() ) {
+       function undelete( $pagetimestamp = 0, $comment = '', $filetimestamp = 0, $Unsuppress = false) {
                // If both the set of text revisions and file revisions are empty,
                // restore everything. Otherwise, just restore the requested items.
-               $restoreAll = empty( $timestamps ) && empty( $fileVersions );
+               $restoreAll = ($pagetimestamp==0 && $filetimestamp==0);
                
-               $restoreText = $restoreAll || !empty( $timestamps );
-               $restoreFiles = $restoreAll || !empty( $fileVersions );
+               $restoreText = ($restoreAll || $pagetimestamp );
+               $restoreFiles = ($restoreAll || $filetimestamp );
+               
+               if( $restoreText && $pagetimestamp >= 0 ) {
+                       $textRestored = $this->undeleteRevisions( $pagetimestamp, $Unsuppress );
+               } else {
+                       $textRestored = 0;
+               }
                
-               if( $restoreFiles && $this->title->getNamespace() == NS_IMAGE ) {
+               if( $restoreFiles && $filetimestamp >= 0 && $this->title->getNamespace()==NS_IMAGE ) {
                        $img = wfLocalFile( $this->title );
-                       $this->fileStatus = $img->restore( $fileVersions );
+                       $this->fileStatus = $img->restore( $filetimestamp, $Unsuppress );
                        $filesRestored = $this->fileStatus->successCount;
                } else {
                        $filesRestored = 0;
                }
-               
-               if( $restoreText ) {
-                       $textRestored = $this->undeleteRevisions( $timestamps );
-               } else {
-                       $textRestored = 0;
-               }
 
                // Touch the log!
                global $wgContLang;
                $log = new LogPage( 'delete' );
                
                if( $textRestored && $filesRestored ) {
-                       $reason = wfMsgForContent( 'undeletedrevisions-files',
+                       $reason = wfMsgExt( 'undeletedrevisions-files', array('parsemag'),
                                $wgContLang->formatNum( $textRestored ),
                                $wgContLang->formatNum( $filesRestored ) );
                } elseif( $textRestored ) {
-                       $reason = wfMsgForContent( 'undeletedrevisions',
+                       $reason = wfMsgExt( 'undeletedrevisions', array('parsemag'),
                                $wgContLang->formatNum( $textRestored ) );
                } elseif( $filesRestored ) {
-                       $reason = wfMsgForContent( 'undeletedfiles',
+                       $reason = wfMsgExt( 'undeletedfiles', array('parsemag'),
                                $wgContLang->formatNum( $filesRestored ) );
                } else {
                        wfDebug( "Undelete: nothing undeleted...\n" );
@@ -304,7 +310,7 @@ class PageArchive {
                
                if( trim( $comment ) != '' )
                        $reason .= ": {$comment}";
-               $log->addEntry( 'restore', $this->title, $reason );
+               $log->addEntry( 'restore', $this->title, $reason, array($pagetimestamp,$filetimestamp) );
 
                if ( $this->fileStatus && !$this->fileStatus->ok ) {
                        return false;
@@ -318,52 +324,73 @@ class PageArchive {
         * to the cur/old tables. If the page currently exists, all revisions will
         * be stuffed into old, otherwise the most recent will go into cur.
         *
-        * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
+        * @param string $timestamps, restore all revisions since this time
         * @param string $comment
         * @param array $fileVersions
+        * @param bool $Unsuppress, remove all ar_deleted/fa_deleted restrictions of seletected revs
         *
         * @return int number of revisions restored
         */
-       private function undeleteRevisions( $timestamps ) {
-               $restoreAll = empty( $timestamps );
+       private function undeleteRevisions( $timestamp, $Unsuppress = false ) {
+               $restoreAll = ($timestamp==0);
                
                $dbw = wfGetDB( DB_MASTER );
+               $makepage = false; // Do we need to make a new page?
 
                # Does this page already exist? We'll have to update it...
                $article = new Article( $this->title );
                $options = 'FOR UPDATE';
                $page = $dbw->selectRow( 'page',
                        array( 'page_id', 'page_latest' ),
-                       array( 'page_namespace' => $this->title->getNamespace(),
-                              'page_title'     => $this->title->getDBkey() ),
+                       array( 'page_namespace' => $this->title->getNamespace(), 
+                               'page_title' => $this->title->getDBkey() ),
                        __METHOD__,
                        $options );
+               
                if( $page ) {
                        # Page already exists. Import the history, and if necessary
                        # we'll update the latest revision field in the record.
                        $newid             = 0;
                        $pageId            = $page->page_id;
                        $previousRevId     = $page->page_latest;
+                       # Get the time span of this page
+                       $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
+                               array( 'rev_id' => $previousRevId ),
+                               __METHOD__ );
+                       
+                       if( $previousTimestamp === false ) {
+                               wfDebug( __METHOD__.": existing page refers to a page_latest that does not exist\n" );
+                               return false;
+                       }
+                       # Do not fuck up histories by merging them in annoying, unrevertable ways
+                       # This page id should match any deleted ones (excepting NULL values)
+                       # We can allow restoration into redirect pages with no edit history
+                       $otherpages = $dbw->selectField( 'archive', 'COUNT(*)',
+                               array( 'ar_namespace' => $this->title->getNamespace(), 
+                                       'ar_title' => $this->title->getDBkey(), 
+                                       'ar_page_id IS NOT NULL', "ar_page_id != $pageId" ),
+                               __METHOD__,
+                               array('LIMIT' => 1) );
+                       if( $otherpages && !$this->title->isValidRestoreOverTarget() ) {
+                               return false;
+                       }
+                       
                } else {
                        # Have to create a new article...
-                       $newid  = $article->insertOn( $dbw );
-                       $pageId = $newid;
+                       $makepage = true;
                        $previousRevId = 0;
+                       $previousTimestamp = 0;
                }
 
-               if( $restoreAll ) {
-                       $oldones = '1 = 1'; # All revisions...
-               } else {
-                       $oldts = implode( ',',
-                               array_map( array( &$dbw, 'addQuotes' ),
-                                       array_map( array( &$dbw, 'timestamp' ),
-                                               $timestamps ) ) );
-
-                       $oldones = "ar_timestamp IN ( {$oldts} )";
+               $conditions = array( 
+                       'ar_namespace' => $this->title->getNamespace(), 
+                       'ar_title' => $this->title->getDBkey() );
+               if( $timestamp ) {
+                       $conditions[] = "ar_timestamp >= {$timestamp}";
                }
 
                /**
-                * Restore each revision...
+                * Select each archived revision...
                 */
                $result = $dbw->select( 'archive',
                        /* fields */ array(
@@ -376,24 +403,40 @@ class PageArchive {
                                'ar_minor_edit',
                                'ar_flags',
                                'ar_text_id',
+                               'ar_deleted',
                                'ar_len' ),
-                       /* WHERE */ array(
-                               'ar_namespace' => $this->title->getNamespace(),
-                               'ar_title'     => $this->title->getDBkey(),
-                               $oldones ),
+                       /* WHERE */ 
+                               $conditions,
                        __METHOD__,
                        /* options */ array(
                                'ORDER BY' => 'ar_timestamp' )
                        );
-               if( $dbw->numRows( $result ) < count( $timestamps ) ) {
-                       wfDebug( __METHOD__.": couldn't find all requested rows\n" );
-                       return false;
+               $ret = $dbw->resultObject( $result );
+               
+               $rev_count = $dbw->numRows( $result );  
+               if( $rev_count ) {
+                       # We need to seek around as just using DESC in the ORDER BY
+                       # would leave the revisions inserted in the wrong order
+                       $first = $ret->fetchObject();
+                       $ret->seek( $rev_count - 1 );
+                       $last = $ret->fetchObject();
+                       // We don't handle well changing the top revision's settings
+                       if( !$Unsuppress && $last->ar_deleted && $last->ar_timestamp > $previousTimestamp ) {
+                               wfDebug( __METHOD__.": restoration would result in a deleted top revision\n" );
+                               return false;
+                       }
+                       $ret->seek( 0 );
                }
                
+               if( $makepage ) {
+                       $newid  = $article->insertOn( $dbw );
+                       $pageId = $newid;
+               }
+
                $revision = null;
                $restored = 0;
-
-               while( $row = $dbw->fetchObject( $result ) ) {
+               
+               while( $row = $ret->fetchObject() ) {
                        if( $row->ar_text_id ) {
                                // Revision was deleted in 1.5+; text is in
                                // the regular text table, use the reference.
@@ -416,12 +459,14 @@ class PageArchive {
                                'timestamp'  => $row->ar_timestamp,
                                'minor_edit' => $row->ar_minor_edit,
                                'text_id'    => $row->ar_text_id,
-                               'len'            => $row->ar_len
+                               'deleted'        => $Unsuppress ? 0 : $row->ar_deleted,
+                               'len'        => $row->ar_len
                                ) );
                        $revision->insertOn( $dbw );
                        $restored++;
                }
-
+               
+               # If there were any revisions restored...
                if( $revision ) {
                        // Attach the latest revision to the page...
                        $wasnew = $article->updateIfNewerOn( $dbw, $revision, $previousRevId );
@@ -430,7 +475,7 @@ class PageArchive {
                                // Update site stats, link tables, etc
                                $article->createUpdates( $revision );
                        }
-
+                       
                        if( $newid ) {
                                wfRunHooks( 'ArticleUndelete', array( &$this->title, true ) );
                                Article::onArticleCreate( $this->title );
@@ -438,16 +483,21 @@ class PageArchive {
                                wfRunHooks( 'ArticleUndelete', array( &$this->title, false ) );
                                Article::onArticleEdit( $this->title );
                        }
-               } else {
-                       # Something went terribly wrong!
                }
 
                # Now that it's safely stored, take it out of the archive
                $dbw->delete( 'archive',
-                       /* WHERE */ array(
-                               'ar_namespace' => $this->title->getNamespace(),
-                               'ar_title' => $this->title->getDBkey(),
-                               $oldones ),
+                       /* WHERE */ 
+                       $conditions,
+                       __METHOD__ );
+               # Update any revision left to reflect the page they belong to.
+               # If a page was deleted, and a new one created over it, then deleted,
+               # selective restore acts as a way to seperate the two. Nevertheless, we
+               # still want the rest to be restorable, in case some mistake was made.
+               $dbw->update( 'archive', 
+                       array( 'ar_page_id' => $newid ),
+                       array( 'ar_namespace' => $this->title->getNamespace(), 
+                                       'ar_title' => $this->title->getDBkey() ),
                        __METHOD__ );
 
                return $restored;
@@ -473,71 +523,92 @@ class UndeleteForm {
                $time = $request->getVal( 'timestamp' );
                $this->mTimestamp = $time ? wfTimestamp( TS_MW, $time ) : '';
                $this->mFile = $request->getVal( 'file' );
+               $this->mDiff = $request->getVal( 'diff' );
+               $this->mOldid = $request->getVal( 'oldid' );
                
-               $posted = $request->wasPosted() &&
-                       $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
+               $posted = $request->wasPosted() && $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) );
                $this->mRestore = $request->getCheck( 'restore' ) && $posted;
                $this->mPreview = $request->getCheck( 'preview' ) && $posted;
                $this->mComment = $request->getText( 'wpComment' );
+               $this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) && $wgUser->isAllowed( 'oversight' );
                
                if( $par != "" ) {
                        $this->mTarget = $par;
+                       $_GET['target'] = $par; // hack for Pager
                }
-               if ( $wgUser->isAllowed( 'delete' ) && !$wgUser->isBlocked() ) {
+               if( $wgUser->isAllowed( 'delete' ) && !$wgUser->isBlocked() ) {
                        $this->mAllowed = true;
                } else {
                        $this->mAllowed = false;
                        $this->mTimestamp = '';
                        $this->mRestore = false;
                }
-               if ( $this->mTarget !== "" ) {
+               if( $this->mTarget !== "" ) {
                        $this->mTargetObj = Title::newFromURL( $this->mTarget );
                } else {
                        $this->mTargetObj = NULL;
                }
                if( $this->mRestore ) {
-                       $timestamps = array();
-                       $this->mFileVersions = array();
-                       foreach( $_REQUEST as $key => $val ) {
-                               $matches = array();
-                               if( preg_match( '/^ts(\d{14})$/', $key, $matches ) ) {
-                                       array_push( $timestamps, $matches[1] );
-                               }
-                               
-                               if( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) {
-                                       $this->mFileVersions[] = intval( $matches[1] );
-                               }
+                       $this->mFileTimestamp = $request->getVal('imgrestorepoint');
+                       $this->mPageTimestamp = $request->getVal('restorepoint');
+               }
+               $this->preCacheMessages();
+       }
+       
+       /**
+        * As we use the same small set of messages in various methods and that
+        * they are called often, we call them once and save them in $this->message
+        */
+       function preCacheMessages() {
+               // Precache various messages
+               if( !isset( $this->message ) ) {
+                       foreach( explode(' ', 'last rev-delundel' ) as $msg ) {
+                               $this->message[$msg] = wfMsgExt( $msg, array( 'escape') );
                        }
-                       rsort( $timestamps );
-                       $this->mTargetTimestamp = $timestamps;
                }
        }
 
        function execute() {
-               global $wgOut;
-               if ( $this->mAllowed ) {
-                       $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
+               global $wgOut, $wgUser;
+               if( $this->mAllowed ) {
+                       $wgOut->setPagetitle( wfMsgHtml( "undeletepage" ) );
                } else {
-                       $wgOut->setPagetitle( wfMsg( "viewdeletedpage" ) );
+                       $wgOut->setPagetitle( wfMsgHtml( "viewdeletedpage" ) );
                }
                
                if( is_null( $this->mTargetObj ) ) {
-                       $this->showSearchForm();
+               # Not all users can just browse every deleted page from the list
+                       if( $wgUser->isAllowed( 'browsearchive' ) ) {
+                               $this->showSearchForm();
 
-                       # List undeletable articles
-                       if( $this->mSearchPrefix ) {
-                               $result = PageArchive::listPagesByPrefix(
-                                       $this->mSearchPrefix );
-                               $this->showList( $result );
+                               # List undeletable articles
+                               if( $this->mSearchPrefix ) {
+                                       $result = PageArchive::listPagesByPrefix( $this->mSearchPrefix );
+                                       $this->showList( $result );
+                               }
+                       } else {
+                               $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) );
                        }
                        return;
                }
                if( $this->mTimestamp !== '' ) {
                        return $this->showRevision( $this->mTimestamp );
                }
+               
+               if( $this->mDiff && $this->mOldid )
+                       return $this->showDiff( $this->mDiff, $this->mOldid );
+               
                if( $this->mFile !== null ) {
-                       return $this->showFile( $this->mFile );
+                       $file = new ArchivedFile( $this->mTargetObj, '', $this->mFile );
+                       // Check if user is allowed to see this file
+                       if( !$file->userCan( File::DELETED_FILE ) ) {
+                               $wgOut->permissionRequired( 'hiderevision' ); 
+                               return false;
+                       } else {
+                               return $this->showFile( $this->mFile );
+                       }
                }
+               
                if( $this->mRestore && $this->mAction == "submit" ) {
                        return $this->undelete();
                }
@@ -565,7 +636,8 @@ class UndeleteForm {
                        '</form>' );
        }
 
-       /* private */ function showList( $result ) {
+       // Generic list of deleted pages
+       private function showList( $result ) {
                global $wgLang, $wgContLang, $wgUser, $wgOut;
                
                if( $result->numRows() == 0 ) {
@@ -593,7 +665,7 @@ class UndeleteForm {
                return true;
        }
 
-       /* private */ function showRevision( $timestamp ) {
+       private function showRevision( $timestamp ) {
                global $wgLang, $wgUser, $wgOut;
                $self = SpecialPage::getTitleFor( 'Undelete' );
                $skin = $wgUser->getSkin();
@@ -604,14 +676,24 @@ class UndeleteForm {
                $rev = $archive->getRevision( $timestamp );
                
                if( !$rev ) {
-                       $wgOut->addWikiTexT( wfMsg( 'undeleterevision-missing' ) );
+                       $wgOut->addWikiText( wfMsg( 'undeleterevision-missing' ) );
                        return;
                }
                
+               if( $rev->isDeleted(Revision::DELETED_TEXT) ) {
+                       if( !$rev->userCan(Revision::DELETED_TEXT) ) {
+                               $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) );
+                               return;
+                       } else {
+                               $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) );
+                               $wgOut->addHTML( '<br/>' );
+                               // and we are allowed to see...
+                       }
+               }
+               
                $wgOut->setPageTitle( wfMsg( 'undeletepage' ) );
                
-               $link = $skin->makeKnownLinkObj(
-                       $self,
+               $link = $skin->makeKnownLinkObj( $self,
                        htmlspecialchars( $this->mTargetObj->getPrefixedText() ),
                        'target=' . $this->mTargetObj->getPrefixedUrl()
                );
@@ -625,7 +707,7 @@ class UndeleteForm {
                
                if( $this->mPreview ) {
                        $wgOut->addHtml( "<hr />\n" );
-                       $wgOut->addWikiTextTitleTidy( $rev->getText(), $this->mTargetObj, false );
+                       $wgOut->addWikiTextTitleTidy( $rev->revText(), $this->mTargetObj, false );
                }
 
                $wgOut->addHtml(
@@ -633,7 +715,7 @@ class UndeleteForm {
                                        'readonly' => 'readonly',
                                        'cols' => intval( $wgUser->getOption( 'cols' ) ),
                                        'rows' => intval( $wgUser->getOption( 'rows' ) ) ),
-                               $rev->getText() . "\n" ) .
+                               $rev->revText() . "\n" ) .
                        wfOpenElement( 'div' ) .
                        wfOpenElement( 'form', array(
                                'method' => 'post',
@@ -660,11 +742,74 @@ class UndeleteForm {
                        wfCloseElement( 'form' ) .
                        wfCloseElement( 'div' ) );
        }
+
+       /**
+        * Show the changes between two deleted revisions
+        */     
+       private function showDiff( $newid, $oldid ) {
+               global $wgOut, $wgUser, $wgLang;
+       
+               if( is_null($this->mTargetObj) )
+                       return;
+               $skin = $wgUser->getSkin();
+               
+               $archive = new PageArchive( $this->mTargetObj );
+               $oldRev = $archive->getRevision( null, $oldid );
+               $newRev = $archive->getRevision( null, $newid );
+               
+               if( !$oldRev || !$newRev )
+                       return;
+                       
+               $oldTitle = $this->mTargetObj->getPrefixedText();
+               $wgOut->addHtml( "<center><h3>$oldTitle</h3></center>" );
+               
+               $oldminor = $newminor = '';
+               
+               if($oldRev->mMinorEdit == 1) {
+                       $oldminor = wfElement( 'span', array( 'class' => 'minor' ),
+                               wfMsg( 'minoreditletter') ) . ' ';
+               }
+
+               if($newRev->mMinorEdit == 1) {
+                       $newminor = wfElement( 'span', array( 'class' => 'minor' ),
+                       wfMsg( 'minoreditletter') ) . ' ';
+               }
+               
+               $ot = $wgLang->timeanddate( $oldRev->getTimestamp(), true );
+               $nt = $wgLang->timeanddate( $newRev->getTimestamp(), true );
+               $oldHeader = htmlspecialchars( wfMsg( 'revisionasof', $ot ) ) . "<br />" .
+                       $skin->revUserTools( $oldRev, true ) . "<br />" .
+                       $oldminor . $skin->revComment( $oldRev, false ) . "<br />";
+               $newHeader = htmlspecialchars( wfMsg( 'revisionasof', $nt ) ) . "<br />" .
+                       $skin->revUserTools( $newRev, true ) . " <br />" .
+                       $newminor . $skin->revComment( $newRev, false ) . "<br />";
+               
+               $otext = $oldRev->revText();
+               $ntext = $newRev->revText();
+               
+               $wgOut->addStyle( 'common/diff.css' );
+        $wgOut->addHtml(
+            "<div>" .
+            "<table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>" .
+            "<col class='diff-marker' />" .
+            "<col class='diff-content' />" .
+            "<col class='diff-marker' />" .
+            "<col class='diff-content' />" .
+            "<tr>" .
+                "<td colspan='2' width='50%' align='center' class='diff-otitle'>" . $oldHeader . "</td>" .
+                "<td colspan='2' width='50%' align='center' class='diff-ntitle'>" . $newHeader . "</td>" .
+            "</tr>" .
+            DifferenceEngine::generateDiffBody( $otext, $ntext ) .
+            "</table>" .
+            "</div>\n" );
+                       
+               return true;
+       }
        
        /**
         * Show a deleted file version requested by the visitor.
         */
-       function showFile( $key ) {
+       private function showFile( $key ) {
                global $wgOut, $wgRequest;
                $wgOut->disable();
                
@@ -680,47 +825,35 @@ class UndeleteForm {
                $store->stream( $key );
        }
 
-       /* private */ function showHistory() {
+       private function showHistory() {
                global $wgLang, $wgContLang, $wgUser, $wgOut;
 
-               $sk = $wgUser->getSkin();
-               if ( $this->mAllowed ) {
+               $this->sk = $wgUser->getSkin();
+               if( $this->mAllowed ) {
                        $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
                } else {
                        $wgOut->setPagetitle( wfMsg( 'viewdeletedpage' ) );
                }
+               
+               $wgOut->addWikiText( wfMsgHtml( 'undeletepagetitle', $this->mTargetObj->getPrefixedText()) );
 
                $archive = new PageArchive( $this->mTargetObj );
-               /*
-               $text = $archive->getLastRevisionText();
-               if( is_null( $text ) ) {
-                       $wgOut->addWikiText( wfMsg( "nohistory" ) );
-                       return;
-               }
-               */
-               if ( $this->mAllowed ) {
-                       $wgOut->addWikiText( wfMsg( "undeletehistory" ) );
+
+               if( $this->mAllowed ) {
+                       $wgOut->addWikiText( '<p>' . wfMsgHtml( "undeletehistory" ) . '</p>' );
+                       $wgOut->addHtml( '<p>' . wfMsgHtml( "undeleterevdel" ) . '</p>' );
                } else {
-                       $wgOut->addWikiText( wfMsg( "undeletehistorynoadmin" ) );
+                       $wgOut->addWikiText( wfMsgHtml( "undeletehistorynoadmin" ) );
                }
 
                # List all stored revisions
-               $revisions = $archive->listRevisions();
+               $revisions = new UndeleteRevisionsPager( $this, array(), $this->mTargetObj );   
                $files = $archive->listFiles();
 
-               $haveRevisions = $revisions && $revisions->numRows() > 0;
+               $haveRevisions = $revisions && $revisions->getNumRows() > 0;
                $haveFiles = $files && $files->numRows() > 0;
 
                # Batch existence check on user and talk pages
-               if( $haveRevisions ) {
-                       $batch = new LinkBatch();
-                       while( $row = $revisions->fetchObject() ) {
-                               $batch->addObj( Title::makeTitleSafe( NS_USER, $row->ar_user_text ) );
-                               $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ar_user_text ) );
-                       }
-                       $batch->execute();
-                       $revisions->seek( 0 );
-               }
                if( $haveFiles ) {
                        $batch = new LinkBatch();
                        while( $row = $files->fetchObject() ) {
@@ -731,7 +864,7 @@ class UndeleteForm {
                        $files->seek( 0 );
                }
 
-               if ( $this->mAllowed ) {
+               if( $this->mAllowed ) {
                        $titleObj = SpecialPage::getTitleFor( "Undelete" );
                        $action = $titleObj->getLocalURL( "action=submit" );
                        # Start the form here
@@ -739,20 +872,6 @@ class UndeleteForm {
                        $wgOut->addHtml( $top );
                }
 
-               # Show relevant lines from the deletion log:
-               $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
-               $logViewer = new LogViewer(
-                       new LogReader(
-                               new FauxRequest(
-                                       array( 
-                                               'page' => $this->mTargetObj->getPrefixedText(),
-                                               'type' => 'delete' 
-                                       ) 
-                               )
-                       ), LogViewer::NO_ACTION_LINK
-               );
-               $logViewer->showList( $wgOut );
-
                if( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) {
                        # Format the user-visible controls (comment field, submission button)
                        # in a nice little table
@@ -778,6 +897,10 @@ class UndeleteForm {
                                                <td>" .
                                                        Xml::submitButton( wfMsg( 'undeletebtn' ), array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) .
                                                        Xml::element( 'input', array( 'type' => 'reset', 'value' => wfMsg( 'undeletereset' ), 'id' => 'mw-undelete-reset' ) ) .
+                                                       Xml::openElement( 'p' ) .
+                                                       Xml::check( 'wpUnsuppress', $this->mUnsuppress, array('id' => 'mw-undelete-unsupress') ) . ' ' .
+                                                       Xml::label( wfMsgHtml('revdelete-unsuppress'), 'mw-undelete-unsupress' ) .
+                                                       Xml::closeElement( 'p' ) .
                                                "</td>
                                        </tr>" .
                                Xml::closeElement( 'table' ) .
@@ -786,59 +909,38 @@ class UndeleteForm {
                        $wgOut->addHtml( $table );
                }
 
-               $wgOut->addHTML( "<h2>" . htmlspecialchars( wfMsg( "history" ) ) . "</h2>\n" );
+               $wgOut->addHTML( "<h2 id=\"pagehistory\">" . wfMsgHtml( "history" ) . "</h2>\n" );
 
                if( $haveRevisions ) {
-                       # The page's stored (deleted) history:
-                       $wgOut->addHTML("<ul>");
-                       $target = urlencode( $this->mTarget );
-                       while( $row = $revisions->fetchObject() ) {
-                               $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
-                               if ( $this->mAllowed ) {
-                                       $checkBox = Xml::check( "ts$ts" );
-                                       $pageLink = $sk->makeKnownLinkObj( $titleObj,
-                                               $wgLang->timeanddate( $ts, true ),
-                                               "target=$target&timestamp=$ts" );
-                               } else {
-                                       $checkBox = '';
-                                       $pageLink = $wgLang->timeanddate( $ts, true );
-                               }
-                               $userLink = $sk->userLink( $row->ar_user, $row->ar_user_text ) . $sk->userToolLinks( $row->ar_user, $row->ar_user_text );
-                               $stxt = '';
-                               if (!is_null($size = $row->ar_len)) {
-                                       if ($size == 0) {
-                                               $stxt = wfMsgHtml('historyempty');
-                                       } else {
-                                               $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) );
-                                       }
-                               }
-                               $comment = $sk->commentBlock( $row->ar_comment );
-                               $wgOut->addHTML( "<li>$checkBox $pageLink . . $userLink $stxt $comment</li>\n" );
-
-                       }
-                       $revisions->free();
-                       $wgOut->addHTML("</ul>");
+                       $wgOut->addHTML( '<p>' . wfMsgHtml( "restorepoint" ) . '</p>' );
+                       $wgOut->addHTML( $revisions->getNavigationBar() );
+                       $wgOut->addHTML( "<ul>" );
+                       $wgOut->addHTML( "<li>" . wfRadio( "restorepoint", -1, false ) . " " . wfMsgHtml('restorenone') . "</li>" );
+                       $wgOut->addHTML( $revisions->getBody() );
+                       $wgOut->addHTML( "</ul>" );
+                       $wgOut->addHTML( $revisions->getNavigationBar() );
                } else {
                        $wgOut->addWikiText( wfMsg( "nohistory" ) );
                }
-
                if( $haveFiles ) {
-                       $wgOut->addHtml( "<h2>" . wfMsgHtml( 'filehist' ) . "</h2>\n" );
+                       $wgOut->addHtml( "<h2 id=\"filehistory\">" . wfMsgHtml( 'filehist' ) . "</h2>\n" );
+                       $wgOut->addHTML( wfMsgHtml( "restorepoint" ) );
                        $wgOut->addHtml( "<ul>" );
+                       $wgOut->addHTML( "<li>" . wfRadio( "imgrestorepoint", -1, false ) . " " . wfMsgHtml('restorenone') . "</li>" );
                        while( $row = $files->fetchObject() ) {
+                               $file = ArchivedFile::newFromRow( $row );
+                       
                                $ts = wfTimestamp( TS_MW, $row->fa_timestamp );
-                               if ( $this->mAllowed && $row->fa_storage_key ) {
-                                       $checkBox = Xml::check( "fileid" . $row->fa_id );
+                               if( $this->mAllowed && $row->fa_storage_key ) {
+                                       $checkBox = wfRadio( "imgrestorepoint", $ts, false );
                                        $key = urlencode( $row->fa_storage_key );
                                        $target = urlencode( $this->mTarget );
-                                       $pageLink = $sk->makeKnownLinkObj( $titleObj,
-                                               $wgLang->timeanddate( $ts, true ),
-                                               "target=$target&file=$key" );
+                                       $pageLink = $this->getFileLink( $file, $titleObj, $ts, $target, $key );
                                } else {
                                        $checkBox = '';
                                        $pageLink = $wgLang->timeanddate( $ts, true );
                                }
-                               $userLink = $sk->userLink( $row->fa_user, $row->fa_user_text ) . $sk->userToolLinks( $row->fa_user, $row->fa_user_text );
+                               $userLink = $this->getFileUser( $file );
                                $data =
                                        wfMsgHtml( 'widthheight',
                                                $wgLang->formatNum( $row->fa_width ),
@@ -846,14 +948,40 @@ class UndeleteForm {
                                        ' (' .
                                        wfMsgHtml( 'nbytes', $wgLang->formatNum( $row->fa_size ) ) .
                                        ')';
-                               $comment = $sk->commentBlock( $row->fa_description );
-                               $wgOut->addHTML( "<li>$checkBox $pageLink . . $userLink $data $comment</li>\n" );
+                               $comment = $this->getFileComment( $file );
+                               $rd='';
+                               if( $wgUser->isAllowed( 'deleterevision' ) ) {
+                                       $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+                                       if( !$file->userCan(File::DELETED_RESTRICTED ) ) {
+                                       // If revision was hidden from sysops
+                                               $del = $this->message['rev-delundel'];
+                                       } else {
+                                               $del = $this->sk->makeKnownLinkObj( $revdel,
+                                                       $this->message['rev-delundel'],
+                                                       'target=' . urlencode( $this->mTarget ) .
+                                                       '&fileid=' . urlencode( $row->fa_id ) );
+                                               // Bolden oversighted content
+                                               if( $file->isDeleted( File::DELETED_RESTRICTED ) )
+                                                       $del = "<strong>$del</strong>";
+                                       }
+                                       $rd = "<tt>(<small>$del</small>)</tt>";
+                               }
+                               $wgOut->addHTML( "<li>$checkBox $rd $pageLink . . $userLink $data $comment</li>\n" );
                        }
                        $files->free();
                        $wgOut->addHTML( "</ul>" );
                }
 
-               if ( $this->mAllowed ) {
+               # Show relevant lines from the deletion log:
+               $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
+               $logViewer = new LogViewer(
+                       new LogReader(
+                               new FauxRequest(
+                                       array( 'page' => $this->mTargetObj->getPrefixedText(),
+                                                  'type' => 'delete' ) ) ) );
+               $logViewer->showList( $wgOut );
+               
+               if( $this->mAllowed ) {
                        # Slip in the hidden controls here
                        $misc  = Xml::hidden( 'target', $this->mTarget );
                        $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() );
@@ -863,6 +991,134 @@ class UndeleteForm {
 
                return true;
        }
+       
+       function formatRevisionRow( $row ) {
+               global $wgUser, $wgLang;
+               
+               $rev = new Revision( array(
+                               'page'       => $this->mTargetObj->getArticleId(),
+                               'id'         => $row->ar_rev_id,
+                               'comment'    => $row->ar_comment,
+                               'user'       => $row->ar_user,
+                               'user_text'  => $row->ar_user_text,
+                               'timestamp'  => $row->ar_timestamp,
+                               'minor_edit' => $row->ar_minor_edit,
+                               'text_id'    => $row->ar_text_id,
+                               'deleted'    => $row->ar_deleted,
+                               'len'        => $row->ar_len) );
+               
+               $stxt = ''; 
+               $last = $this->message['last'];
+               
+               if( $this->mAllowed ) {
+                       $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
+                       $checkBox = wfRadio( "restorepoint", $ts, false );
+                       $titleObj = SpecialPage::getTitleFor( "Undelete" );
+                       $pageLink = $this->getPageLink( $rev, $titleObj, $ts, $this->mTarget );
+                       # Last link
+                       if( !$rev->userCan( Revision::DELETED_TEXT ) )
+                               $last = $this->message['last'];
+                       else if( isset($this->prevId[$row->ar_rev_id]) )
+                               $last = $this->sk->makeKnownLinkObj( $titleObj, $this->message['last'], "target=" . $this->mTarget .
+                               "&diff=" . $row->ar_rev_id . "&oldid=" . $this->prevId[$row->ar_rev_id] );
+               } else {
+                       $checkBox = '';
+                       $pageLink = $wgLang->timeanddate( $ts, true );
+               }
+               $userLink = $this->sk->revUserTools( $rev );
+               
+               if(!is_null($size = $row->ar_len)) {
+                       if($size == 0)
+                               $stxt = wfMsgHtml('historyempty');
+                       else
+                               $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) );
+               }
+               $comment = $this->sk->revComment( $rev );
+               $revd='';
+               if( $wgUser->isAllowed( 'deleterevision' ) ) {
+                       $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+                       if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
+                       // If revision was hidden from sysops
+                               $del = $this->message['rev-delundel'];                  
+                       } else {
+                               $del = $this->sk->makeKnownLinkObj( $revdel,
+                                       $this->message['rev-delundel'],
+                                       'target=' . urlencode( $this->mTarget ) .
+                                       '&artimestamp=' . urlencode( $row->ar_timestamp ) );
+                               // Bolden oversighted content
+                               if( $rev->isDeleted( Revision::DELETED_RESTRICTED ) )
+                                       $del = "<strong>$del</strong>";
+                       }
+                       $revd = "<tt>(<small>$del</small>)</tt>";
+               }
+               
+               return "<li>$checkBox $revd ($last) $pageLink . . $userLink $stxt $comment</li>";
+       }
+
+       /**
+        * Fetch revision text link if it's available to all users
+        * @return string
+        */
+       function getPageLink( $rev, $titleObj, $ts, $target ) {
+               global $wgLang;
+               
+               if( !$rev->userCan(Revision::DELETED_TEXT) ) {
+                       return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
+               } else {
+                       $link = $this->sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ), "target=$target&timestamp=$ts" );
+                       if( $rev->isDeleted(Revision::DELETED_TEXT) )
+                               $link = '<span class="history-deleted">' . $link . '</span>';
+                       return $link;
+               }
+       }
+       
+       /**
+        * Fetch image view link if it's available to all users
+        * @return string
+        */
+       function getFileLink( $file, $titleObj, $ts, $target, $key ) {
+               global $wgLang;
+
+               if( !$file->userCan(File::DELETED_FILE) ) {
+                       return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
+               } else {
+                       $link = $this->sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ), "target=$target&file=$key" );
+                       if( $file->isDeleted(File::DELETED_FILE) )
+                               $link = '<span class="history-deleted">' . $link . '</span>';
+                       return $link;
+               }
+       }
+
+       /**
+        * Fetch file's user id if it's available to this user
+        * @return string
+        */
+       function getFileUser( $file ) { 
+               if( !$file->userCan(File::DELETED_USER) ) {
+                       return '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
+               } else {
+                       $link = $this->sk->userLink( $file->user, $file->userText ) . 
+                               $this->sk->userToolLinks( $file->user, $file->userText );
+                       if( $file->isDeleted(File::DELETED_USER) )
+                               $link = '<span class="history-deleted">' . $link . '</span>';
+                       return $link;
+               }
+       }
+
+       /**
+        * Fetch file upload comment if it's available to this user
+        * @return string
+        */
+       function getFileComment( $file ) {
+               if( !$file->userCan(File::DELETED_COMMENT) ) {
+                       return '<span class="history-deleted"><span class="comment">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span></span>';
+               } else {
+                       $link = $this->sk->commentBlock( $file->description );
+                       if( $file->isDeleted(File::DELETED_COMMENT) )
+                               $link = '<span class="history-deleted">' . $link . '</span>';
+                       return $link;
+               }
+       }
 
        function undelete() {
                global $wgOut, $wgUser;
@@ -870,16 +1126,17 @@ class UndeleteForm {
                        $archive = new PageArchive( $this->mTargetObj );
                        
                        $ok = $archive->undelete(
-                               $this->mTargetTimestamp,
+                               $this->mPageTimestamp,
                                $this->mComment,
-                               $this->mFileVersions );
-
+                               $this->mFileTimestamp,
+                               $this->mUnsuppress );
                        if( $ok ) {
                                $skin = $wgUser->getSkin();
-                               $link = $skin->makeKnownLinkObj( $this->mTargetObj );
+                               $link = $skin->makeKnownLinkObj( $this->mTargetObj, $this->mTargetObj->getPrefixedText(), 'redirect=no' );
                                $wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) );
                        } else {
                                $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
+                               $wgOut->addHtml( '<p>' . wfMsgHtml( "undeleterevdel" ) . '</p>' );
                        }
 
                        // Show file deletion warnings and errors
@@ -894,4 +1151,61 @@ class UndeleteForm {
        }
 }
 
+class UndeleteRevisionsPager extends ReverseChronologicalPager {
+       public $mForm, $mConds;
 
+       function __construct( $form, $conds = array(), $title ) {
+               $this->mForm = $form;
+               $this->mConds = $conds;
+               $this->title = $title;
+               parent::__construct();
+       }
+       
+       function getStartBody() {
+               wfProfileIn( __METHOD__ );
+               # Do a link batch query
+               $this->mResult->seek( 0 );
+               $batch = new LinkBatch();
+               # Give some pointers to make (last) links
+               $this->mForm->prevId = array();
+               while( $row = $this->mResult->fetchObject() ) {
+                       $batch->addObj( Title::makeTitleSafe( NS_USER, $row->ar_user_text ) );
+                       $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ar_user_text ) );
+                       
+                       $rev_id = isset($rev_id) ? $rev_id : $row->ar_rev_id;
+                       if( $rev_id > $row->ar_rev_id )
+                               $this->mForm->prevId[$rev_id] = $row->ar_rev_id;
+                       else if( $rev_id < $row->ar_rev_id )
+                               $this->mForm->prevId[$row->ar_rev_id] = $rev_id;
+                       
+                       $rev_id = $row->ar_rev_id;
+               }
+               
+               $batch->execute();
+               $this->mResult->seek( 0 );
+
+               wfProfileOut( __METHOD__ );
+               return '';
+       }
+       
+       function formatRow( $row ) {
+               $block = new Block;
+               return $this->mForm->formatRevisionRow( $row );
+       }
+
+       function getQueryInfo() {
+               $conds = $this->mConds;
+               $conds['ar_namespace'] = $this->title->getNamespace();
+               $conds['ar_title'] = $this->title->getDBkey();
+               return array(
+                       'tables' => array('archive'),
+                       'fields' => array( 'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text', 'ar_comment', 
+                               'ar_rev_id', 'ar_text_id', 'ar_len', 'ar_deleted' ),
+                       'conds' => $conds
+               );
+       }
+
+       function getIndexField() {
+               return 'ar_timestamp';
+       }
+}
index e9aa7e6..be42bf5 100644 (file)
@@ -161,7 +161,8 @@ function wfSpecialWatchlist( $par ) {
                $andLatest='';
                $limitWatchlist = 'LIMIT ' . intval( $wgUser->getOption( 'wllimit' ) );
        } else {
-               $andLatest= 'AND rc_this_oldid=page_latest';
+       # Top log Ids for a page are not stored
+               $andLatest = 'AND (rc_this_oldid=page_latest OR rc_type=' . RC_LOG . ') ';
                $limitWatchlist = '';
        }
 
@@ -364,4 +365,4 @@ function wlCountItems( &$user, $talk = true ) {
                $count = floor( $count / 2 );
 
        return( $count );
-}
\ No newline at end of file
+}
index dc4a763..428f10e 100644 (file)
@@ -2444,6 +2444,38 @@ class Title {
                return $row === false;
        }
        
+       /**
+        * Checks if the deleted history of another page can be merged into the same title as $this
+        * - Selects for update, so don't call it unless you mean business
+        */
+       public function isValidRestoreOverTarget() {
+
+               $fname = 'Title::isValidRestoreOverTarget';
+               $dbw = wfGetDB( DB_MASTER );
+
+               # Is it a redirect?
+               $page_is_redirect = $dbw->selectField( 'page', 'page_is_redirect',
+                       array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ),
+                       $fname, 'FOR UPDATE' );
+
+               if ( !$page_is_redirect ) {
+                       # Not a redirect
+                       wfDebug( __METHOD__ . ": not a redirect\n" );
+                       return false;
+               }
+
+               # Does the article have a history?
+               $row = $dbw->selectRow( array('page','revision'),
+                       array( 'rev_id' ),
+                       array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform,
+                               'page_id=rev_page AND page_latest != rev_id'
+                       ), $fname, 'FOR UPDATE'
+               );
+
+               # Return true if there was no history
+               return $row === false;
+       }
+       
        /**
         * Can this title be added to a user's watchlist?
         *
index bd9ff63..38fcc0a 100644 (file)
@@ -5,24 +5,49 @@
  */
 class ArchivedFile
 {
+       function ArchivedFile( $title, $id=0, $key='' ) {
+               if( !is_object( $title ) ) {
+                       throw new MWException( 'ArchivedFile constructor given bogus title.' );
+               }
+               $this->id = -1;
+               $this->title = $title;
+               $this->name = $title->getDBKey();
+               $this->group = '';
+               $this->key = '';
+               $this->size = 0;
+               $this->bits = 0;
+               $this->width = 0;
+               $this->height = 0;
+               $this->metaData = '';
+               $this->mime = "unknown/unknown";
+               $this->type = '';
+               $this->description = '';
+               $this->user = 0;
+               $this->userText = '';
+               $this->timestamp = NULL;
+               $this->deleted = 0;
+               # BC, load if these are specified
+               if( $id || $key ) {
+                       $this->load();
+               }
+       }
+
        /**
-        * Returns a file object from the filearchive table
-        * @param $title, the corresponding image page title
-        * @param $id, the image id, a unique key
-        * @param $key, optional storage key
+        * Loads a file object from the filearchive table
         * @return ResultWrapper
         */
-       function ArchivedFile( $title, $id=0, $key='' ) {
-               if( !is_object( $title ) ) {
+       function load() {
+               if( !is_object( $this->title ) ) {
                        throw new MWException( 'ArchivedFile constructor given bogus title.' );
                }
-               $conds = ($id) ? "fa_id = $id" : "fa_storage_key = '$key'";
-               if( $title->getNamespace() == NS_IMAGE ) {
+               $conds = ($this->id) ? "fa_id = {$this->id}" : "fa_storage_key = '{$this->key}'";
+               if( $this->title->getNamespace() == NS_IMAGE ) {
                        $dbr = wfGetDB( DB_SLAVE );
                        $res = $dbr->select( 'filearchive',
                                array(
                                        'fa_id',
                                        'fa_name',
+                                       'fa_archive_name',
                                        'fa_storage_key',
                                        'fa_storage_group',
                                        'fa_size',
@@ -39,7 +64,7 @@ class ArchivedFile
                                        'fa_timestamp',
                                        'fa_deleted' ),
                                array( 
-                                       'fa_name' => $title->getDbKey(),
+                                       'fa_name' => $this->title->getDBKey(),
                                        $conds ),
                                __METHOD__,
                                array( 'ORDER BY' => 'fa_timestamp DESC' ) );
@@ -52,22 +77,23 @@ class ArchivedFile
                        $row = $ret->fetchObject();
        
                        // initialize fields for filestore image object
-                       $this->mId = intval($row->fa_id);
-                       $this->mName = $row->fa_name;
-                       $this->mGroup = $row->fa_storage_group;
-                       $this->mKey = $row->fa_storage_key;
-                       $this->mSize = $row->fa_size;
-                       $this->mBits = $row->fa_bits;
-                       $this->mWidth = $row->fa_width;
-                       $this->mHeight = $row->fa_height;
-                       $this->mMetaData = $row->fa_metadata;
-                       $this->mMime = "$row->fa_major_mime/$row->fa_minor_mime";
-                       $this->mType = $row->fa_media_type;
-                       $this->mDescription = $row->fa_description;
-                       $this->mUser = $row->fa_user;
-                       $this->mUserText = $row->fa_user_text;
-                       $this->mTimestamp = $row->fa_timestamp;
-                       $this->mDeleted = $row->fa_deleted;             
+                       $this->id = intval($row->fa_id);
+                       $this->name = $row->fa_name;
+                       $this->archive_name = $row->fa_archive_name;
+                       $this->group = $row->fa_storage_group;
+                       $this->key = $row->fa_storage_key;
+                       $this->size = $row->fa_size;
+                       $this->bits = $row->fa_bits;
+                       $this->width = $row->fa_width;
+                       $this->height = $row->fa_height;
+                       $this->metaData = $row->fa_metadata;
+                       $this->mime = "$row->fa_major_mime/$row->fa_minor_mime";
+                       $this->type = $row->fa_media_type;
+                       $this->description = $row->fa_description;
+                       $this->user = $row->fa_user;
+                       $this->userText = $row->fa_user_text;
+                       $this->timestamp = $row->fa_timestamp;
+                       $this->deleted = $row->fa_deleted;
                } else {
                        throw new MWException( 'This title does not correspond to an image page.' );
                        return;
@@ -75,13 +101,41 @@ class ArchivedFile
                return true;
        }
 
+       /**
+        * Loads a file object from the filearchive table
+        * @return ResultWrapper
+        */     
+       public static function newFromRow( $row ) {     
+               $file = new ArchivedFile( Title::makeTitle( NS_IMAGE, $row->fa_name ) );
+               
+               $file->id = intval($row->fa_id);
+               $file->name = $row->fa_name;
+               $file->archive_name = $row->fa_archive_name;
+               $file->group = $row->fa_storage_group;
+               $file->key = $row->fa_storage_key;
+               $file->size = $row->fa_size;
+               $file->bits = $row->fa_bits;
+               $file->width = $row->fa_width;
+               $file->height = $row->fa_height;
+               $file->metaData = $row->fa_metadata;
+               $file->mime = "$row->fa_major_mime/$row->fa_minor_mime";
+               $file->type = $row->fa_media_type;
+               $file->description = $row->fa_description;
+               $file->user = $row->fa_user;
+               $file->userText = $row->fa_user_text;
+               $file->timestamp = $row->fa_timestamp;
+               $file->deleted = $row->fa_deleted;
+               
+               return $file;
+       }
+
        /**
         * int $field one of DELETED_* bitfield constants
         * for file or revision rows
         * @return bool
         */
        function isDeleted( $field ) {
-               return ($this->mDeleted & $field) == $field;
+               return ($this->deleted & $field) == $field;
        }
        
        /**
@@ -91,18 +145,16 @@ class ArchivedFile
         * @return bool
         */
        function userCan( $field ) {
-               if( isset($this->mDeleted) && ($this->mDeleted & $field) == $field ) {
+               if( isset($this->deleted) && ($this->deleted & $field) == $field ) {
                // images
                        global $wgUser;
-                       $permission = ( $this->mDeleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED
+                       $permission = ( $this->deleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED
                                ? 'hiderevision'
                                : 'deleterevision';
-                       wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" );
+                       wfDebug( "Checking for $permission due to $field match on $this->deleted\n" );
                        return $wgUser->isAllowed( $permission );
                } else {
                        return true;
                }
        }
 }
-
-
index 84ec9a2..f98d089 100644 (file)
@@ -6,7 +6,7 @@
  */
 
 class FSRepo extends FileRepo {
-       var $directory, $deletedDir, $url, $hashLevels, $deletedHashLevels;
+       var $directory, $deletedDir, $hiddenDir, $url, $hashLevels, $deletedHashLevels;
        var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' );
        var $oldFileFactory = false;
        var $pathDisclosureProtection = 'simple';
@@ -22,7 +22,10 @@ class FSRepo extends FileRepo {
                $this->hashLevels = isset( $info['hashLevels'] ) ? $info['hashLevels'] : 2;
                $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) ? 
                        $info['deletedHashLevels'] : $this->hashLevels;
+               $this->hiddenHashLevels = isset( $info['hiddenHashLevels'] ) ? 
+                       $info['hiddenHashLevels'] : $this->hashLevels;
                $this->deletedDir = isset( $info['deletedDir'] ) ? $info['deletedDir'] : false;
+               $this->hiddenDir = isset( $info['hiddenDir'] ) ? $info['hiddenDir'] : false;
        }
 
        /**
@@ -55,6 +58,8 @@ class FSRepo extends FileRepo {
                                return $this->directory;
                        case 'temp':
                                return "{$this->directory}/temp";
+                       case 'hidden':
+                               return $this->hiddenDir;
                        case 'deleted':
                                return $this->deletedDir;
                        default:
@@ -71,6 +76,8 @@ class FSRepo extends FileRepo {
                                return $this->url;
                        case 'temp':
                                return "{$this->url}/temp";
+                       case 'hidden':
+                               return false; // no public URL
                        case 'deleted':
                                return false; // no public URL
                        default:
@@ -417,7 +424,7 @@ class FSRepo extends FileRepo {
                                        $status->error( 'filedeleteerror', $srcPath );
                                        $good = false;
                                }
-                       } else{
+                       } else {
                                if ( !@rename( $srcPath, $archivePath ) ) {
                                        $status->error( 'filerenameerror', $srcPath, $archivePath );
                                        $good = false;
index fbf4d25..9928415 100644 (file)
@@ -380,6 +380,17 @@ abstract class File {
                return $this->getPath() && file_exists( $this->path );
        }
 
+       /**
+        * Returns true if file exists in the repository and can be included in a page.
+        * It would be unsafe to include private images, making public thumbnails inadvertently
+        *
+        * @return boolean Whether file exists in the repository and is includable.
+        * @public
+        */
+       function isVisible() { 
+               return $this->exists();
+       }
+
        function getTransformScript() {
                if ( !isset( $this->transformScript ) ) {
                        $this->transformScript = false;
@@ -600,7 +611,7 @@ abstract class File {
         * STUB
         * Overridden by LocalFile
         */
-       function purgeCache( $archiveFiles = array() ) {}
+       function purgeCache() {}
 
        /**
         * Purge the file description page, but don't go after
@@ -912,14 +923,13 @@ abstract class File {
         *
         * May throw database exceptions on error.
         *
-        * @param $versions set of record ids of deleted items to restore,
-        *                    or empty to restore all revisions.
+        * @param string $timestamp, restore all revisions since this time
         * @return the number of file revisions restored if successful,
         *         or false on failure
         * STUB
         * Overridden by LocalFile
         */
-       function restore( $versions=array(), $Unsuppress=false ) {
+       function restore( $timestamp = 0, $Unsuppress=false ) {
                $this->readOnlyError();
        }
 
index cf6d65c..b616661 100644 (file)
@@ -6,6 +6,7 @@
  */
 abstract class FileRepo {
        const DELETE_SOURCE = 1;
+       const FIND_PRIVATE = 1;
        const OVERWRITE = 2;
        const OVERWRITE_SAME = 4;
 
@@ -76,20 +77,23 @@ abstract class FileRepo {
         *
         * @param mixed $time 14-character timestamp, or false for the current version
         */
-       function findFile( $title, $time = false ) {
+       function findFile( $title, $time = false, $flags = 0 ) {
                # First try the current version of the file to see if it precedes the timestamp
                $img = $this->newFile( $title );
-               if ( !$img ) {
-                       return false;
-               }
-               if ( $img->exists() && ( !$time || $img->getTimestamp() <= $time ) ) {
+               # Check if the image exists and is of the specified timestamp
+               if ( $img->exists() && ( !$time || $img->getTimestamp()==$time ) ) {
                        return $img;
                }
                # Now try an old version of the file
                $img = $this->newFile( $title, $time );
                if ( $img->exists() ) {
-                       return $img;
+                       if( !$img->isDeleted(File::DELETED_FILE) ) {
+                               return $img;
+                       } else if( ($flags & FileRepo::FIND_PRIVATE) && $img->userCan(File::DELETED_FILE) ) {
+                               return $img;
+                       }
                }
+               return false;
        }
 
        /**
index 392bfa0..47d10ad 100644 (file)
@@ -46,7 +46,8 @@ class LocalFile extends File
                $sha1,          # SHA-1 base 36 content hash
                $dataLoaded,    # Whether or not all this has been loaded from the database (loadFromXxx)
                $upgraded,      # Whether the row was upgraded on load
-               $locked;        # True if the image row is locked
+               $locked,        # True if the image row is locked
+               $deleted;       # Bitfield akin to rev_deleted
 
        /**#@-*/
 
@@ -236,8 +237,13 @@ class LocalFile extends File
                        $this->$name = $value;
                }
                $this->fileExists = true;
-               // Check for rows from a previous schema, quietly upgrade them
-               $this->maybeUpgradeRow();
+               // Check if the file is hidden...
+               if( $this->isDeleted(File::DELETED_FILE) ) {
+                       $this->fileExists = false; // treat as not existing
+               } else {
+                       // Check for rows from a previous schema, quietly upgrade them
+                       $this->maybeUpgradeRow();
+               }
        }
 
        /**
@@ -572,7 +578,9 @@ class LocalFile extends File
                        $this->historyRes = $dbr->select( 'image', 
                                array(
                                        '*',
-                                       "'' AS oi_archive_name"
+                                       "'' AS oi_archive_name",
+                                       '0 as oi_deleted',
+                                       'img_sha1'
                                ),
                                array( 'img_name' => $this->title->getDBkey() ),
                                __METHOD__
@@ -741,7 +749,7 @@ class LocalFile extends File
                                        'oi_media_type' => 'img_media_type',
                                        'oi_major_mime' => 'img_major_mime',
                                        'oi_minor_mime' => 'img_minor_mime',
-                                       'oi_sha1' => 'img_sha1',
+                                       'oi_sha1' => 'img_sha1'
                                ), array( 'img_name' => $this->getName() ), __METHOD__
                        );
 
@@ -857,9 +865,9 @@ class LocalFile extends File
         * @param $reason
         * @return FileRepoStatus object.
         */
-       function delete( $reason ) {
+       function delete( $reason, $suppress=false ) {
                $this->lock();
-               $batch = new LocalFileDeleteBatch( $this, $reason );
+               $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
                $batch->addCurrent();
 
                # Get old version relative paths
@@ -895,9 +903,9 @@ class LocalFile extends File
         * @throws MWException or FSException on database or filestore failure
         * @return FileRepoStatus object.
         */
-       function deleteOld( $archiveName, $reason ) {
+       function deleteOld( $archiveName, $reason, $suppress=false ) {
                $this->lock();
-               $batch = new LocalFileDeleteBatch( $this, $reason );
+               $batch = new LocalFileDeleteBatch( $this, $reason, $suppress );
                $batch->addOld( $archiveName );
                $status = $batch->execute();
                $this->unlock();
@@ -908,22 +916,21 @@ class LocalFile extends File
                return $status;
        }
 
-       /**
+       /*
         * Restore all or specified deleted revisions to the given file.
         * Permissions and logging are left to the caller.
         *
         * May throw database exceptions on error.
         *
-        * @param $versions set of record ids of deleted items to restore,
-        *                    or empty to restore all revisions.
+        * @param string $timestamp, restore all revisions since this time
         * @return FileRepoStatus
         */
-       function restore( $versions = array(), $unsuppress = false ) {
+       function restore( $timestamp = 0, $unsuppress = false ) {
                $batch = new LocalFileRestoreBatch( $this );
-               if ( !$versions ) {
+               if ( !$timestamp ) {
                        $batch->addAll();
                } else {
-                       $batch->addIds( $versions );
+                       $batch->addAll( $timestamp );
                }
                $status = $batch->execute();
                if ( !$status->ok ) {
@@ -1103,9 +1110,10 @@ class LocalFileDeleteBatch {
        var $file, $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch;
        var $status;
 
-       function __construct( File $file, $reason = '' ) {
+       function __construct( File $file, $reason = '', $suppress=false ) {
                $this->file = $file;
                $this->reason = $reason;
+               $this->suppress = $suppress;
                $this->status = $file->repo->newGood();
        }
 
@@ -1186,6 +1194,16 @@ class LocalFileDeleteBatch {
                $dotExt = $ext === '' ? '' : ".$ext";
                $encExt = $dbw->addQuotes( $dotExt );
                list( $oldRels, $deleteCurrent ) = $this->getOldRels();
+               
+               // Bitfields to further suppress the content
+               if ( $this->suppress ) {
+                       $bitfield = 0;
+                       // This should be 15...
+                       $bitfield |= Revision::DELETED_TEXT;
+                       $bitfield |= Revision::DELETED_COMMENT;
+                       $bitfield |= Revision::DELETED_USER;
+                       $bitfield |= Revision::DELETED_RESTRICTED;
+               }
 
                if ( $deleteCurrent ) {
                        $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
@@ -1197,7 +1215,7 @@ class LocalFileDeleteBatch {
                                        'fa_deleted_user'      => $encUserId,
                                        'fa_deleted_timestamp' => $encTimestamp,
                                        'fa_deleted_reason'    => $encReason,
-                                       'fa_deleted'               => 0,
+                                       'fa_deleted'               => $this->suppress ? $bitfield : 0,
 
                                        'fa_name'         => 'img_name',
                                        'fa_archive_name' => 'NULL',
@@ -1228,7 +1246,7 @@ class LocalFileDeleteBatch {
                                        'fa_deleted_user'      => $encUserId,
                                        'fa_deleted_timestamp' => $encTimestamp,
                                        'fa_deleted_reason'    => $encReason,
-                                       'fa_deleted'               => 0,
+                                       'fa_deleted'               => $this->suppress ? $bitfield : 'oi_deleted',
 
                                        'fa_name'         => 'oi_name',
                                        'fa_archive_name' => 'oi_archive_name',
@@ -1271,7 +1289,25 @@ class LocalFileDeleteBatch {
                wfProfileIn( __METHOD__ );
 
                $this->file->lock();
-
+               // Use revisiondelete to handle private files
+               $privateFiles = array();
+               list( $oldRels, $deleteCurrent ) = $this->getOldRels();
+               $dbw = $this->file->repo->getMasterDB();
+               $revisionDeleter = new RevisionDeleter( $dbw );
+               if( !empty( $oldRels ) ) {
+                       $res = $dbw->select( 'oldimage', 
+                               array( 'oi_archive_name', 'oi_sha1' ),
+                               array( 'oi_name' => $this->file->getName(),
+                                       'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
+                                       'oi_deleted & '.File::DELETED_FILE => File::DELETED_FILE ),
+                               __METHOD__ );
+                       while( $row = $dbw->fetchObject( $res ) ) {
+                               $title = $this->file->getTitle();
+                               $oimage = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $title, $row->oi_archive_name );
+                               $oimage->sha1 = $row->oi_sha1;
+                               $privateFiles[$row->oi_archive_name] = $oimage;
+                       }
+               }
                // Prepare deletion batch
                $hashes = $this->getHashes();
                $this->deletionBatch = array();
@@ -1279,7 +1315,8 @@ class LocalFileDeleteBatch {
                $dotExt = $ext === '' ? '' : ".$ext";
                foreach ( $this->srcRels as $name => $srcRel ) {
                        // Skip files that have no hash (missing source)
-                       if ( isset( $hashes[$name] ) ) {
+                       // Move private files using revisiondelete
+                       if ( isset($hashes[$name]) && !array_key_exists($name,$privateFiles) ) {
                                $hash = $hashes[$name];
                                $key = $hash . $dotExt;
                                $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
@@ -1308,6 +1345,19 @@ class LocalFileDeleteBatch {
                        $this->file->unlockAndRollback();
                        return $this->status;
                }
+               
+               // Delete image/oldimage rows
+               $this->doDBDeletes();
+               
+               // Move private files to deletion archives
+               $revisionDeleter = new RevisionDeleter( $dbw );
+               foreach( $privateFiles as $name => $oimage ) {
+                       $ok = $revisionDeleter->moveImageFromFileRepos( $oimage, 'hidden', 'deleted' );
+                       if( $ok )
+                               $status->successCount++;
+                       else
+                               $status->failCount++;
+               }
 
                // Purge squid
                if ( $wgUseSquid ) {
@@ -1319,9 +1369,6 @@ class LocalFileDeleteBatch {
                        SquidUpdate::purge( $urls );
                }
 
-               // Delete image/oldimage rows
-               $this->doDBDeletes();
-
                // Commit and return
                $this->file->unlock();
                wfProfileOut( __METHOD__ );
@@ -1335,12 +1382,13 @@ class LocalFileDeleteBatch {
  * Helper class for file undeletion
  */
 class LocalFileRestoreBatch {
-       var $file, $cleanupBatch, $ids, $all, $unsuppress = false;
+       var $file, $cleanupBatch, $ids, $all;
 
-       function __construct( File $file ) {
+       function __construct( File $file, $unsuppress = false ) {
                $this->file = $file;
                $this->cleanupBatch = $this->ids = array();
                $this->ids = array();
+               $this->unsuppress = $unsuppress;
        }
 
        /**
@@ -1359,9 +1407,11 @@ class LocalFileRestoreBatch {
 
        /**
         * Add all revisions of the file
+        * Can be all from $timestamp if given
         */
-       function addAll() {
+       function addAll( $timestamp = false ) {
                $this->all = true;
+               $this->timestamp = $timestamp;
        }
        
        /**
@@ -1382,11 +1432,16 @@ class LocalFileRestoreBatch {
                $dbw = $this->file->repo->getMasterDB();
                $status = $this->file->repo->newGood();
                
+               $revisionDeleter = new RevisionDeleter( $dbw );
+               $privateFiles = array();
+               
                // Fetch all or selected archived revisions for the file,
                // sorted from the most recent to the oldest.
                $conditions = array( 'fa_name' => $this->file->getName() );
                if( !$this->all ) {
                        $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')';
+               } else if( $this->timestamp ) {
+                       $conditions[] = "fa_timestamp >= {$this->timestamp}";
                }
 
                $result = $dbw->select( 'filearchive', '*',
@@ -1403,12 +1458,7 @@ class LocalFileRestoreBatch {
                $archiveNames = array();
                while( $row = $dbw->fetchObject( $result ) ) {
                        $idsPresent[] = $row->fa_id;
-                       if ( $this->unsuppress ) {
-                               // Currently, fa_deleted flags fall off upon restore, lets be careful about this
-                       } else if ( ($row->fa_deleted & Revision::DELETED_RESTRICTED) && !$wgUser->isAllowed('hiderevision') ) {
-                               // Skip restoring file revisions that the user cannot restore
-                               continue;
-                       }
+
                        if ( $row->fa_name != $this->file->getName() ) {
                                $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
                                $status->failCount++;
@@ -1446,6 +1496,11 @@ class LocalFileRestoreBatch {
                        }
 
                        if ( $first && !$exists ) {
+                               // The live (current) version cannot be hidden!
+                               if( $row->fa_deleted ) {
+                                       $this->file->unlock();
+                                       return $status;
+                               }
                                // This revision will be published as the new current version
                                $destRel = $this->file->getRel();
                                $insertCurrent = array(
@@ -1492,13 +1547,21 @@ class LocalFileRestoreBatch {
                                        'oi_media_type'   => $props['media_type'],
                                        'oi_major_mime'   => $props['major_mime'],
                                        'oi_minor_mime'   => $props['minor_mime'],
-                                       'oi_deleted'      => $row->fa_deleted,
+                                       'oi_deleted'      => $this->unsuppress ? 0 : $row->fa_deleted,
                                        'oi_sha1'         => $sha1 );
                        }
 
                        $deleteIds[] = $row->fa_id;
-                       $storeBatch[] = array( $deletedUrl, 'public', $destRel );
-                       $this->cleanupBatch[] = $row->fa_storage_key;
+                       // Use revisiondelete to handle private files
+                       if( $row->fa_deleted & File::DELETED_FILE ) {
+                               $title = $this->file->getTitle();
+                               $oimage = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $title, $archiveName );
+                               $oimage->sha1 = $sha1;
+                               $privateFiles[$archiveName] = $oimage;
+                       } else {
+                               $storeBatch[] = array( $deletedUrl, 'public', $destRel );
+                               $this->cleanupBatch[] = $row->fa_storage_key;
+                       }
                        $first = false;
                }
                unset( $result );
@@ -1538,6 +1601,16 @@ class LocalFileRestoreBatch {
                                array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ), 
                                __METHOD__ );
                }
+               
+               // Immediatly move to private files to hidden directory
+               $revisionDeleter = new RevisionDeleter( $dbw );
+               foreach ( $privateFiles as $oimage ) {
+                       $ok = $revisionDeleter->moveImageFromFileRepos( $oimage, 'deleted', 'hidden' );
+                       if( $ok )
+                               $status->successCount++;
+                       else
+                               $status->failCount++;
+               }
 
                if( $status->successCount > 0 ) {
                        if( !$exists ) {
index 850a8d8..689ec09 100644 (file)
@@ -56,6 +56,10 @@ class OldLocalFile extends LocalFile {
        function isOld() {
                return true;
        }
+       
+       function isVisible() { 
+               return $this->exists() && !$this->isDeleted(File::DELETED_FILE);
+       }
 
        /**
         * Try to load file metadata from memcached. Returns true on success.
@@ -179,10 +183,8 @@ class OldLocalFile extends LocalFile {
        function getCacheFields( $prefix = 'img_' ) {
                $fields = parent::getCacheFields( $prefix );
                $fields[] = $prefix . 'archive_name';
-
-               // XXX: Temporary hack before schema update
-               //$fields = array_diff( $fields, array( 
-               //      'oi_media_type', 'oi_major_mime', 'oi_minor_mime', 'oi_metadata' ) );
+               $fields[] = $prefix . 'deleted';
+               
                return $fields;
        }
 
@@ -226,7 +228,35 @@ class OldLocalFile extends LocalFile {
                );
                wfProfileOut( __METHOD__ );
        }
-}
+       
+       /**
+        * int $field one of DELETED_* bitfield constants
+        * for file or revision rows
+        * @return bool
+        */
+       function isDeleted( $field ) {
+               return ($this->deleted & $field) == $field;
+       }
+       
+       /**
+        * Determine if the current user is allowed to view a particular
+        * field of this FileStore image file, if it's marked as deleted.
+        * @param int $field                                    
+        * @return bool
+        */
+       function userCan( $field ) {
+               if( isset($this->deleted) && ($this->deleted & $field) == $field ) {
+                       global $wgUser;
+                       $permission = ( $this->deleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED
+                               ? 'hiderevision'
+                               : 'deleterevision';
+                       wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" );
+                       return $wgUser->isAllowed( $permission );
+               } else {
+                       return true;
+               }
+       }
 
+}
 
 
index f2bb899..6792bea 100644 (file)
@@ -782,6 +782,8 @@ Please report this to an administrator, making note of the URL.',
 'formerror'            => 'Error: could not submit form',
 'badarticleerror'      => 'This action cannot be performed on this page.',
 'cannotdelete'         => 'Could not delete the page or file specified. (It may have already been deleted by someone else.)',
+'cannotdelete-merge'   => 'Pages cannot be deleted if a different page already has archived revisions under the same title. This can 
+happen if you move a page over another one and then delete it.',
 'badtitle'             => 'Bad title',
 'badtitletext'         => 'The requested page title was invalid, empty, or an incorrectly linked inter-language or inter-wiki title. It may contain one or more characters which cannot be used in titles.',
 'perfdisabled'         => 'Sorry! This feature has been temporarily disabled because it slows the database down to the point that no one can use the wiki.',
@@ -1146,10 +1148,11 @@ there may be details in the [{{fullurl:Special:Log/delete|page={{FULLPAGENAMEE}}
 </div>',
 'rev-delundel'                => 'show/hide',
 'revisiondelete'              => 'Delete/undelete revisions',
-'revdelete-nooldid-title'     => 'No target revision',
-'revdelete-nooldid-text'      => 'You have not specified target revision or revisions to perform this function on.',
-'revdelete-selected'          => "{{PLURAL:$2|Selected revision|Selected revisions}} of '''$1:'''",
-'logdelete-selected'          => "{{PLURAL:$2|Selected log event|Selected log events}} for '''$1:'''",
+'revdelete-nooldid-title'     => 'Invalid target revision',
+'revdelete-nooldid-text'      => 'You have either not specified a target revision(s) to perform this 
+function, the specified revision does not exist, or you are attempting to hide the current revision.',
+'revdelete-selected'          => "{{PLURAL:$2|Selected revision|Selected revisions}} of [[:$1]]:",
+'logdelete-selected'          => "{{PLURAL:$1|Selected log event|Selected log events}}:",
 'revdelete-text'              => 'Deleted revisions and events will still appear in the page history and logs,
 but parts of their content will be inaccessible to the public.
 
@@ -1160,7 +1163,7 @@ undelete it again through this same interface, unless additional restrictions ar
 'revdelete-hide-name'         => 'Hide action and target',
 'revdelete-hide-comment'      => 'Hide edit comment',
 'revdelete-hide-user'         => "Hide editor's username/IP",
-'revdelete-hide-restricted'   => 'Apply these restrictions to sysops as well as others',
+'revdelete-hide-restricted'   => 'Apply these restrictions to Sysops and lock this interface',
 'revdelete-suppress'          => 'Suppress data from sysops as well as others',
 'revdelete-hide-image'        => 'Hide file content',
 'revdelete-unsuppress'        => 'Remove restrictions on restored revisions',
@@ -1169,14 +1172,43 @@ undelete it again through this same interface, unless additional restrictions ar
 'revdelete-logentry'          => 'changed revision visibility of [[$1]]',
 'logdelete-logentry'          => 'changed event visibility of [[$1]]',
 'revdelete-logaction'         => '$1 {{PLURAL:$1|revision|revisions}} set to mode $2',
-'logdelete-logaction'         => '$1 {{PLURAL:$1|event|events}} to [[$3]] set to mode $2',
-'revdelete-success'           => 'Revision visibility successfully set.',
-'logdelete-success'           => 'Event visibility successfully set.',
+'logdelete-logaction'         => '$1 {{PLURAL:$1|event|events}} set to mode $2',
+'revdelete-success'           => "'''Revision visibility successfully set.'''",
+'logdelete-success'           => "'''Log visibility successfully set.'''",
+'revdel-restore'              => 'Change visiblity',
 
 # Oversight log
-'oversightlog'    => 'Oversight log',
-'overlogpagetext' => 'Below is a list of the most recent deletions and blocks involving content
-hidden from Sysops. See the [[Special:Ipblocklist|IP block list]] for the list of currently operational bans and blocks.',
+'oversightlog'    => 'Suppression log',
+'overlogpagetext' => 'Below is a list of the most recent deletions and blocks involving items  
+hidden from Sysops. Automatically blocked IP addresses are not listed. See the [[Special:Ipblocklist|IP block list]] 
+for the list of currently operational bans and blocks.
+
+Blocked users listed here can cannot edit their talk pages and thus can only communicate via email. Their accounts 
+will remain hidden only as long as they are blocked.',
+
+# History merging
+'mergehistory'          => 'Merge page histories',
+'mergehistory-header'   => 'This page lets you merge revisions of the history of one source page into a newer page.
+Please make sure that this change will maintain historical page continuity.
+
+At least the current revision of the source page must be left.',
+'mergehistory-box'      => 'Merge revisions of two pages:',
+'mergehistory-from'     => 'Source page:',
+'mergehistory-into'     => 'Destination page:',
+'mergehistory-list'     => 'Mergeable edit history',
+'mergehistory-merge'    => 'The following revisions of [[:$1|$1]] can be merged into [[:$2|$2]]. Use the radio 
+button column to merge in only the revisions created at or before the specified time. Note that you will have to 
+reselect any options if you use the navigation links.',
+'mergehistory-go'       => 'Show mergeable edits',
+'mergehistory-submit'   => 'Merge revisions',
+'mergehistory-empty'    => 'No revisions can be merged',
+'mergehistory-success'  => '$3 revisions of [[:$1]] successfully merged into [[:$2]].',
+'mergehistory-fail'     => 'Unable to perform history merge, please recheck the page and time parameters.',
+
+'mergelog'              => 'Merge log',
+'pagemerge-logentry'    => 'merged $1 into $2 (revisions up to $3)',
+'revertmerge'           => 'Unmerge',
+'mergelogpagetext'      => 'Below is a list of the most recent merges of one page history into another.',
 
 # Diffs
 'history-title'             => 'Revision history of "$1"',
@@ -1322,6 +1354,11 @@ Unselected groups will not be changed. You can deselect a group with CTRL + Left
 'grouppage-sysop'         => '{{ns:project}}:Administrators',
 'grouppage-bureaucrat'    => '{{ns:project}}:Bureaucrats',
 
+'oversight'              => 'Oversight',
+'group-oversight'        => 'Oversights',
+'group-oversight-member' => 'Oversight',
+'grouppage-oversight'    => '{{ns:project}}:Oversight',
+
 # User rights log
 'rightslog'      => 'User rights log',
 'rightslogtext'  => 'This is a log of changes to user rights.',
@@ -1665,6 +1702,7 @@ The [http://meta.wikimedia.org/wiki/Help:Job_queue job queue] length is '''\$7''
 'specialpages-summary'            => '', # only translate this message to other languages if you have to change it
 'spheading'                       => 'Special pages for all users',
 'restrictedpheading'              => 'Restricted special pages',
+'restrictedlheading'              => 'Restricted logs',
 'rclsub'                          => '(to pages linked from "$1")',
 'newpages'                        => 'New pages',
 'newpages-summary'                => '', # only translate this message to other languages if you have to change it
@@ -1703,10 +1741,10 @@ further information about books you are looking for:',
 'specialloguserlabel'  => 'User:',
 'speciallogtitlelabel' => 'Title:',
 'log'                  => 'Logs',
-'all-logs-page'        => 'All logs',
+'all-logs-page'        => 'All public logs',
 'log-search-legend'    => 'Search for logs',
 'log-search-submit'    => 'Go',
-'alllogstext'          => 'Combined display of all available logs of {{SITENAME}}.
+'alllogstext'          => 'Combined display of all available public logs of {{SITENAME}}.
 You can narrow down the view by selecting a log type, the user name, or the affected page.',
 'logempty'             => 'No matching items in log.',
 'log-title-wildcard'   => 'Search titles starting with this text',
@@ -1853,6 +1891,7 @@ consequences, and that you are doing this in accordance with
 'deletedtext'                 => '"$1" has been deleted.
 See $2 for a record of recent deletions.',
 'deletedarticle'              => 'deleted "[[$1]]"',
+'suppressedarticle'           => 'suppressed "[[$1]]"',
 'dellogpage'                  => 'Deletion log',
 'dellogpagetext'              => 'Below is a list of the most recent deletions.',
 'deletionlog'                 => 'deletion log',
@@ -1873,6 +1912,7 @@ Last edit was by [[User:$3|$3]] ([[User talk:$3|Talk]]).',
 'sessionfailure'              => 'There seems to be a problem with your login session;
 this action has been canceled as a precaution against session hijacking.
 Please hit "back" and reload the page you came from, then try again.',
+
 'protectlogpage'              => 'Protection log',
 'protectlogtext'              => 'Below is a list of page locks and unlocks. See the [[Special:Protectedpages|protected pages list]] for the list of currently operational page protections.',
 'protectedarticle'            => 'protected "[[$1]]"',
@@ -1880,6 +1920,7 @@ Please hit "back" and reload the page you came from, then try again.',
 'unprotectedarticle'          => 'unprotected "[[$1]]"',
 'protectsub'                  => '(Setting protection level for "$1")',
 'confirmprotect'              => 'Confirm protection',
+'protect-fileonly'            => 'Apply edit restrictions to file uploads only',
 'protectcomment'              => 'Comment:',
 'protectexpiry'               => 'Expires:',
 'protect_expiry_invalid'      => 'Expiry time is invalid.',
@@ -1910,6 +1951,7 @@ Here are the current settings for the page <strong>$1</strong>:',
 # Restrictions (nouns)
 'restriction-edit' => 'Edit',
 'restriction-move' => 'Move',
+'restriction-upload' => 'Upload',
 
 # Restriction levels
 'restriction-level-sysop'         => 'full protected',
@@ -1918,25 +1960,29 @@ Here are the current settings for the page <strong>$1</strong>:',
 
 # Undelete
 'undelete'                 => 'View deleted pages',
+'undeleterevs'             => 'Deleted revisions',
 'undeletepage'             => 'View and restore deleted pages',
 'viewdeletedpage'          => 'View deleted pages',
+'undeletepagetitle'        => '\'\'\'The following consists of deleted revisions of [[:$1]]\'\'\'.',
 'undeletepagetext'         => 'The following pages have been deleted but are still in the archive and
 can be restored. The archive may be periodically cleaned out.',
-'undeleteextrahelp'        => "To restore the entire page, leave all checkboxes deselected and
-click '''''Restore'''''. To perform a selective restoration, check the boxes corresponding to the
-revisions to be restored, and click '''''Restore'''''. Clicking '''''Reset''''' will clear the
-comment field and all checkboxes.",
+'undeleteextrahelp'        => "To restore the entire page, leave all radios deselected and click '''''Restore'''''. 
+To perform a selective restoration, check the desired restore point below and click '''''Restore'''''. 
+Clicking '''''Reset''''' will reset this form. Note that you will have to reselect any options if you 
+use the navigation links.",
 'undeleterevisions'        => '$1 {{PLURAL:$1|revision|revisions}} archived',
 'undeletehistory'          => 'If you restore the page, all revisions will be restored to the history.
 If a new page with the same name has been created since the deletion, the restored
 revisions will appear in the prior history, and the current revision of the live page
-will not be automatically replaced. Also note that restrictions on file revisions are lost upon restoration',
-'undeleterevdel'           => "Undeletion will not be performed if it will result in the top page revision being
-partially deleted. In such cases, you must uncheck or unhide the newest deleted revisions. Revisions of files
-that you don't have permission to view will not be restored.",
+will not be automatically replaced.',
+'undeleterevdel'           => 'Undeletion will not be performed if either it would result in the top page/image revision 
+being restricted. Histories of different pages cannot be merged unless the live page is a redirect with no edit history.',
 'undeletehistorynoadmin'   => 'This article has been deleted. The reason for deletion is
 shown in the summary below, along with details of the users who had edited this page
 before deletion. The actual text of these deleted revisions is only available to administrators.',
+'restorepoint'             => 'Use the radio button column to restore only revisions from the specified time onwards.',
+'restorenone'              => '(select this button to restore none of these revisions)',
+
 'undelete-revision' => 'Deleted revision of $1 (as of $2) by $3:',
 'undeleterevision-missing' => 'Invalid or missing revision. You may have a bad link, or the
 revision may have been restored or removed from the archive.',
index 5101815..1e14040 100644 (file)
@@ -11,7 +11,6 @@ function rebuildRecentChangesTablePass1()
 {
        $fname = 'rebuildRecentChangesTablePass1';
        $dbw = wfGetDB( DB_MASTER );
-       extract( $dbw->tableNames( 'recentchanges', 'cur', 'old' ) );
 
        $dbw->delete( 'recentchanges', '*' );
 
@@ -35,6 +34,7 @@ function rebuildRecentChangesTablePass1()
                        'rc_this_oldid' => 'rev_id',
                        'rc_last_oldid' => 0, // is this ok?
                        'rc_type'       => $dbw->conditional( 'page_is_new != 0', RC_NEW, RC_EDIT ),
+                       'rc_deleted'    => 'rev_deleted'
                ), array(
                        'rev_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $cutoff ) ),
                        'rev_page=page_id'
@@ -98,6 +98,70 @@ function rebuildRecentChangesTablePass2()
 }
 
 function rebuildRecentChangesTablePass3()
+{
+       $fname = 'rebuildRecentChangesTablePass3';
+       $dbw = wfGetDB( DB_MASTER );
+
+       print( "Loading from user, page, and logging tables...\n" );
+       
+       global $wgRCMaxAge, $wgLogRestrictions;
+       
+       // Exclude non-public logs
+       $avoidLogs = array();
+       if( isset($wgLogRestrictions) ) {
+               foreach ( $wgLogRestrictions as $logtype => $right ) {
+                       // Do not show private logs when not specifically requested
+                       if ( $right !='*' ) {
+                               $safetype =$dbw->strencode( $logtype );
+                               $avoidLogs[] = "'$safetype'";
+                       }
+               }
+       }
+       // Some logs don't go in RC. This can't really detect all of those ... :(
+       $avoidLogs[] = "'patrol'";      // hack...we don't want this here
+       
+       if( !empty($avoidLogs) ) {
+               $skipLogs = 'log_type NOT IN(' . implode(',',$avoidLogs) . ')';
+       } else {
+               $skipLogs = '1 = 1';
+       }
+       
+       $cutoff = time() - $wgRCMaxAge;
+       $dbw->insertSelect( 'recentchanges', array( 'logging', 'page', 'user' ),
+               array(
+                       'rc_timestamp'  => 'log_timestamp',
+                       'rc_cur_time'   => 'log_timestamp',
+                       'rc_user'       => 'log_user',
+                       'rc_user_text'  => 'user_name',
+                       'rc_namespace'  => 'log_namespace',
+                       'rc_title'      => 'log_title',
+                       'rc_comment'    => 'log_comment',
+                       'rc_minor'      => 0,
+                       'rc_bot'        => 0,
+                       'rc_new'        => 0,
+                       'rc_cur_id'     => 0,
+                       'rc_this_oldid' => 0,
+                       'rc_last_oldid' => 0,
+                       'rc_type'       => RC_LOG,
+                       'rc_cur_id'     => 'page_id',
+                       'rc_log_type'   => 'log_type',
+                       'rc_log_action' => 'log_action',
+                       'rc_logid'      => 'log_id',
+                       'rc_params'     => 'log_params',
+                       'rc_deleted'    => 'log_deleted'
+               ), array(
+                       'log_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $cutoff ) ),
+                       'log_user=user_id',
+                       'log_namespace=page_namespace',
+                       'log_title=page_title',
+                       $skipLogs
+               ), $fname,
+               array(), // INSERT options
+               array( 'ORDER BY' => 'log_timestamp DESC', 'LIMIT' => 5000 ) // SELECT options
+       );
+}
+
+function rebuildRecentChangesTablePass4()
 {
        global $wgGroupPermissions, $wgUseRCPatrol;
                        
index a94780e..7825625 100644 (file)
@@ -17,7 +17,8 @@ $wgDBpassword         = $wgDBadminpassword;
 
 rebuildRecentChangesTablePass1();
 rebuildRecentChangesTablePass2();
-rebuildRecentChangesTablePass3(); // flag bot edits
+rebuildRecentChangesTablePass3(); // logs entries
+rebuildRecentChangesTablePass4(); // flag bot edits
 
 print "Done.\n";
 exit();