Index: includes/Article.php =================================================================== --- includes/Article.php (revision 43299) +++ includes/Article.php (working copy) @@ -2054,9 +2054,9 @@ // This page has no revisions, which is very weird return false; if( $res->numRows() > 1 ) - $hasHistory = true; + $hasHistory = true; else - $hasHistory = false; + $hasHistory = false; $row = $dbw->fetchObject( $res ); $onlyAuthor = $row->rev_user_text; // Try to find a second contributor @@ -2140,7 +2140,7 @@ $conds = $this->mTitle->pageCond(); $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ ); if ( $latest === false ) { - $wgOut->showFatalError( wfMsg( 'cannotdelete' ) ); + $wgOut->showFatalError( wfMsgExt( 'cannotdelete', array('parseinline') ) ); return; } @@ -2366,7 +2366,7 @@ wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason, $id)); } else { if ($error == '') - $wgOut->showFatalError( wfMsg( 'cannotdelete' ) ); + $wgOut->showFatalError( wfMsgExt( 'cannotdelete', array('parseinline') ) ); else $wgOut->showFatalError( $error ); } @@ -2379,86 +2379,59 @@ * Returns success */ function doDeleteArticle( $reason, $suppress = false, $id = 0 ) { - global $wgUseSquid, $wgDeferredUpdateList; - global $wgUseTrackbacks; - - wfDebug( __METHOD__."\n" ); - - $dbw = wfGetDB( DB_MASTER ); - $ns = $this->mTitle->getNamespace(); - $t = $this->mTitle->getDBkey(); + global $wgUseSquid, $wgDeferredUpdateList, $wgUseTrackbacks; + // Get the page ID $id = $id ? $id : $this->mTitle->getArticleID( GAID_FOR_UPDATE ); - - if ( $t == '' || $id == 0 ) { + // Make sure this page actually exists! + if ( $this->mTitle->getDBkey() == '' || $id == 0 ) { return false; } - - $u = new SiteStatsUpdate( 0, 1, -(int)$this->isCountable( $this->getContent() ), -1 ); - array_push( $wgDeferredUpdateList, $u ); - - // Bitfields to further suppress the content - if ( $suppress ) { - $bitfield = 0; - // This should be 15... + // Set revisions as hidden from Sysops if requested + $bitfield = 0; + if( $suppress ) { $bitfield |= Revision::DELETED_TEXT; $bitfield |= Revision::DELETED_COMMENT; $bitfield |= Revision::DELETED_USER; $bitfield |= Revision::DELETED_RESTRICTED; - } else { - $bitfield = 'rev_deleted'; } - - $dbw->begin(); - // For now, shunt the revision data into the archive table. + $dbw = wfGetDB( DB_MASTER ); // Text is *not* removed from the text table; bulk storage // is left intact to avoid breaking block-compression or // immutable storage schemes. - // - // For backwards compatibility, note that some older archive - // table entries will have ar_text and ar_flags fields still. - // - // In the future, we may keep revisions and mark them with - // the rev_deleted field, which is reserved for this purpose. - $dbw->insertSelect( 'archive', array( 'page', 'revision' ), + $dbw->begin(); + // Move title row from page table to deleted_pages table + $dbw->insertSelect( 'deleted_pages', array( 'page' ), array( - 'ar_namespace' => 'page_namespace', - 'ar_title' => 'page_title', - 'ar_comment' => 'rev_comment', - 'ar_user' => 'rev_user', - 'ar_user_text' => 'rev_user_text', - 'ar_timestamp' => 'rev_timestamp', - 'ar_minor_edit' => 'rev_minor_edit', - 'ar_rev_id' => 'rev_id', - 'ar_text_id' => 'rev_text_id', - 'ar_text' => '\'\'', // Be explicit to appease - 'ar_flags' => '\'\'', // MySQL's "strict mode"... - 'ar_len' => 'rev_len', - 'ar_page_id' => 'page_id', - 'ar_deleted' => $bitfield + 'deleted_page_namespace' => 'page_namespace', + 'deleted_page_title' => 'page_title', + 'deleted_page_id' => 'page_id', + 'deleted_page_suppressed' => intval($suppress), + 'deleted_on_timestamp' => $dbw->timestamp() ), array( - 'page_id' => $id, - 'page_id = rev_page' - ), __METHOD__ + 'page_id' => $id + ), __METHOD__, + array( 'IGNORE' ) ); - - # Delete restrictions for it - $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ ); - - # Now that it's safely backed up, delete it - $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__); - $ok = ( $dbw->affectedRows() > 0 ); // getArticleId() uses slave, could be laggy - if( !$ok ) { + if ( !$dbw->affectedRows() ) { $dbw->rollback(); - return false; + wfDebug( "Page already deleted!\n" ); + return false; // Page already deleted! } - + // Suppress the revisions if set to do so + if ( $bitfield ) { + $dbw->update( 'revision', + array( "rev_deleted = rev_deleted | $bitfield" ), + array( 'rev_page' => $id ), + __METHOD__ + ); + } + # Now that it's safely backed up, delete it + $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ ); # If using cascading deletes, we can skip some explicit deletes if ( !$dbw->cascadingDeletes() ) { - $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ ); - - if ($wgUseTrackbacks) + if ( $wgUseTrackbacks ) { $dbw->delete( 'trackbacks', array( 'tb_page' => $id ), __METHOD__ ); - + } # Delete outgoing links $dbw->delete( 'pagelinks', array( 'pl_from' => $id ) ); $dbw->delete( 'imagelinks', array( 'il_from' => $id ) ); @@ -2468,7 +2441,6 @@ $dbw->delete( 'langlinks', array( 'll_from' => $id ) ); $dbw->delete( 'redirect', array( 'rd_from' => $id ) ); } - # If using cleanup triggers, we can skip some manual deletes if ( !$dbw->cleanupTriggers() ) { # Clean up recentchanges entries... @@ -2481,29 +2453,27 @@ array( 'rc_type != '.RC_LOG, 'rc_cur_id' => $id ), __METHOD__ ); } - # Clear caches Article::onArticleDelete( $this->mTitle ); - # Fix category table counts $cats = array(); $res = $dbw->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ ); - foreach( $res as $row ) { + foreach ( $res as $row ) { $cats []= $row->cl_to; } $this->updateCategoryCounts( array(), $cats ); - + // One less article on the site + $u = new SiteStatsUpdate( 0, 1, -(int)$this->isCountable( $this->getContent() ), -1 ); + array_push( $wgDeferredUpdateList, $u ); # Clear the cached article id so the interface doesn't act like we exist $this->mTitle->resetArticleID( 0 ); $this->mTitle->mArticleID = 0; - # Log the deletion, if the page was suppressed, log it at Oversight instead $logtype = $suppress ? 'suppress' : 'delete'; $log = new LogPage( $logtype ); - # Make sure logging got through $log->addEntry( 'delete', $this->mTitle, $reason, array() ); - + // Done! $dbw->commit(); return true; @@ -2589,7 +2559,7 @@ $user_text = $dbw->addQuotes( $current->getUserText() ); $s = $dbw->selectRow( 'revision', array( 'rev_id', 'rev_timestamp', 'rev_deleted' ), - array( 'rev_page' => $current->getPage(), + array( 'rev_page' => $current->getPage(), "rev_user <> {$user} OR rev_user_text <> {$user_text}" ), __METHOD__, array( 'USE INDEX' => 'page_timestamp', Index: includes/AutoLoader.php =================================================================== --- includes/AutoLoader.php (revision 43299) +++ includes/AutoLoader.php (working copy) @@ -465,10 +465,12 @@ 'MostlinkedPage' => 'includes/specials/SpecialMostlinked.php', 'MostrevisionsPage' => 'includes/specials/SpecialMostrevisions.php', 'MovePageForm' => 'includes/specials/SpecialMovepage.php', + 'SpecialRestore' => 'includes/specials/SpecialRestore.php', 'SpecialNewpages' => 'includes/specials/SpecialNewpages.php', 'SpecialContributions' => 'includes/specials/SpecialContributions.php', 'NewPagesPager' => 'includes/specials/SpecialNewpages.php', 'PageArchive' => 'includes/specials/SpecialUndelete.php', + 'DeletedPage' => 'includes/specials/SpecialRestore.php', 'PasswordResetForm' => 'includes/specials/SpecialResetpass.php', 'PopularPagesPage' => 'includes/specials/SpecialPopularpages.php', 'PreferencesForm' => 'includes/specials/SpecialPreferences.php', Index: includes/SpecialPage.php =================================================================== --- includes/SpecialPage.php (revision 43299) +++ includes/SpecialPage.php (working copy) @@ -144,7 +144,7 @@ 'Allmessages' => array( 'SpecialPage', 'Allmessages' ), 'Log' => array( 'SpecialPage', 'Log' ), 'Blockip' => array( 'SpecialPage', 'Blockip', 'block' ), - 'Undelete' => array( 'SpecialPage', 'Undelete', 'deletedhistory' ), + 'Undelete' => 'SpecialRestore', 'Import' => array( 'SpecialPage', 'Import', 'import' ), 'Lockdb' => array( 'SpecialPage', 'Lockdb', 'siteadmin' ), 'Unlockdb' => array( 'SpecialPage', 'Unlockdb', 'siteadmin' ), Index: includes/specials/SpecialRestore.php =================================================================== --- includes/specials/SpecialRestore.php (revision 0) +++ includes/specials/SpecialRestore.php (revision 0) @@ -0,0 +1,1128 @@ +setHeaders(); + $this->skin = $wgUser->getSkin(); + // Permission check + if( !$wgUser->isAllowed( 'deletedhistory' ) ) { + $wgOut->permissionRequired( 'deletedhistory' ); + return; + } + if( $wgUser->isAllowed( 'undelete' ) && !$wgUser->isBlocked() ) { + $this->mAllowed = true; + } else { + $this->mAllowed = false; + $this->mRestore = false; + } + if( $this->mAllowed ) { + $wgOut->setPagetitle( wfMsg( "undeletepage" ) ); + } else { + $wgOut->setPagetitle( wfMsg( "viewdeletedpage" ) ); + } + + $token = $wgRequest->getVal( 'wpEditToken' ); + $posted = $wgRequest->wasPosted() && $wgUser->matchEditToken( $token ); + + $this->mTarget = $par ? $par : $wgRequest->getVal( 'target' ); + if( !is_null($this->mTarget) && $this->mTarget !== "" ) { + $this->mTargetObj = Title::newFromURL( $this->mTarget ); + // Check for revs deleted the old way + if( !$this->mTargetObj->isDeleted( 'skiparchive' ) ) { + // Load the old form... + $form = new UndeleteForm( $wgRequest, $par ); + $form->execute(); + return; + } + } else { + $this->mTargetObj = NULL; + } + $this->mDeletedPage = $wgRequest->getIntOrNull( 'deletedpage' ); + if( !$this->mDeletedPage && preg_match( '/^\d+$/', $par ) ) { + $this->mDeletedPage = intval( $par ); + } + $this->mRevId = $wgRequest->getIntOrNull( 'oldid' ); + $this->mFile = $wgRequest->getVal( 'file' ); + $this->mSearchPrefix = $wgRequest->getText( 'prefix' ); + $this->mRestore = $wgRequest->getCheck( 'restore' ) && $posted; + $this->mPreview = $wgRequest->getCheck( 'preview' ) && $posted; + $this->mDiff = $wgRequest->getCheck( 'diff' ); + $this->mComment = $wgRequest->getText( 'wpComment' ); + $this->mUnsuppress = $wgRequest->getVal( 'wpUnsuppress' ) && $wgUser->isAllowed( 'suppressrevision' ); + if( $this->mRestore ) { + $this->mFileVersions = array(); + foreach( $_REQUEST as $key => $val ) { + $matches = array(); + if( preg_match( '/^fileid(\d+)$/', $key, $matches ) ) { + $this->mFileVersions[] = intval( $matches[1] ); + } + } + } + # Submit or show form... + if( $posted && $this->mRestore ) { + return $this->undelete(); + } else { + return $this->showForm(); + } + } + + private function showForm() { + global $wgUser, $wgOut, $wgLang; + if( is_null($this->mTargetObj) && !$this->mDeletedPage ) { + return $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) ); + /* FIXME: Old item must be migrated over first... + # 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 = DeletedPage::listPagesByPrefix( $this->mSearchPrefix ); + $this->showList( $result ); + } + } else { + $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) ); + } + */ + } + // Show a revision + if( !is_null($this->mDeletedPage) && !is_null($this->mRevId) ) { + return $this->showRevision( $this->mRevId ); + } + // Show a file + if( !is_null($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( 'suppressrevision' ); + return false; + } else { + return $this->showFile( $this->mFile ); + } + } + // Show the revision list for a page + if( !is_null($this->mDeletedPage) ) { + return $this->showHistory(); + } + // If only one "incarnation", just go straight to it + $pagesWithTitle = DeletedPage::getPages( $this->mTargetObj ); + if( count($pagesWithTitle) == 1 ) { + $this->mDeletedPage = $pagesWithTitle[0]; + return $this->showHistory(); + } else if( count($pagesWithTitle) > 0 ) { + // Show list of deleted pages for this title + $wgOut->addHTML( '' ); + // Page may just be file versions... + } else { + $this->mDeletedPage = 0; + return $this->showHistory(); + } + } + + private function showSearchForm() { + global $wgOut, $wgScript; + $wgOut->addWikiMsg( 'undelete-header' ); + $wgOut->addHtml( + Xml::openElement( 'form', array( + 'method' => 'get', + 'action' => $wgScript ) ) . + '
' . + Xml::element( 'legend', array(), + wfMsg( 'undelete-search-box' ) ) . + Xml::hidden( 'title', + SpecialPage::getTitleFor( 'Undelete' )->getPrefixedDbKey() ) . + Xml::inputLabel( wfMsg( 'undelete-search-prefix' ), + 'prefix', 'prefix', 20, + $this->mSearchPrefix ) . + Xml::submitButton( wfMsg( 'undelete-search-submit' ) ) . + '
' . + '' + ); + } + + // Generic list of deleted pages + private function showList( $result ) { + global $wgLang, $wgContLang, $wgUser, $wgOut; + if( $result->numRows() == 0 ) { + $wgOut->addWikiMsg( 'undelete-no-results' ); + return; + } + $wgOut->addWikiMsg( "undeletepagetext" ); + $wgOut->addHTML( "\n" ); + return true; + } + + private function showRevision( $revId ) { + global $wgLang, $wgUser, $wgOut; + $archive = new DeletedPage( $this->mDeletedPage, $this->mTargetObj ); + $this->mTargetObj = $archive->getTitle(); + $rev = $archive->getRevision( $revId ); + if( !$rev ) { + $wgOut->addWikiMsg( 'undeleterevision-missing' ); + return; + } + if( $rev->isDeleted(Revision::DELETED_TEXT) ) { + if( !$rev->userCan(Revision::DELETED_TEXT) ) { + $wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) ); + return; + } else { + $wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) ); + $wgOut->addHTML( '
' ); + } + } + $wgOut->setPageTitle( wfMsg( 'undeletepage' ) ); + if( $this->mDiff ) { + $previousRev = $archive->getPreviousRevision( $revId ); + if( $previousRev ) { + $this->showDiff( $previousRev, $rev ); + if( $wgUser->getOption( 'diffonly' ) ) { + return; + } else { + $wgOut->addHtml( '
' ); + } + } else { + $wgOut->addHtml( wfMsgHtml( 'undelete-nodiff' ) ); + } + } + // Date and time are separate parameters to facilitate localisation. + // $time is kept for backward compat reasons. + $timestamp = $rev->getTimestamp(); + $time = htmlspecialchars( $wgLang->timeAndDate( $timestamp, true ) ); + $d = htmlspecialchars( $wgLang->date( $timestamp, true ) ); + $t = htmlspecialchars( $wgLang->time( $timestamp, true ) ); + $user = $this->skin->revUserTools( $rev ); + // Add header + $link = $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ), + $this->mTargetObj->getPrefixedText(), 'deletedpage='.intval($this->mDeletedPage) ); + $wgOut->addHtml( '

' . wfMsgHtml( 'undelete-revision', $link, $time, $user, $d, $t ) . '

' ); + if( $this->mPreview ) { + $wgOut->addHtml( "
\n" ); + //Hide [edit]s + $popts = $wgOut->parserOptions(); + $popts->setEditSection( false ); + $wgOut->parserOptions( $popts ); + $wgOut->addWikiTextTitleTidy( $rev->getText( Revision::FOR_THIS_USER ), $this->mTargetObj, true ); + } + $wgOut->addHtml( + wfElement( 'textarea', array( + 'readonly' => 'readonly', + 'cols' => intval( $wgUser->getOption( 'cols' ) ), + 'rows' => intval( $wgUser->getOption( 'rows' ) ) ), + $rev->getText( Revision::FOR_THIS_USER ) . "\n" ) . + wfOpenElement( 'div' ) . + wfOpenElement( 'form', array( + 'method' => 'post', + 'action' => $this->getTitle()->getLocalURL( "action=submit" ) ) ) . + wfElement( 'input', array( + 'type' => 'hidden', + 'name' => 'deletedpage', + 'value' => $this->mDeletedPage ) ) . + wfElement( 'input', array( + 'type' => 'hidden', + 'name' => 'oldid', + 'value' => $rev->getId() ) ) . + wfElement( 'input', array( + 'type' => 'hidden', + 'name' => 'wpEditToken', + 'value' => $wgUser->editToken() ) ) . + wfElement( 'input', array( + 'type' => 'submit', + 'name' => 'preview', + 'value' => wfMsg( 'showpreview' ) ) ) . + wfElement( 'input', array( + 'name' => 'diff', + 'type' => 'submit', + 'value' => wfMsg( 'showdiff' ) ) ) . + wfCloseElement( 'form' ) . + wfCloseElement( 'div' ) + ); + } + + /** + * Build a diff display between this and the previous either deleted + * or non-deleted edit. + * @param Revision $previousRev + * @param Revision $currentRev + * @return string HTML + */ + private function showDiff( $previousRev, $currentRev ) { + global $wgOut, $wgUser; + $diffEngine = new DifferenceEngine(); + $diffEngine->showDiffStyle(); + $wgOut->addHtml( + "
" . + "" . + "" . + "" . + "" . + "" . + "" . + "\n" . + "\n" . + "" . + $diffEngine->generateDiffBody( $previousRev->getText(), $currentRev->getText() ) . + "
" . + $this->diffHeader( $previousRev, 'o' ) . + "" . + $this->diffHeader( $currentRev, 'n' ) . + "
" . + "
\n" + ); + } + + private function diffHeader( $rev, $prefix ) { + global $wgUser, $wgLang, $wgLang; + $isDeleted = !( $rev->getId() && $rev->getTitle() ); + if( $isDeleted ) { + $targetPage = SpecialPage::getTitleFor( 'Undelete' ); + $targetQuery = 'deletedpage=' . intval($this->mDeletedPage) . '&oldid=' . $rev->getId(); + } else { + $targetPage = $rev->getTitle(); + $targetQuery = 'oldid=' . $rev->getId(); + } + return + '
' . + $this->skin->makeLinkObj( $targetPage, + wfMsgHtml( 'revisionasof', + $wgLang->timeanddate( $rev->getTimestamp(), true ) ), + $targetQuery ) . + ( $isDeleted ? ' ' . wfMsgHtml( 'deletedrev' ) : '' ) . + '
' . + '
' . + $this->skin->revUserTools( $rev ) . '
' . + '
' . + '
' . + $this->skin->revComment( $rev ) . '
' . + '
'; + } + + /** + * Show a deleted file version requested by the visitor. + */ + private function showFile( $key ) { + global $wgOut, $wgRequest; + $wgOut->disable(); + + # We mustn't allow the output to be Squid cached, otherwise + # if an admin previews a deleted image, and it's cached, then + # a user without appropriate permissions can toddle off and + # nab the image, and Squid will serve it + $wgRequest->response()->header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', 0 ) . ' GMT' ); + $wgRequest->response()->header( 'Cache-Control: no-cache, no-store, max-age=0, must-revalidate' ); + $wgRequest->response()->header( 'Pragma: no-cache' ); + + $store = FileStore::get( 'deleted' ); + $store->stream( $key ); + } + + private function showHistory( ) { + global $wgLang, $wgUser, $wgOut; + + $archive = new DeletedPage( $this->mDeletedPage, $this->mTargetObj ); + $this->mTargetObj = $archive->getTitle(); + if( is_null($this->mTargetObj) ) { + $wgOut->showFatalError( wfMsg( "cannotundelete" ) ); + $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) ); + return; + } + + # Show info about how to restore/view this page + $wgOut->addWikiText( wfMsgHtml( 'undeletepagetitle', $this->mTargetObj->getPrefixedText()) ); + if( $this->mAllowed ) { + $wgOut->addWikiMsg( "undelete-usage" ); + $wgOut->addWikiMsg( "undelete-exceptions" ); + } else { + $wgOut->addWikiMsg( "undeletehistorynoadmin" ); + } + + # List all stored revisions + $revisions = new DeletedHistoryPager( $this, $this->mTargetObj, $this->mDeletedPage ); + $files = $archive->listFiles(); + + $haveRevisions = $revisions->getNumRows() > 0; + $haveFiles = $files && $files->numRows() > 0; + + if( $this->mAllowed ) { + $titleObj = SpecialPage::getTitleFor( 'Undelete' ); + $action = $titleObj->getLocalURL( "action=submit" ); + # Start the form here + $top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' ) ); + $wgOut->addHtml( $top ); + } + + # Show relevant lines from the deletion log: + $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) . "\n" ); + LogEventsList::showLogExtract( $wgOut, 'delete', $this->mTargetObj->getPrefixedText() ); + if( $wgUser->isAllowed( 'suppressionlog' ) ) { + $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'suppress' ) ) . "\n" ); + LogEventsList::showLogExtract( $wgOut, 'suppress', $this->mTargetObj->getPrefixedText() ); + } + + if( $this->mAllowed && ( $haveRevisions || $haveFiles ) ) { + # Format the user-visible controls (comment field, submission button) + # in a nice little table + if( $wgUser->isAllowed( 'suppressrevision' ) ) { + $unsuppressBox = + " +   + " . + Xml::checkLabel( wfMsg('revdelete-unsuppress'), 'wpUnsuppress', + 'mw-undelete-unsuppress', $this->mUnsuppress ). + " + "; + } else { + $unsuppressBox = ""; + } + $table = + Xml::openElement( 'fieldset' ) . + Xml::element( 'legend', null, wfMsg( 'undelete-fieldset-title' ) ). + Xml::openElement( 'table', array( 'id' => 'mw-undelete-table' ) ) . + " + " . + wfMsgWikiHtml( 'undeleteextrahelp' ) . + " + + + " . + Xml::label( wfMsg( 'undeletecomment' ), 'wpComment' ) . + " + " . + Xml::input( 'wpComment', 50, $this->mComment, array( 'id' => 'wpComment' ) ) . + " + + +   + " . + Xml::submitButton( wfMsg( 'undeletebtn' ), + array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) . + " + " . + $unsuppressBox . + Xml::closeElement( 'table' ) . + Xml::closeElement( 'fieldset' ); + + $wgOut->addHtml( $table ); + } + + # The page's stored (deleted) history: + $wgOut->addHTML( Xml::element( 'h2', null, wfMsg( 'history' ) ) . "\n" ); + if( $haveRevisions ) { + $wgOut->addHTML( + $revisions->getNavigationBar() . + $revisions->getBody() . + $revisions->getNavigationBar() + ); + } else { + $wgOut->addWikiMsg( "nohistory" ); + } + if( $haveFiles ) { + $wgOut->addHtml( Xml::element( 'h2', null, wfMsg( 'filehist' ) ) . "\n" ); + $wgOut->addHtml( "" ); + } + + # Slip in the hidden controls here + if( $this->mAllowed ) { + $misc = Xml::hidden( 'deletedpage', $this->mDeletedPage ); + $misc .= Xml::hidden( 'target', $this->mTargetObj->getPrefixedDBKey() ); + $misc .= Xml::hidden( 'wpEditToken', $wgUser->editToken() ); + $misc .= Xml::closeElement( 'form' ); + $wgOut->addHtml( $misc ); + } + + return true; + } + + public function formatRevisionRow( $row, $earliestLiveTime ) { + global $wgUser, $wgLang; + $stxt = $revdlink = ''; + $rev = new Revision( $row ); + $ts = $rev->getTimestamp(); + if( $this->mAllowed ) { + $pageLink = $this->getPageLink( $rev, $this->getTitle(), $ts, $this->skin ); + # Last link + if( !$rev->userCan( Revision::DELETED_TEXT ) ) { + $last = wfMsgHtml('diff'); + } else if( $rev->getParentId() || ($earliestLiveTime && $ts > $earliestLiveTime) ) { + $last = $this->skin->makeKnownLinkObj( $this->getTitle(), wfMsgHtml('diff'), + "deletedpage=" . intval($this->mDeletedPage) . "&oldid=".$rev->getId()."&diff=prev" ); + } else { + $last = wfMsgHtml('diff'); + } + } else { + $pageLink = $wgLang->timeanddate( $ts, true ); + $last = wfMsgHtml('diff'); + } + $userLink = $this->skin->revUserTools( $rev ); + if( !is_null($size = $row->rev_len) ) { + $stxt = $this->skin->formatRevisionSize( $size ); + } + $comment = $this->skin->revComment( $rev ); + if( $wgUser->isAllowed( 'deleterevision' ) ) { + if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) { + // If revision was hidden from sysops + $del = wfMsgHtml('rev-delundel'); + } else { + $del = $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Revisiondelete' ), + wfMsgHtml('rev-delundel'), + 'page=' . intval($this->mDeletedPage) . "&oldid=".$rev->getId() ); + // Bolden oversighted content + if( $rev->isDeleted( Revision::DELETED_RESTRICTED ) ) + $del = "$del"; + } + $revdlink = "($del)"; + } + return "
  • $revdlink ($last) $pageLink . . $userLink $stxt $comment
  • "; + } + + /** + * Fetch revision text link if it's available to all users + * @return string + */ + private 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 ), + "deletedpage=".intval($this->mDeletedPage)."&oldid=".$rev->getId() ); + if( $rev->isDeleted(Revision::DELETED_TEXT) ) + $link = '' . $link . ''; + return $link; + } + } + + /** + * Fetch image view link if it's available to all users + * @return string + */ + private 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->mTargetObj->getPrefixedUrl()."&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 + */ + private 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 + * @param File $file + * @return string + */ + private 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; + } + } + + private function formatFileRow( $row, $sk ) { + global $wgUser, $wgLang; + $file = ArchivedFile::newFromRow( $row ); + $ts = $file->getTimestamp(); + 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 = $this->getFileLink( $file, $this->getTitle(), $ts, $key, $sk ); + } else { + $checkBox = ''; + $pageLink = $wgLang->timeanddate( $ts, true ); + } + $userLink = $this->getFileUser( $file, $sk ); + $data = + wfMsg( 'widthheight', + $wgLang->formatNum( $row->fa_width ), + $wgLang->formatNum( $row->fa_height ) ) . + ' (' . + wfMsg( 'nbytes', $wgLang->formatNum( $row->fa_size ) ) . + ')'; + $data = htmlspecialchars( $data ); + $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 = wfMsgHtml('rev-delundel'); + } else { + $del = $sk->makeKnownLinkObj( $revdel, + wfMsgHtml('rev-delundel'), + 'target=' . $this->mTargetObj->getPrefixedUrl() . + '&fileid=' . $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 undelete() { + global $wgOut, $wgUser; + if( wfReadOnly() ) { + $wgOut->readOnlyPage(); + return false; + } + $archive = new DeletedPage( $this->mDeletedPage, $this->mTargetObj ); + $this->mTargetObj = $archive->getTitle(); + if( is_null($this->mTargetObj) ) { + $wgOut->showFatalError( wfMsg( "cannotundelete" ) ); + $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) ); + return false; + } + $ok = $archive->undelete( $this->mComment, $this->mFileVersions, $this->mUnsuppress ); + if( is_array($ok) ) { + if( $ok[1] ) // Undeleted file count + wfRunHooks( 'FileUndeleteComplete', array( $this->mTargetObj, $this->mFileVersions, $wgUser, $this->mComment) ); + + $link = $this->skin->makeKnownLinkObj( $this->mTargetObj ); + $wgOut->addHtml( wfMsgWikiHtml( 'undeletedpage', $link ) ); + } else { + $wgOut->showFatalError( '' . wfMsg( "cannotundelete" ) . '' ); + $wgOut->addHtml( '

    ' . wfMsgHtml( "undelete-exceptions" ) . '

    ' ); + $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) ); + return false; + } + // Show file deletion warnings and errors + $status = $archive->getFileStatus(); + if( $status && !$status->isGood() ) { + $wgOut->addWikiText( $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) ); + } + return true; + } +} + +/** + * Used to show archived pages and eventually restore them. + * @ingroup SpecialPage + */ +class DeletedPage { + protected $title; + var $fileStatus; + + public function __construct( $pageId, $title = NULL ) { + $this->mId = intval($pageId); + // Only use title if no Id is given + if( !is_null($this->mId) ) { + $this->mTitle = $title; + } + } + + public function getTitle() { + if( !is_null($this->mTitle) ) { + return $this->mTitle; + } + $dbr = wfGetDB( DB_SLAVE ); + $row = $dbr->selectRow( 'deleted_pages', + array( 'deleted_page_namespace', 'deleted_page_title' ), + array( 'deleted_page_id' => $this->mId ), + __METHOD__ + ); + if( !$row ) { + return NULL; + } + $this->mTitle = Title::makeTitleSafe( $row->deleted_page_namespace, $row->deleted_page_title ); + return $this->mTitle; + } + + public function getOldPageId() { + return $this->mId; + } + + /** + * List all deleted pages recorded in the archive table. Returns result + * wrapper with (ar_namespace, ar_title, count) fields, ordered by page + * namespace/title. + * + * @return ResultWrapper + */ + public static function listAllPages() { + $dbr = wfGetDB( DB_SLAVE ); + return self::listPages( $dbr, '' ); + } + + /** + * List deleted pages recorded in the archive table matching the + * given title prefix. + * Returns result wrapper with (ar_namespace, ar_title, count) fields. + * + * @return ResultWrapper + */ + public static function listPagesByPrefix( $prefix ) { + $dbr = wfGetDB( DB_SLAVE ); + $title = Title::newFromText( $prefix ); + if( $title ) { + $ns = $title->getNamespace(); + $encPrefix = $dbr->escapeLike( $title->getDBkey() ); + } else { + // Prolly won't work too good + // @todo handle bare namespace names cleanly? + $ns = 0; + $encPrefix = $dbr->escapeLike( $prefix ); + } + $conds = array( + 'deleted_page_namespace' => $ns, + "deleted_page_title LIKE '$encPrefix%'", + 'deleted_page_id' => 'rev_id', + 'deleted_page_suppressed' => 0 + ); + return self::listPages( $dbr, $conds ); + } + + protected static function listPages( $dbr, $condition ) { + $condition['deleted_page_suppressed'] = 0; + return $dbr->resultObject( + $dbr->select( + array( 'deleted_pages', 'revision' ), + array( + 'deleted_page_namespace', + 'deleted_page_title', + 'COUNT(*) AS count' + ), + $condition, + __METHOD__, + array( + 'GROUP BY' => 'deleted_page_namespace,deleted_page_title', + 'ORDER BY' => 'deleted_page_namespace,deleted_page_title', + 'LIMIT' => 100, + ) + ) + ); + } + + /** + * List the deleted file revisions for this page, if it's a file page. + * Returns a result wrapper with various filearchive fields, or null + * if not a file page. + * + * @return ResultWrapper + * @todo Does this belong in Image for fuller encapsulation? + */ + public function listFiles() { + if( $this->getTitle()->getNamespace() == NS_IMAGE ) { + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'filearchive', + array( + 'fa_id', + 'fa_name', + 'fa_archive_name', + 'fa_storage_key', + 'fa_storage_group', + 'fa_size', + '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_deleted' ), + array( 'fa_name' => $this->getTitle()->getDBkey() ), + __METHOD__, + array( 'ORDER BY' => 'fa_timestamp DESC' ) + ); + $ret = $dbr->resultObject( $res ); + # Batch existence check on user and talk pages + $batch = new LinkBatch(); + while( $row = $ret->fetchObject() ) { + $batch->addObj( Title::makeTitleSafe( NS_USER, $row->fa_user_text ) ); + $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->fa_user_text ) ); + } + $batch->execute(); + $ret->seek( 0 ); + return $ret; + } + return null; + } + + /** + * Return a Revision object containing data for the deleted revision. + * @param int $revId + * @return Revision + */ + public function getRevision( $revId ) { + if( !$this->mId ) { + return null; + } + $dbr = wfGetDB( DB_SLAVE ); + $row = $dbr->selectRow( 'revision', + array( '*' ), + array( 'rev_id' => intval($revId), + 'rev_page' => intval($this->mId) ), + __METHOD__ ); + if( $row ) { + return new Revision( array( + 'page' => $this->mId, + 'id' => $row->rev_id, + 'comment' => $row->rev_comment, + 'user' => $row->rev_user, + 'user_text' => $row->rev_user_text, + 'timestamp' => $row->rev_timestamp, + 'minor_edit' => $row->rev_minor_edit, + 'text_id' => $row->rev_text_id, + 'deleted' => $row->rev_deleted, + 'len' => $row->rev_len) ); + } else { + return null; + } + } + + /** + * Return the most-previous revision + * + * @param int $revId + * @return Revision or null + */ + public function getPreviousRevision( $revId ) { + $dbr = wfGetDB( DB_SLAVE ); + // Check the previous deleted revision... + $prevId = $dbr->selectField( 'revision', + 'rev_id', + array( + 'rev_page' => intval($this->mId), + 'rev_id < ' . intval($revId) + ), + __METHOD__, + array( 'ORDER BY' => 'rev_id DESC', 'LIMIT' => 1 ) + ); + if( $prevId ) { + return $this->getRevision( $prevId ); + } else { + // No prior revision on this page. + return null; + } + } + + /** + * Restore the given (or all) text and file revisions for the page. + * Once restored, the items will be removed from the archive tables. + * The deletion log will be updated with an undeletion notice. + * + * @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 + */ + public function undelete( $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( $fileVersions ); + + $restoreText = true; + $restoreFiles = $restoreAll || !empty( $fileVersions ); + + if( $restoreFiles && $this->getTitle()->getNamespace() == NS_IMAGE ) { + $img = wfLocalFile( $this->getTitle() ); + $this->fileStatus = $img->restore( $fileVersions, $unsuppress ); + $filesRestored = $this->fileStatus->successCount; + } else { + $filesRestored = 0; + } + + if( $restoreText ) { + $textRestored = $this->undeleteRevisions( $unsuppress ); + if( $textRestored === false ) { + return false; // It must be one of UNDELETE_* + } + } else { + $textRestored = 0; + } + + // Touch the log! + global $wgContLang; + + if( $textRestored && $filesRestored ) { + $reason = wfMsgExt( 'undeletedrevisions-files', array( 'content', 'parsemag' ), + $wgContLang->formatNum( $textRestored ), + $wgContLang->formatNum( $filesRestored ) ); + } elseif( $textRestored ) { + $reason = wfMsgExt( 'undeletedrevisions', array( 'content', 'parsemag' ), + $wgContLang->formatNum( $textRestored ) ); + } elseif( $filesRestored ) { + $reason = wfMsgExt( 'undeletedfiles', array( 'content', 'parsemag' ), + $wgContLang->formatNum( $filesRestored ) ); + } else { + wfDebug( "Undelete: nothing undeleted...\n" ); + return false; + } + if( trim( $comment ) != '' ) $reason .= ": {$comment}"; + + $log = new LogPage( 'delete' ); + $log->addEntry( 'restore', $this->getTitle(), $reason ); + + return array($textRestored, $filesRestored, $reason); + } + + /** + * This is the meaty bit -- restores archived revisions of the given page + * to the cur/old tables. If the page currently exists, all revisions will + * be stuffed into old, otherwise the most recent will go into cur. + * + * @param 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( $unsuppress = false ) { + if( wfReadOnly() ) { + return false; + } + $dbw = wfGetDB( DB_MASTER ); + # Does this page already exist? We'll have to update it... + $curId = $this->getTitle()->getArticleId( GAID_FOR_UPDATE ); + $previousRevId = $this->getTitle()->getLatestRevId( GAID_FOR_UPDATE ); + if( !$this->getTitle()->isSingleRevRedirect( $curId ) ) { + return false; + } + // Clear any old redirects + $dbw->delete( 'page', + array( + 'page_namespace' => $this->getTitle()->getNamespace(), + 'page_title' => $this->getTitle()->getDBKey() ), + __METHOD__ + ); + // Move title row from deleted_pages table to page table + $dbw->insertSelect( 'page', array( 'deleted_pages' ), + array( + 'page_namespace' => 'deleted_page_namespace', + 'page_title' => 'deleted_page_title', + 'page_id' => 'deleted_page_id', + 'page_random' => wfRandom(), + ), array( + 'deleted_page_id' => $this->mId + ), __METHOD__ + ); + // Mark selected revisions as undeleted + if( $unsuppress ) { + $dbw->update( 'revision', + array( 'rev_deleted' => 0 ), + array( 'rev_page' => $this->mId ), + __METHOD__ + ); + } + // Now that it's safely backed up, delete it + $dbw->delete( 'deleted_pages', array( 'deleted_page_id' => $this->mId ), __METHOD__ ); + // Was anything restored at all? (This is still O(n), but should be reasonable) + $restored = (int)$dbw->selectField( 'revision', 'COUNT(*)', array( 'rev_page' => $this->mId ) ); + if( $restored === 0 ) { + return 0; + } + // Make a fresh Title object + $title = Title::makeTitleSafe( $this->getTitle()->getNamespace(), $this->getTitle()->getDBKey() ); + $title->resetArticleId( $this->mId ); + // Get the new latest revision + $row = $dbw->selectRow( 'revision', '*', + array( 'rev_page' => $this->mId ), + __METHOD__, + array( 'ORDER BY' => 'rev_timestamp DESC' ) + ); + if( !$row ) { + // Revision couldn't be created. This is very weird + return 0; + } + $revision = new Revision( $row ); + // We don't handle well with top revision deleted + if( $revision->getVisibility() ) { + $dbw->update( 'revision', + array( 'rev_deleted' => 0 ), + array( 'rev_page' => $this->mId, + 'rev_id' => $revision->getId() ), + __METHOD__ + ); + } + // Attach the latest revision to the page... + $article = new Article( $title ); + $wasnew = $article->updateIfNewerOn( $dbw, $revision, $previousRevId ); + if( !$curId || $wasnew ) { + // Update site stats, link tables, etc + $article->createUpdates( $revision ); + } + if( !$curId ) { + wfRunHooks( 'ArticleUndelete', array( &$title, true ) ); + Article::onArticleCreate( $this->getTitle() ); + } else { + wfRunHooks( 'ArticleUndelete', array( &$title, false ) ); + Article::onArticleEdit( $this->getTitle() ); + } + // Update hist page + $this->getTitle()->invalidateCache(); + // Clear cache for images + if( $this->getTitle()->getNamespace() == NS_IMAGE ) { + $update = new HTMLCacheUpdate( $title, 'imagelinks' ); + $update->doUpdate(); + } + return $restored; + } + + + public function getFileStatus() { + return $this->fileStatus; + } + + public static function fetchPageList( $title ) { + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( array('deleted_pages','revision'), + array( 'deleted_page_id AS page', 'deleted_on_timestamp', 'COUNT(*) AS revs' ), + array( + 'deleted_page_namespace' => $title->getNamespace(), + 'deleted_page_title' => $title->getDBKey(), + 'deleted_page_id = rev_page' + ), + __METHOD__, + array( 'GROUP BY' => 'deleted_page_id', 'ORDER BY' => 'deleted_on_timestamp DESC' ) + ); + return $res; + } + + // Quick function to grab the page IDs of deleted pages here + public static function getPages( $title ) { + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'deleted_pages', + 'deleted_page_id', + array( 'deleted_page_namespace' => $title->getNamespace(), + 'deleted_page_title' => $title->getDBKey() ), + __METHOD__ ); + $pages = array(); + while( $row = $res->fetchObject() ) { + $pages[] = intval($row->deleted_page_id); + } + return $pages; + } +} + +/** + * @ingroup Pager + */ +class DeletedHistoryPager extends ReverseChronologicalPager { + public $mRemaining = 0, $mEarliestLiveTime = '0', $mTitle, $mForm; + + function __construct( $form, $title, $id, $year='', $month='' ) { + parent::__construct(); + $this->mForm = $form; + $this->mTitle = $title; + $this->mId = $id; + $this->mEarliestLiveTime = $this->mTitle->getEarliestRevTime(); + $this->getDateCond( $year, $month ); + # Treat 500 as the default limit + $urlLimit = $this->mRequest->getInt( 'limit' ); + $this->mLimit = $urlLimit ? $urlLimit : 500; + } + + public function getDefaultQuery() { + $query = parent::getDefaultQuery(); + $query['target'] = $this->mTitle->getPrefixedDBKey(); + return $query; + } + + function getQueryInfo() { + if( !$this->mId ) { + return false; + } + $queryInfo = array( + 'tables' => array('revision'), + 'fields' => Revision::selectFields(), + 'conds' => array( 'rev_page' => $this->mId ), + 'options' => array( 'USE INDEX' => array('revision' => 'page_timestamp') ) + ); + return $queryInfo; + } + + function getIndexField() { + return 'rev_timestamp'; + } + + function formatRow( $row ) { + return $this->mForm->formatRevisionRow( $row, $this->mEarliestLiveTime ); + } + + function getStartBody() { + wfProfileIn( __METHOD__ ); + # Do a link batch query + if( $this->getNumRows() > 0 ) { + $lb = new LinkBatch(); + while( $row = $this->mResult->fetchObject() ) { + $lb->addObj( Title::makeTitleSafe( NS_USER, $row->rev_user_text ) ); + $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->rev_user_text ) ); + } + $lb->execute(); + $this->mResult->seek( 0 ); + } + wfProfileOut( __METHOD__ ); + return ''; + } +} Property changes on: includes\specials\SpecialRestore.php ___________________________________________________________________ Name: svn:eol-style + native Index: includes/specials/SpecialRevisiondelete.php =================================================================== --- includes/specials/SpecialRevisiondelete.php (revision 43299) +++ includes/specials/SpecialRevisiondelete.php (working copy) @@ -19,7 +19,20 @@ # For reviewing deleted files... $file = $wgRequest->getVal( 'file' ); # If this is a revision, then we need a target page - $page = Title::newFromUrl( $target ); + $pageId = $wgRequest->getIntOrNull( 'page' ); + if( $pageId ) { + // Try live pages + $page = Title::newFromID( $pageId ); + // Try deleted pages + if( is_null($page) ) { + $archive = new DeletedPage( $pageId ); + $page = $archive->getTitle(); + if( $page ) + $page->resetArticleId( $pageId ); + } + } else { + $page = Title::newFromUrl( $target ); + } if( is_null($page) ) { $wgOut->addWikiMsg( 'undelete-header' ); return; @@ -74,6 +87,7 @@ global $wgUser, $wgOut; $this->page = $page; + $this->pageExists = (bool)$page->getLatestRevID(); # For reviewing deleted files... if( $file ) { $oimage = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $page, $file ); @@ -184,9 +198,8 @@ $where[] = intval($revid); } $whereClause = 'rev_id IN(' . implode(',',$where) . ')'; - $result = $dbr->select( array('revision','page'), '*', - array( 'rev_page' => $this->page->getArticleID(), - $whereClause, 'rev_page = page_id' ), + $result = $dbr->select( 'revision', '*', + array( 'rev_page' => $this->page->getArticleID(), $whereClause ), __METHOD__ ); while( $row = $dbr->fetchObject( $result ) ) { $revObjs[$row->rev_id] = new Revision( $row ); @@ -268,6 +281,7 @@ $hidden = array( Xml::hidden( 'wpEditToken', $wgUser->editToken() ), Xml::hidden( 'target', $this->page->getPrefixedText() ), + Xml::hidden( 'page', $this->page->getArticleId() ), Xml::hidden( 'type', $this->deleteKey ) ); if( $this->deleteKey=='oldid' ) { @@ -538,13 +552,23 @@ $date = $wgLang->timeanddate( $rev->getTimestamp() ); $difflink = $del = ''; - // Live revisions - if( $this->deleteKey=='oldid' ) { - $revlink = $this->skin->makeLinkObj( $this->page, $date, 'oldid=' . $rev->getId() ); - $difflink = '(' . $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml('diff'), - 'diff=' . $rev->getId() . '&oldid=prev' ) . ')'; + // Live and deleted revisions + if( $this->deleteKey == 'oldid' ) { + // Live revisions + if( $this->pageExists ) { + $revlink = $this->skin->makeLinkObj( $this->page, $date, 'oldid=' . $rev->getId() ); + $difflink = '(' . $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml('diff'), + 'diff=' . $rev->getId() . '&oldid=prev' ) . ')'; + // Deleted revisions + } else { + $undelete = SpecialPage::getTitleFor( 'Undelete' ); + $revlink = $this->skin->makeLinkObj( $undelete, $date, + "deletedpage=".$this->page->getArticleId()."&oldid=".$rev->getId() ); + $difflink = '(' . $this->skin->makeKnownLinkObj( $undelete, wfMsgHtml('diff'), + "deletedpage=".$this->page->getArticleId()."&diff=prev&oldid=" . $rev->getId() ) . ')'; + } // Archived revisions - } else { + } else if( $this->deleteKey == 'artimestamp' ) { $undelete = SpecialPage::getTitleFor( 'Undelete' ); $target = $this->page->getPrefixedText(); $revlink = $this->skin->makeLinkObj( $undelete, $date, @@ -562,7 +586,7 @@ } } - return "
  • $difflink $revlink ".$this->skin->revUserLink( $rev )." ".$this->skin->revComment( $rev )."$del
  • "; + return "
  • $difflink $revlink ".$this->skin->revUserLink( $rev )." ".$this->skin->revComment( $rev )."$del
  • "; } /** Index: includes/Title.php =================================================================== --- includes/Title.php (revision 43299) +++ includes/Title.php (working copy) @@ -1854,19 +1854,31 @@ * Is there a version of this page in the deletion archive? * @return \type{\int} the number of archived revisions */ - public function isDeleted() { - $fname = 'Title::isDeleted'; - if ( $this->getNamespace() < 0 ) { - $n = 0; - } else { - $dbr = wfGetDB( DB_SLAVE ); - $n = $dbr->selectField( 'archive', 'COUNT(*)', array( 'ar_namespace' => $this->getNamespace(), - 'ar_title' => $this->getDBkey() ), $fname ); - if( $this->getNamespace() == NS_IMAGE ) { - $n += $dbr->selectField( 'filearchive', 'COUNT(*)', - array( 'fa_name' => $this->getDBkey() ), $fname ); - } + public function isDeleted( $skipArchive = '' ) { + if( $this->getNamespace() < 0 ) { + return 0; } + $dbr = wfGetDB( DB_SLAVE ); + // See if this page is deleted and if so, how many revs it had + $n = $dbr->selectField( array('deleted_pages','revision'), + 'COUNT(*)', + array( + 'deleted_page_namespace' => $this->getNamespace(), + 'deleted_page_title' => $this->getDBkey(), + 'deleted_page_id = rev_page' + ), __METHOD__ + ); + // Check the old way + if( !$n && $skipArchive !== 'skiparchive' ) { + $n = $dbr->selectField( 'archive', 'COUNT(*)', + array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), + __METHOD__ ); + } + // Check for deleted files + if( $this->getNamespace() == NS_IMAGE ) { + $n += $dbr->selectField( 'filearchive', 'COUNT(*)', + array( 'fa_name' => $this->getDBkey() ), __METHOD__ ); + } return (int)$n; } @@ -2840,6 +2852,43 @@ $this->purgeSquid(); } + + /** + * Checks if this page is just a one-rev redirect + * + * @param &$curId \type{int} page Id + * @return \type{\bool} TRUE or FALSE + */ + public function isSingleRevRedirect( $curId = 0 ) { + $dbw = wfGetDB( DB_MASTER ); + $curId = $curId ? $curId : $this->getArticleId(); + # Nothing here? + if( !$curId ) { + return true; + } + # Is it a redirect? + $isRedirect = $dbw->selectField( array( 'page' ), + 'page_is_redirect', + array( 'page_id' => $curId ), + __METHOD__, + 'FOR UPDATE' + ); + if( !$isRedirect ) { + return false; + } + # Does the article have a history? + $row = $dbw->selectRow( array( 'page', 'revision'), + array( 'rev_id' ), + array( 'page_namespace' => $this->getNamespace(), + 'page_title' => $this->getDBkey(), + 'page_id=rev_page AND page_latest != rev_id' + ), + __METHOD__, + 'FOR UPDATE' + ); + # Return true if there was no history + return ($row === false); + } /** * Checks if $this can be moved to a given Title @@ -2849,10 +2898,7 @@ * @return \type{\bool} TRUE or FALSE */ public function isValidMoveTarget( $nt ) { - - $fname = 'Title::isValidMoveTarget'; $dbw = wfGetDB( DB_MASTER ); - # Is it an existsing file? if( $nt->getNamespace() == NS_IMAGE ) { $file = wfLocalFile( $nt ); @@ -2861,21 +2907,14 @@ return false; } } - - # Is it a redirect? - $id = $nt->getArticleID(); - $obj = $dbw->selectRow( array( 'page', 'revision', 'text'), - array( 'page_is_redirect','old_text','old_flags' ), - array( 'page_id' => $id, 'page_latest=rev_id', 'rev_text_id=old_id' ), - $fname, 'FOR UPDATE' ); - - if ( !$obj || 0 == $obj->page_is_redirect ) { - # Not a redirect - wfDebug( __METHOD__ . ": not a redirect\n" ); + # Is it a redirect with no history? + if( !$nt->isSingleRevRedirect() ) { + wfDebug( __METHOD__ . ": not a one-rev redirect\n" ); return false; } - $text = Revision::getRevisionText( $obj ); - + # Get the article text + $rev = Revision::newFromTitle( $nt ); + $text = $rev->getText(); # Does the redirect point to the source? # Or is it a broken self-redirect, usually caused by namespace collisions? $m = array(); @@ -2892,18 +2931,7 @@ wfDebug( __METHOD__ . ": failsafe\n" ); return false; } - - # Does the article have a history? - $row = $dbw->selectRow( array( 'page', 'revision'), - array( 'rev_id' ), - array( 'page_namespace' => $nt->getNamespace(), - 'page_title' => $nt->getDBkey(), - 'page_id=rev_page AND page_latest != rev_id' - ), $fname, 'FOR UPDATE' - ); - - # Return true if there was no history - return $row === false; + return true; } /** Index: languages/messages/MessagesEn.php =================================================================== --- languages/messages/MessagesEn.php (revision 43299) +++ languages/messages/MessagesEn.php (working copy) @@ -838,7 +838,8 @@ 'unexpected' => 'Unexpected value: "$1"="$2".', 'formerror' => 'Error: could not submit form', 'badarticleerror' => 'This action cannot be performed on this page.', -'cannotdelete' => 'Could not delete the page or file specified. +'cannotdelete' => '\'\'\'Could not delete the page or file specified.\'\'\' + It may have already been deleted by someone else.', 'badtitle' => 'Bad title', 'badtitletext' => 'The requested page title was invalid, empty, or an incorrectly linked inter-language or inter-wiki title. @@ -2425,6 +2426,10 @@ 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.", 'undeleterevisions' => '$1 {{PLURAL:$1|revision|revisions}} archived', +'undelete-deletiondate' => 'deleted on $1', +'undelete-usage' => 'If you restore the page, it will be re-created and all revisions will be returned to the public history.', +'undelete-exceptions' => 'If undeletion would result in the active (top) page or file revision being partially deleted, then such +restrictions will be automatically removed. If a new page with the same name has been created since the deletion, you will have to move the page elsewhere.', 'undeletehistory' => 'If you restore the page, all revisions will be restored to the history. If a new page with the same name has been created since the deletion, the restored revisions will appear in the prior history.', 'undeleterevdel' => 'Undeletion will not be performed if it will result in the top page or file revision being partially deleted. Index: maintenance/archives/patch-deleted_pages.sql =================================================================== --- maintenance/archives/patch-deleted_pages.sql (revision 0) +++ maintenance/archives/patch-deleted_pages.sql (revision 0) @@ -0,0 +1,26 @@ +-- +-- Holding area for deleted pages +-- +CREATE TABLE /*$wgDBprefix*/deleted_pages ( + -- Unique identifier number. The page_id will be preserved across + -- edits and rename operations. + deleted_page_id int unsigned NOT NULL, + + -- A page name is broken into a namespace and a title. + -- The namespace keys are UI-language-independent constants, + -- defined in includes/Defines.php + deleted_page_namespace int NOT NULL, + + -- The rest of the title, as text. + -- Spaces are transformed into underscores in title storage. + deleted_page_title varchar(255) binary NOT NULL, + + -- Was this page suppressed? + deleted_page_suppressed bool NOT NULL, + + -- Timestamp of the last page deletion + deleted_on_timestamp binary(14) NOT NULL default '', + + PRIMARY KEY deleted_page_id (deleted_page_id), + INDEX name_title (deleted_page_namespace,deleted_page_title) +) /*$wgDBTableOptions*/; Property changes on: maintenance\archives\patch-deleted_pages.sql ___________________________________________________________________ Name: svn:eol-style + native Index: maintenance/postgres/archives/patch-deleted_pages.sql =================================================================== --- maintenance/postgres/archives/patch-deleted_pages.sql (revision 0) +++ maintenance/postgres/archives/patch-deleted_pages.sql (revision 0) @@ -0,0 +1,10 @@ +CREATE TABLE deleted_pages ( + deleted_page_id int unsigned NOT NULL PRIMARY KEY, + deleted_page_namespace int NOT NULL, + deleted_page_title varchar(255) binary NOT NULL, + deleted_page_suppressed int NOT NULL, + deleted_on_timestamp TIMESTAMPTZ NOT NULL +); +CREATE INDEX name_title ON deleted_pages (deleted_page_namespace,deleted_page_title); + +ALTER TABLE revision DROP CONSTRAINT revision_rev_page_fkey; Property changes on: maintenance\postgres\archives\patch-deleted_pages.sql ___________________________________________________________________ Name: svn:eol-style + native Index: maintenance/postgres/tables.sql =================================================================== --- maintenance/postgres/tables.sql (revision 43299) +++ maintenance/postgres/tables.sql (working copy) @@ -85,10 +85,10 @@ CREATE SEQUENCE rev_rev_id_val; CREATE TABLE revision ( rev_id INTEGER NOT NULL UNIQUE DEFAULT nextval('rev_rev_id_val'), - rev_page INTEGER NULL REFERENCES page (page_id) ON DELETE CASCADE, + rev_page INTEGER NULL, -- FK rev_text_id INTEGER NULL, -- FK rev_comment TEXT, - rev_user INTEGER NOT NULL REFERENCES mwuser(user_id) ON DELETE RESTRICT, + rev_user INTEGER NOT NULL REFERENCES mwuser(user_id) ON DELETE RESTRICT, rev_user_text TEXT NOT NULL, rev_timestamp TIMESTAMPTZ NOT NULL, rev_minor_edit SMALLINT NOT NULL DEFAULT 0, @@ -131,6 +131,15 @@ ALTER TABLE page_props ADD CONSTRAINT page_props_pk PRIMARY KEY (pp_page,pp_propname); CREATE INDEX page_props_propname ON page_props (pp_propname); +CREATE TABLE deleted_pages ( + deleted_page_id int unsigned NOT NULL PRIMARY KEY, + deleted_page_namespace int NOT NULL, + deleted_page_title varchar(255) binary NOT NULL, + deleted_page_suppressed int NOT NULL, + deleted_on_timestamp TIMESTAMPTZ NOT NULL +); +CREATE INDEX name_title ON deleted_pages (deleted_page_namespace,deleted_page_title); + CREATE TABLE archive ( ar_namespace SMALLINT NOT NULL, ar_title TEXT NOT NULL, Index: maintenance/tables.sql =================================================================== --- maintenance/tables.sql (revision 43299) +++ maintenance/tables.sql (working copy) @@ -327,6 +327,33 @@ -- In case tables are created as MyISAM, use row hints for MySQL <5.0 to avoid 4GB limit -- +-- Holding area for deleted pages +-- +CREATE TABLE /*$wgDBprefix*/deleted_pages ( + -- Unique identifier number. The page_id will be preserved across + -- edits and rename operations. + deleted_page_id int unsigned NOT NULL, + + -- A page name is broken into a namespace and a title. + -- The namespace keys are UI-language-independent constants, + -- defined in includes/Defines.php + deleted_page_namespace int NOT NULL, + + -- The rest of the title, as text. + -- Spaces are transformed into underscores in title storage. + deleted_page_title varchar(255) binary NOT NULL, + + -- Was this page suppressed? + deleted_page_suppressed bool NOT NULL, + + -- Timestamp of the last page deletion + deleted_on_timestamp binary(14) NOT NULL default '', + + PRIMARY KEY deleted_page_id (deleted_page_id), + INDEX name_title (deleted_page_namespace,deleted_page_title) +) /*$wgDBTableOptions*/; + +-- -- Holding area for deleted articles, which may be viewed -- or restored by admins through the Special:Undelete interface. -- The fields generally correspond to the page, revision, and text Index: maintenance/updaters.inc =================================================================== --- maintenance/updaters.inc (revision 43299) +++ maintenance/updaters.inc (working copy) @@ -147,7 +147,8 @@ // 1.14 array( 'add_field', 'site_stats', 'ss_active_users', 'patch-ss_active_users.sql' ), array( 'do_active_users_init' ), - array( 'add_field', 'ipblocks', 'ipb_allow_usertalk', 'patch-ipb_allow_usertalk.sql' ) + array( 'add_field', 'ipblocks', 'ipb_allow_usertalk', 'patch-ipb_allow_usertalk.sql' ), + array( 'add_table', 'deleted_pages', 'patch-deleted_pages.sql' ), ); @@ -1449,6 +1450,7 @@ array("protected_titles", "patch-protected_titles.sql"), array("redirect", "patch-redirect.sql"), array("updatelog", "patch-updatelog.sql"), + array("deleted_pages", "patch-deleted_pages.sql"), ); $newcols = array(