More rev_deleted merging
authorAaron Schulz <aaron@users.mediawiki.org>
Sat, 15 Mar 2008 00:27:57 +0000 (00:27 +0000)
committerAaron Schulz <aaron@users.mediawiki.org>
Sat, 15 Mar 2008 00:27:57 +0000 (00:27 +0000)
*Add suppress option to file delete form. Sprinkle in argument where needed.
*Restrict content at sp:undelete
*FileRepo can deal with images in the 'deleted' FS group (corresponds to a setting of oi_deleted)

includes/FileDeleteForm.php
includes/SpecialUndelete.php
includes/filerepo/File.php
includes/filerepo/LocalFile.php
includes/filerepo/LocalRepo.php
includes/filerepo/OldLocalFile.php
languages/messages/MessagesEn.php

index 13d35f4..16a2d11 100644 (file)
@@ -48,6 +48,9 @@ class FileDeleteForm {
                
                $this->oldimage = $wgRequest->getText( '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;
@@ -75,7 +78,7 @@ class FileDeleteForm {
                                
                        $article = null;
                        if( $this->oldimage ) {
-                               $status = $this->file->deleteOld( $this->oldimage, $reason );
+                               $status = $this->file->deleteOld( $this->oldimage, $reason, $suppress );
                                if( $status->ok ) {
                                        // Need to do a log item
                                        $log = new LogPage( 'delete' );
@@ -85,11 +88,11 @@ class FileDeleteForm {
                                        $log->addEntry( 'delete', $this->title, $logComment );
                                }
                        } else {
-                               $status = $this->file->delete( $reason );
+                               $status = $this->file->delete( $reason, $suppress );
                                if( $status->ok ) {
                                        // Need to delete the associated article
                                        $article = new Article( $this->title );
-                                       $article->doDeleteArticle( $reason );
+                                       $article->doDeleteArticle( $reason, $suppress );
                                }
                        }
                        if( $status->isGood() ) wfRunHooks('FileDeleteComplete', array( 
@@ -118,6 +121,14 @@ class FileDeleteForm {
                global $wgOut, $wgUser, $wgRequest, $wgContLang;
                $align = $wgContLang->isRtl() ? 'left' : 'right';
 
+               if( $wgUser->isAllowed( 'deleterevision' ) ) {
+                       $suppress = "<tr id=\"wpDeleteSuppressRow\" name=\"wpDeleteSuppressRow\"><td></td><td>";
+                       $suppress .= Xml::checkLabel( wfMsg( 'revdelete-suppress' ), 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '2' ) );
+                       $suppress .= "</td></tr>";
+               } else {
+                       $suppress = '';
+               }
+
                $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getAction() ) ) .
                        Xml::openElement( 'fieldset' ) .
                        Xml::element( 'legend', null, wfMsg( 'filedelete-legend' ) ) .
@@ -142,6 +153,7 @@ class FileDeleteForm {
                                        Xml::input( 'wpReason', 60, $wgRequest->getText( 'wpReason' ), array( 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ) ) .
                                "</td>
                        </tr>
+                       {$suppress}
                        <tr>
                                <td></td>
                                <td>" .
index 5f2ecd1..6718520 100644 (file)
@@ -78,7 +78,7 @@ class PageArchive {
                                array(
                                        'ar_namespace',
                                        'ar_title',
-                                       'COUNT(*) AS count',
+                                       'COUNT(*) AS count'
                                ),
                                $condition,
                                __METHOD__,
@@ -100,7 +100,7 @@ class PageArchive {
        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_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text', 'ar_comment', 'ar_len', 'ar_deleted' ),
                        array( 'ar_namespace' => $this->title->getNamespace(),
                               'ar_title' => $this->title->getDBkey() ),
                        'PageArchive::listRevisions',
@@ -124,14 +124,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' ) );
@@ -172,6 +180,7 @@ 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(),
@@ -189,7 +198,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;
                }
@@ -311,11 +322,12 @@ class PageArchive {
         * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
         * @param string $comment
         * @param array $fileVersions
+        * @param bool $Unsuppress
         *
         * @return array(number of file revisions restored, number of image revisions restored, log message)
         * on success, false on failure
         */
-       function undelete( $timestamps, $comment = '', $fileVersions = array() ) {
+       function undelete( $timestamps, $comment = '', $fileVersions = array(), $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 );
@@ -325,14 +337,14 @@ class PageArchive {
                
                if( $restoreFiles && $this->title->getNamespace() == NS_IMAGE ) {
                        $img = wfLocalFile( $this->title );
-                       $this->fileStatus = $img->restore( $fileVersions );
+                       $this->fileStatus = $img->restore( $fileVersions, $Unsuppress );
                        $filesRestored = $this->fileStatus->successCount;
                } else {
                        $filesRestored = 0;
                }
                
                if( $restoreText ) {
-                       $textRestored = $this->undeleteRevisions( $timestamps );
+                       $textRestored = $this->undeleteRevisions( $timestamps, $Unsuppress );
                        if($textRestored === false) // It must be one of UNDELETE_*
                                return false;
                } else {
@@ -373,13 +385,13 @@ class PageArchive {
         * @param array $timestamps Pass an empty array to restore all revisions, otherwise list the ones to undelete.
         * @param string $comment
         * @param array $fileVersions
+        * @param bool $Unsuppress, remove all ar_deleted/fa_deleted restrictions of seletected revs
         *
         * @return mixed number of revisions restored or false on failure
         */
-       private function undeleteRevisions( $timestamps ) {
+       private function undeleteRevisions( $timestamps, $Unsuppress = false ) {
                if ( wfReadOnly() )
                        return false;
-
                $restoreAll = empty( $timestamps );
                
                $dbw = wfGetDB( DB_MASTER );
@@ -394,16 +406,25 @@ class PageArchive {
                        __METHOD__,
                        $options );
                if( $page ) {
+                       $makepage = false;
                        # 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 0;
+                       }
                } else {
                        # Have to create a new article...
-                       $newid  = $article->insertOn( $dbw );
-                       $pageId = $newid;
+                       $makepage = true;
                        $previousRevId = 0;
+                       $previousTimestamp = 0;
                }
 
                if( $restoreAll ) {
@@ -418,7 +439,7 @@ class PageArchive {
                }
 
                /**
-                * Restore each revision...
+                * Select each archived revision...
                 */
                $result = $dbw->select( 'archive',
                        /* fields */ array(
@@ -431,6 +452,7 @@ class PageArchive {
                                'ar_minor_edit',
                                'ar_flags',
                                'ar_text_id',
+                               'ar_deleted',
                                'ar_page_id',
                                'ar_len' ),
                        /* WHERE */ array(
@@ -441,15 +463,32 @@ class PageArchive {
                        /* 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.
@@ -472,7 +511,8 @@ 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++;
@@ -547,6 +587,7 @@ class UndeleteForm {
                $this->mPreview = $request->getCheck( 'preview' ) && $posted;
                $this->mDiff = $request->getCheck( 'diff' );
                $this->mComment = $request->getText( 'wpComment' );
+               $this->mUnsuppress = $request->getVal( 'wpUnsuppress' ) && $wgUser->isAllowed( 'oversight' );
                
                if( $par != "" ) {
                        $this->mTarget = $par;
@@ -582,7 +623,7 @@ class UndeleteForm {
        }
 
        function execute() {
-               global $wgOut;
+               global $wgOut, $wgUser;
                if ( $this->mAllowed ) {
                        $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
                } else {
@@ -590,13 +631,17 @@ class UndeleteForm {
                }
                
                if( is_null( $this->mTargetObj ) ) {
-                       $this->showSearchForm();
-
-                       # List undeletable articles
-                       if( $this->mSearchPrefix ) {
-                               $result = PageArchive::listPagesByPrefix(
-                                       $this->mSearchPrefix );
-                               $this->showList( $result );
+               # 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 );
+                               }
+                       } else {
+                               $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) );
                        }
                        return;
                }
@@ -604,7 +649,14 @@ class UndeleteForm {
                        return $this->showRevision( $this->mTimestamp );
                }
                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();
@@ -633,7 +685,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 ) {
@@ -661,7 +714,7 @@ class UndeleteForm {
                return true;
        }
 
-       /* private */ function showRevision( $timestamp ) {
+       private function showRevision( $timestamp ) {
                global $wgLang, $wgUser, $wgOut;
                $self = SpecialPage::getTitleFor( 'Undelete' );
                $skin = $wgUser->getSkin();
@@ -676,6 +729,17 @@ class UndeleteForm {
                        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(
@@ -683,8 +747,7 @@ class UndeleteForm {
                        htmlspecialchars( $this->mTargetObj->getPrefixedText() )
                );
                $time = htmlspecialchars( $wgLang->timeAndDate( $timestamp, true ) );
-               $user = $skin->userLink( $rev->getUser(), $rev->getUserText() )
-                       . $skin->userToolLinks( $rev->getUser(), $rev->getUserText() );
+               $user = $skin->revUserTools( $rev );
 
                if( $this->mDiff ) {
                        $previousRev = $archive->getPreviousRevision( $timestamp );
@@ -706,7 +769,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(
@@ -714,7 +777,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',
@@ -812,7 +875,7 @@ class UndeleteForm {
        /**
         * Show a deleted file version requested by the visitor.
         */
-       function showFile( $key ) {
+       private function showFile( $key ) {
                global $wgOut, $wgRequest;
                $wgOut->disable();
                
@@ -828,15 +891,17 @@ class UndeleteForm {
                $store->stream( $key );
        }
 
-       /* private */ function showHistory() {
+       private function showHistory() {
                global $wgLang, $wgContLang, $wgUser, $wgOut;
 
                $sk = $wgUser->getSkin();
-               if ( $this->mAllowed ) {
+               if( $this->mAllowed ) {
                        $wgOut->setPagetitle( wfMsg( "undeletepage" ) );
                } else {
                        $wgOut->setPagetitle( wfMsg( 'viewdeletedpage' ) );
                }
+               
+               $wgOut->addWikiText( wfMsgHtml( 'undeletepagetitle', $this->mTargetObj->getPrefixedText()) );
 
                $archive = new PageArchive( $this->mTargetObj );
                /*
@@ -848,6 +913,7 @@ class UndeleteForm {
                */
                if ( $this->mAllowed ) {
                        $wgOut->addWikiMsg( "undeletehistory" );
+                       $wgOut->addWikiMsg( "undeleterevdel" );
                } else {
                        $wgOut->addWikiMsg( "undeletehistorynoadmin" );
                }
@@ -928,6 +994,13 @@ class UndeleteForm {
                                                        Xml::submitButton( wfMsg( 'undeletebtn' ), array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) .
                                                        Xml::element( 'input', array( 'type' => 'reset', 'value' => wfMsg( 'undeletereset' ), 'id' => 'mw-undelete-reset' ) ) .
                                                "</td>
+                                       </tr>
+                                       <tr>
+                                               <td>&nbsp;</td>
+                                               <td>" .
+                                                       Xml::check( 'wpUnsuppress', $this->mUnsuppress, array('id' => 'mw-undelete-unsupress') ) . ' ' .
+                                                       Xml::label( wfMsgHtml('revdelete-unsuppress'), 'mw-undelete-unsupress' ) .
+                                               "</td>
                                        </tr>" .
                                Xml::closeElement( 'table' ) .
                                Xml::closeElement( 'fieldset' );
@@ -946,40 +1019,7 @@ class UndeleteForm {
 
                        while( $row = $revisions->fetchObject() ) {
                                $remaining--;
-                               $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" );
-                                       if( ($remaining > 0) ||
-                                                       ($earliestLiveTime && $ts > $earliestLiveTime ) ) {
-                                               $diffLink = '(' .
-                                                       $sk->makeKnownLinkObj( $titleObj,
-                                                               wfMsgHtml( 'diff' ),
-                                                               "target=$target&timestamp=$ts&diff=prev" ) .
-                                                       ')';
-                                       } else {
-                                               // No older revision to diff against
-                                               $diffLink = '';
-                                       }
-                               } else {
-                                       $checkBox = '';
-                                       $pageLink = $wgLang->timeanddate( $ts, true );
-                                       $diffLink = '';
-                               }
-                               $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 $diffLink . . $userLink $stxt $comment</li>\n" );
-
+                               $wgOut->addHTML( $this->formatRevisionRow( $row , $sk ) );
                        }
                        $revisions->free();
                        $wgOut->addHTML("</ul>");
@@ -991,28 +1031,7 @@ class UndeleteForm {
                        $wgOut->addHtml( Xml::element( 'h2', null, wfMsg( 'filehist' ) ) . "\n" );
                        $wgOut->addHtml( "<ul>" );
                        while( $row = $files->fetchObject() ) {
-                               $ts = wfTimestamp( TS_MW, $row->fa_timestamp );
-                               if ( $this->mAllowed && $row->fa_storage_key ) {
-                                       $checkBox = Xml::check( "fileid" . $row->fa_id );
-                                       $key = urlencode( $row->fa_storage_key );
-                                       $target = urlencode( $this->mTarget );
-                                       $pageLink = $sk->makeKnownLinkObj( $titleObj,
-                                               $wgLang->timeanddate( $ts, true ),
-                                               "target=$target&file=$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 );
-                               $data =
-                                       wfMsgHtml( 'widthheight',
-                                               $wgLang->formatNum( $row->fa_width ),
-                                               $wgLang->formatNum( $row->fa_height ) ) .
-                                       ' (' .
-                                       wfMsgHtml( 'nbytes', $wgLang->formatNum( $row->fa_size ) ) .
-                                       ')';
-                               $comment = $sk->commentBlock( $row->fa_description );
-                               $wgOut->addHTML( "<li>$checkBox $pageLink . . $userLink $data $comment</li>\n" );
+                               $wgOut->addHTML( $this->formatFileRow( $row, $sk ) );
                        }
                        $files->free();
                        $wgOut->addHTML( "</ul>" );
@@ -1029,6 +1048,112 @@ class UndeleteForm {
                return true;
        }
 
+       private function formatRevisionRow( $row, $sk ) {
+               global $wgUser, $wgLang;
+               
+               $rev = new Revision( array(
+                               'page'       => $this->mTargetObj->getArticleId(),
+                               'comment'    => $row->ar_comment,
+                               'user'       => $row->ar_user,
+                               'user_text'  => $row->ar_user_text,
+                               'timestamp'  => $row->ar_timestamp,
+                               'minor_edit' => $row->ar_minor_edit,
+                               'deleted'    => $row->ar_deleted,
+                               'len'        => $row->ar_len) );
+               
+               $stxt = '';
+               
+               if( $this->mAllowed ) {
+                       $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
+                       $checkBox = Xml::check( "ts$ts" );
+                       $titleObj = SpecialPage::getTitleFor( "Undelete" );
+                       $pageLink = $this->getPageLink( $rev, $titleObj, $ts, $sk );
+                       # Last link
+                       if( !$rev->userCan( Revision::DELETED_TEXT ) ) {
+                               $last = wfMsgHtml('diff');
+                       } else {
+                               $last = $sk->makeKnownLinkObj( $titleObj, wfMsgHtml('diff'), 
+                               "target=" . $this->mTarget . "&timestamp=" . $row->ar_timestamp . "&diff=prev" );
+                       }
+               } else {
+                       $checkBox = '';
+                       $pageLink = $wgLang->timeanddate( $ts, true );
+               }
+               $userLink = $sk->revUserTools( $rev );
+               
+               if(!is_null($size = $row->ar_len)) {
+                       if($size == 0)
+                               $stxt = wfMsgHtml('historyempty');
+                       else
+                               $stxt = wfMsgHtml('historysize', $wgLang->formatNum( $size ) );
+               }
+               $comment = $sk->revComment( $rev );
+               $revdlink = '';
+               if( $wgUser->isAllowed( 'deleterevision' ) ) {
+                       $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+                       if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
+                       // If revision was hidden from sysops
+                               $del = wfMsgHtml('rev-delundel');                       
+                       } else {
+                               $del = $sk->makeKnownLinkObj( $revdel,
+                                       wfMsgHtml('rev-delundel'),
+                                       'target=' . urlencode( $this->mTarget ) .
+                                       '&artimestamp=' . urlencode( $row->ar_timestamp ) );
+                               // Bolden oversighted content
+                               if( $rev->isDeleted( Revision::DELETED_RESTRICTED ) )
+                                       $del = "<strong>$del</strong>";
+                       }
+                       $revdlink = "<tt>(<small>$del</small>)</tt>";
+               }
+               
+               return "<li>$checkBox $revdlink ($last) $pageLink . . $userLink $stxt $comment</li>";
+       }
+       
+       private function formatFileRow( $row, $sk ) {
+               global $wgUser, $wgLang;
+       
+               $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 );
+                       $key = urlencode( $row->fa_storage_key );
+                       $target = urlencode( $this->mTarget );
+                       $titleObj = SpecialPage::getTitleFor( "Undelete" );
+                       $pageLink = $this->getFileLink( $file, $titleObj, $ts, $key, $sk );
+               } else {
+                       $checkBox = '';
+                       $pageLink = $wgLang->timeanddate( $ts, true );
+               }
+               $userLink = $this->getFileUser( $file, $sk );
+               $data =
+                       wfMsgHtml( 'widthheight',
+                               $wgLang->formatNum( $row->fa_width ),
+                               $wgLang->formatNum( $row->fa_height ) ) .
+                       ' (' .
+                       wfMsgHtml( 'nbytes', $wgLang->formatNum( $row->fa_size ) ) .
+                       ')';
+               $comment = $this->getFileComment( $file, $sk );
+               $revdlink = '';
+               if( $wgUser->isAllowed( 'deleterevision' ) ) {
+                       $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+                       if( !$file->userCan(File::DELETED_RESTRICTED ) ) {
+                       // If revision was hidden from sysops
+                               $del = $this->messages['rev-delundel'];
+                       } else {
+                               $del = $sk->makeKnownLinkObj( $revdel,
+                                       wfMsgHtml('rev-delundel'),
+                                       'target=' . urlencode( $this->mTarget ) .
+                                       '&fileid=' . urlencode( $row->fa_id ) );
+                               // Bolden oversighted content
+                               if( $file->isDeleted( File::DELETED_RESTRICTED ) )
+                                       $del = "<strong>$del</strong>";
+                       }
+                       $revdlink = "<tt>(<small>$del</small>)</tt>";
+               }
+               return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
+       }
+       
        private function getEarliestTime( $title ) {
                $dbr = wfGetDB( DB_SLAVE );
                if( $title->exists() ) {
@@ -1041,6 +1166,71 @@ class UndeleteForm {
                return null;
        }
 
+       /**
+        * Fetch revision text link if it's available to all users
+        * @return string
+        */
+       function getPageLink( $rev, $titleObj, $ts, $sk ) {
+               global $wgLang;
+               
+               if( !$rev->userCan(Revision::DELETED_TEXT) ) {
+                       return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
+               } else {
+                       $link = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ), "target={$this->mTarget}&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, $key, $sk ) {
+               global $wgLang;
+
+               if( !$file->userCan(File::DELETED_FILE) ) {
+                       return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
+               } else {
+                       $link = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ), "target={$this->mTarget}&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, $sk ) {    
+               if( !$file->userCan(File::DELETED_USER) ) {
+                       return '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
+               } else {
+                       $link = $sk->userLink( $file->getRawUser(), $file->getRawUserText() ) . 
+                               $sk->userToolLinks( $file->getRawUser(), $file->getRawUserText() );
+                       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, $sk ) {
+               if( !$file->userCan(File::DELETED_COMMENT) ) {
+                       return '<span class="history-deleted"><span class="comment">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span></span>';
+               } else {
+                       $link = $sk->commentBlock( $file->getRawDescription() );
+                       if( $file->isDeleted(File::DELETED_COMMENT) )
+                               $link = '<span class="history-deleted">' . $link . '</span>';
+                       return $link;
+               }
+       }
+
        function undelete() {
                global $wgOut, $wgUser;
                if ( wfReadOnly() ) {
@@ -1049,11 +1239,11 @@ class UndeleteForm {
                }
                if( !is_null( $this->mTargetObj ) ) {
                        $archive = new PageArchive( $this->mTargetObj );
-                       
                        $ok = $archive->undelete(
                                $this->mTargetTimestamp,
                                $this->mComment,
-                               $this->mFileVersions );
+                               $this->mFileVersions,
+                               $this->mUnsuppress );
 
                        if( is_array($ok) ) {
                                if ( $ok[1] ) // Undeleted file count
@@ -1066,11 +1256,12 @@ class UndeleteForm {
                                $wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) );
                        } else {
                                $wgOut->showFatalError( wfMsg( "cannotundelete" ) );
+                               $wgOut->addHtml( '<p>' . wfMsgHtml( "undeleterevdel" ) . '</p>' );
                        }
 
                        // Show file deletion warnings and errors
                        $status = $archive->getFileStatus();
-                       if ( $status && !$status->isGood() ) {
+                       if( $status && !$status->isGood() ) {
                                $wgOut->addWikiText( $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) );
                        }
                } else {
index c129ae3..1a5504e 100644 (file)
@@ -921,11 +921,12 @@ abstract class File {
         * Cache purging is done; logging is caller's responsibility.
         *
         * @param $reason
+        * @param $Suppress, hide content from sysops?
         * @return true on success, false on some kind of failure
         * STUB
         * Overridden by LocalFile
         */
-       function delete( $reason ) {
+       function delete( $reason, $suppress = false ) {
                $this->readOnlyError();
        }
 
@@ -937,12 +938,13 @@ abstract class File {
         *
         * @param $versions set of record ids of deleted items to restore,
         *                    or empty to restore all revisions.
+        * @param $unsuppress, remove restrictions on content upon restoration?
         * @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( $versions=array(), $unsuppress=false ) {
                $this->readOnlyError();
        }
 
index 9b06fe2..4949aac 100644 (file)
@@ -48,7 +48,8 @@ class LocalFile extends File
                $description,      # Description of current revision of the file
                $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
 
        /**#@-*/
 
@@ -235,8 +236,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();
+               }
        }
 
        /**
@@ -345,6 +351,7 @@ class LocalFile extends File
        /** getURL inherited */
        /** getViewURL inherited */
        /** getPath inherited */
+       /** isVisible inhereted */
 
        /**
         * Return the width of the image
@@ -620,7 +627,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() ),
                                $fname
@@ -793,7 +802,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__
                        );
 
@@ -907,11 +916,12 @@ class LocalFile extends File
         * Cache purging is done; logging is caller's responsibility.
         *
         * @param $reason
+        * @param $suppress
         * @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
@@ -944,12 +954,13 @@ class LocalFile extends File
         * Cache purging is done; logging is caller's responsibility.
         *
         * @param $reason
+        * @param $suppress
         * @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();
@@ -968,10 +979,11 @@ class LocalFile extends File
         *
         * @param $versions set of record ids of deleted items to restore,
         *                    or empty to restore all revisions.
+        * @param $unuppress
         * @return FileRepoStatus
         */
        function restore( $versions = array(), $unsuppress = false ) {
-               $batch = new LocalFileRestoreBatch( $this );
+               $batch = new LocalFileRestoreBatch( $this, $unsuppress );
                if ( !$versions ) {
                        $batch->addAll();
                } else {
@@ -1157,12 +1169,13 @@ class Image extends LocalFile {
  * Helper class for file deletion
  */
 class LocalFileDeleteBatch {
-       var $file, $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch;
+       var $file, $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
        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();
        }
 
@@ -1243,6 +1256,18 @@ 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;
+               } else {
+                       $bitfield = 'oi_deleted';
+               }
 
                if ( $deleteCurrent ) {
                        $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
@@ -1254,7 +1279,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',
@@ -1285,7 +1310,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',
@@ -1300,7 +1325,8 @@ class LocalFileDeleteBatch {
                                        'fa_description'  => 'oi_description',
                                        'fa_user'         => 'oi_user',
                                        'fa_user_text'    => 'oi_user_text',
-                                       'fa_timestamp'    => 'oi_timestamp'
+                                       'fa_timestamp'    => 'oi_timestamp',
+                                       'fa_deleted'      => $bitfield
                                ), $where, __METHOD__ );
                }
        }
@@ -1328,15 +1354,31 @@ class LocalFileDeleteBatch {
                wfProfileIn( __METHOD__ );
 
                $this->file->lock();
-
+               // Leave private files alone
+               $privateFiles = array();
+               list( $oldRels, $deleteCurrent ) = $this->getOldRels();
+               $dbw = $this->file->repo->getMasterDB();
+               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();
+                               $privateFiles[$row->oi_archive_name] = 1;
+                       }
+               }
                // Prepare deletion batch
                $hashes = $this->getHashes();
                $this->deletionBatch = array();
                $ext = $this->file->getExtension();
                $dotExt = $ext === '' ? '' : ".$ext";
                foreach ( $this->srcRels as $name => $srcRel ) {
-                       // Skip files that have no hash (missing source)
-                       if ( isset( $hashes[$name] ) ) {
+                       // Skip files that have no hash (missing source).
+                       // Keep private files where they are.
+                       if ( isset($hashes[$name]) && !array_key_exists($name,$privateFiles) ) {
                                $hash = $hashes[$name];
                                $key = $hash . $dotExt;
                                $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
@@ -1394,10 +1436,11 @@ class LocalFileDeleteBatch {
 class LocalFileRestoreBatch {
        var $file, $cleanupBatch, $ids, $all, $unsuppress = false;
 
-       function __construct( File $file ) {
+       function __construct( File $file, $unsuppress = false ) {
                $this->file = $file;
                $this->cleanupBatch = $this->ids = array();
                $this->ids = array();
+               $this->unsuppress = $unsuppress;
        }
 
        /**
@@ -1460,12 +1503,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++;
@@ -1503,6 +1541,11 @@ class LocalFileRestoreBatch {
                        }
 
                        if ( $first && !$exists ) {
+                               // The live (current) version cannot be hidden!
+                               if( !$this->unsuppress && $row->fa_deleted ) {
+                                       $this->file->unlock();
+                                       return $status;
+                               }
                                // This revision will be published as the new current version
                                $destRel = $this->file->getRel();
                                $insertCurrent = array(
@@ -1549,13 +1592,17 @@ 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;
+                       if( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
+                               // private files can stay where they are
+                       } else {
+                               $storeBatch[] = array( $deletedUrl, 'public', $destRel );
+                               $this->cleanupBatch[] = $row->fa_storage_key;
+                       }
                        $first = false;
                }
                unset( $result );
index a259bd4..8a8bba2 100644 (file)
@@ -48,6 +48,13 @@ class LocalRepo extends FSRepo {
                        $inuse = $dbw->selectField( 'filearchive', '1', 
                                array( 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ),
                                __METHOD__, array( 'FOR UPDATE' ) );
+                       if( !$inuse ) {
+                               $sha1 = substr( $key, 0, strcspn( $key, '.' ) );
+                               $inuse = $dbw->selectField( 'oldimage', '1',
+                               array( 'oi_sha1' => $sha1,
+                                       'oi_deleted & '.File::DELETED_FILE => File::DELETED_FILE ),
+                               __METHOD__, array( 'FOR UPDATE' ) );
+                       }
                        if ( !$inuse ) {
                                wfDebug( __METHOD__ . ": deleting $key\n" );
                                if ( !@unlink( $path ) ) {
index 850a8d8..bc712c7 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,7 @@ 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 +227,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 0da6748..34ac4f1 100644 (file)
@@ -1955,6 +1955,7 @@ Here are the current settings for the page <strong>$1</strong>:',
 # Undelete
 'undelete'                     => 'View deleted pages',
 'undeletepage'                 => 'View and restore deleted pages',
+'undeletepagetitle'            => '\'\'\'The following consists of deleted revisions of [[:$1]]\'\'\'.',
 'viewdeletedpage'              => 'View deleted pages',
 '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.",