From: Aaron Schulz Date: Sat, 15 Mar 2008 00:27:57 +0000 (+0000) Subject: More rev_deleted merging X-Git-Tag: 1.31.0-rc.0~49091 X-Git-Url: https://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/exercices/journal.php?a=commitdiff_plain;h=1f60b024dec5f21b5eae1a1db1a3d8d16e36cb21;p=lhc%2Fweb%2Fwiklou.git More rev_deleted merging *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) --- diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index 13d35f45a7..16a2d1159e 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -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 = ""; + $suppress .= Xml::checkLabel( wfMsg( 'revdelete-suppress' ), 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '2' ) ); + $suppress .= ""; + } 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' ) ) . " + {$suppress} " . diff --git a/includes/SpecialUndelete.php b/includes/SpecialUndelete.php index 5f2ecd15f9..6718520c1f 100644 --- a/includes/SpecialUndelete.php +++ b/includes/SpecialUndelete.php @@ -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 { '' ); } - /* 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( '
' ); + // 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( "
\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' ) ) . " + + +   + " . + Xml::check( 'wpUnsuppress', $this->mUnsuppress, array('id' => 'mw-undelete-unsupress') ) . ' ' . + Xml::label( wfMsgHtml('revdelete-unsuppress'), 'mw-undelete-unsupress' ) . + " " . 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×tamp=$ts" ); - if( ($remaining > 0) || - ($earliestLiveTime && $ts > $earliestLiveTime ) ) { - $diffLink = '(' . - $sk->makeKnownLinkObj( $titleObj, - wfMsgHtml( 'diff' ), - "target=$target×tamp=$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( "
  • $checkBox $pageLink $diffLink . . $userLink $stxt $comment
  • \n" ); - + $wgOut->addHTML( $this->formatRevisionRow( $row , $sk ) ); } $revisions->free(); $wgOut->addHTML(""); @@ -991,28 +1031,7 @@ class UndeleteForm { $wgOut->addHtml( Xml::element( 'h2', null, wfMsg( 'filehist' ) ) . "\n" ); $wgOut->addHtml( "" ); @@ -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 . "×tamp=" . $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 = "$del"; + } + $revdlink = "($del)"; + } + + return "
  • $checkBox $revdlink ($last) $pageLink . . $userLink $stxt $comment
  • "; + } + + 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 = "$del"; + } + $revdlink = "($del)"; + } + return "
  • $checkBox $revdlink $pageLink . . $userLink $data $comment
  • \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 '' . $wgLang->timeanddate( $ts, true ) . ''; + } else { + $link = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ), "target={$this->mTarget}×tamp=$ts" ); + if( $rev->isDeleted(Revision::DELETED_TEXT) ) + $link = '' . $link . ''; + 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 '' . $wgLang->timeanddate( $ts, true ) . ''; + } else { + $link = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ), "target={$this->mTarget}&file=$key" ); + if( $file->isDeleted(File::DELETED_FILE) ) + $link = '' . $link . ''; + 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 '' . wfMsgHtml( 'rev-deleted-user' ) . ''; + } else { + $link = $sk->userLink( $file->getRawUser(), $file->getRawUserText() ) . + $sk->userToolLinks( $file->getRawUser(), $file->getRawUserText() ); + if( $file->isDeleted(File::DELETED_USER) ) + $link = '' . $link . ''; + 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 '' . wfMsgHtml( 'rev-deleted-comment' ) . ''; + } else { + $link = $sk->commentBlock( $file->getRawDescription() ); + if( $file->isDeleted(File::DELETED_COMMENT) ) + $link = '' . $link . ''; + 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( '

    ' . wfMsgHtml( "undeleterevdel" ) . '

    ' ); } // 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 { diff --git a/includes/filerepo/File.php b/includes/filerepo/File.php index c129ae3c4e..1a5504e896 100644 --- a/includes/filerepo/File.php +++ b/includes/filerepo/File.php @@ -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(); } diff --git a/includes/filerepo/LocalFile.php b/includes/filerepo/LocalFile.php index 9b06fe2d8f..4949aac0a9 100644 --- a/includes/filerepo/LocalFile.php +++ b/includes/filerepo/LocalFile.php @@ -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 ); diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php index a259bd4851..8a8bba2df3 100644 --- a/includes/filerepo/LocalRepo.php +++ b/includes/filerepo/LocalRepo.php @@ -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 ) ) { diff --git a/includes/filerepo/OldLocalFile.php b/includes/filerepo/OldLocalFile.php index 850a8d8ad8..bc712c75c6 100644 --- a/includes/filerepo/OldLocalFile.php +++ b/includes/filerepo/OldLocalFile.php @@ -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; + } + } +} diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index 0da6748ceb..34ac4f1cde 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -1955,6 +1955,7 @@ Here are the current settings for the page $1:', # 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.",