Page MenuHomePhabricator

deletionNew.patch

Authored By
bzimport
Nov 21 2014, 9:56 PM
Size
61 KB
Referenced Files
None
Subscribers
None

deletionNew.patch

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 @@
+<?php
+
+/**
+ * Special page allowing users with the appropriate permissions to view
+ * and restore deleted content
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+
+class SpecialRestore extends UnlistedSpecialPage
+{
+ public function __construct() {
+ parent::__construct( 'Undelete', 'deletedhistory' );
+ }
+
+ public function execute( $par ) {
+ global $wgRequest, $wgUser, $wgOut;
+ $this->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( '<ul>' );
+ $res = DeletedPage::fetchPageList( $this->mTargetObj );
+ while( $row = $res->fetchObject() ) {
+ $deleted = $wgLang->timeanddate( wfTimestamp(TS_MW,$row->deleted_on_timestamp), true );
+ $wgOut->addHTML( '<li>' );
+ $wgOut->addHTML( $this->skin->makeLinkObj( $this->getTitle(),
+ $this->mTargetObj->getPrefixedText(), 'deletedpage='.intval($row->page) ) );
+ $wgOut->addHTML( ' [' . wfMsgExt('undeleterevisions',array('parseinline'),$row->revs) . ']' );
+ $wgOut->addHTML( ' (' . wfMsgExt('undelete-deletiondate',array('parseinline'),$deleted) . ')' );
+ $wgOut->addHTML( '</li>' );
+ }
+ $wgOut->addHTML( '</ul>' );
+ // 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 ) ) .
+ '<fieldset>' .
+ 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' ) ) .
+ '</fieldset>' .
+ '</form>'
+ );
+ }
+
+ // 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( "<ul>\n" );
+ while( $row = $result->fetchObject() ) {
+ $title = Title::makeTitleSafe( $row->deleted_page_namespace, $row->deleted_page_title );
+ $link = $this->skin->makeKnownLinkObj( $this->getTitle(), htmlspecialchars( $title->getPrefixedText() ),
+ 'target=' . $title->getPrefixedUrl() );
+ $revs = wfMsgExt( 'undeleterevisions', array( 'parseinline' ), $wgLang->formatNum( $row->count ) );
+ $wgOut->addHtml( "<li>{$link} ({$revs})</li>\n" );
+ }
+ $result->free();
+ $wgOut->addHTML( "</ul>\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( '<br/>' );
+ }
+ }
+ $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( '<hr />' );
+ }
+ } 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( '<p>' . wfMsgHtml( 'undelete-revision', $link, $time, $user, $d, $t ) . '</p>' );
+ if( $this->mPreview ) {
+ $wgOut->addHtml( "<hr />\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(
+ "<div>" .
+ "<table border='0' width='98%' cellpadding='0' cellspacing='4' class='diff'>" .
+ "<col class='diff-marker' />" .
+ "<col class='diff-content' />" .
+ "<col class='diff-marker' />" .
+ "<col class='diff-content' />" .
+ "<tr>" .
+ "<td colspan='2' width='50%' align='center' class='diff-otitle'>" .
+ $this->diffHeader( $previousRev, 'o' ) .
+ "</td>\n" .
+ "<td colspan='2' width='50%' align='center' class='diff-ntitle'>" .
+ $this->diffHeader( $currentRev, 'n' ) .
+ "</td>\n" .
+ "</tr>" .
+ $diffEngine->generateDiffBody( $previousRev->getText(), $currentRev->getText() ) .
+ "</table>" .
+ "</div>\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
+ '<div id="mw-diff-'.$prefix.'title1"><strong>' .
+ $this->skin->makeLinkObj( $targetPage,
+ wfMsgHtml( 'revisionasof',
+ $wgLang->timeanddate( $rev->getTimestamp(), true ) ),
+ $targetQuery ) .
+ ( $isDeleted ? ' ' . wfMsgHtml( 'deletedrev' ) : '' ) .
+ '</strong></div>' .
+ '<div id="mw-diff-'.$prefix.'title2">' .
+ $this->skin->revUserTools( $rev ) . '<br/>' .
+ '</div>' .
+ '<div id="mw-diff-'.$prefix.'title3">' .
+ $this->skin->revComment( $rev ) . '<br/>' .
+ '</div>';
+ }
+
+ /**
+ * 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 =
+ "<tr>
+ <td>&nbsp;</td>
+ <td class='mw-input'>" .
+ Xml::checkLabel( wfMsg('revdelete-unsuppress'), 'wpUnsuppress',
+ 'mw-undelete-unsuppress', $this->mUnsuppress ).
+ "</td>
+ </tr>";
+ } else {
+ $unsuppressBox = "";
+ }
+ $table =
+ Xml::openElement( 'fieldset' ) .
+ Xml::element( 'legend', null, wfMsg( 'undelete-fieldset-title' ) ).
+ Xml::openElement( 'table', array( 'id' => 'mw-undelete-table' ) ) .
+ "<tr>
+ <td colspan='2'>" .
+ wfMsgWikiHtml( 'undeleteextrahelp' ) .
+ "</td>
+ </tr>
+ <tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'undeletecomment' ), 'wpComment' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'wpComment', 50, $this->mComment, array( 'id' => 'wpComment' ) ) .
+ "</td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td class='mw-submit'>" .
+ Xml::submitButton( wfMsg( 'undeletebtn' ),
+ array( 'name' => 'restore', 'id' => 'mw-undelete-submit' ) ) .
+ "</td>
+ </tr>" .
+ $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( "<ul>" );
+ while( $row = $files->fetchObject() ) {
+ $wgOut->addHTML( $this->formatFileRow( $row, $this->skin ) );
+ }
+ $files->free();
+ $wgOut->addHTML( "</ul>" );
+ }
+
+ # 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 = "<strong>$del</strong>";
+ }
+ $revdlink = "<tt>(<small>$del</small>)</tt>";
+ }
+ return "<li>$revdlink ($last) $pageLink . . $userLink $stxt $comment</li>";
+ }
+
+ /**
+ * 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 '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
+ } else {
+ $link = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ),
+ "deletedpage=".intval($this->mDeletedPage)."&oldid=".$rev->getId() );
+ 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
+ */
+ private 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->mTargetObj->getPrefixedUrl()."&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
+ */
+ private 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
+ * @param File $file
+ * @return string
+ */
+ private 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;
+ }
+ }
+
+ 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 = "<strong>$del</strong>";
+ }
+ $revdlink = "<tt>(<small>$del</small>)</tt>";
+ }
+ return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\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( '<strong>' . wfMsg( "cannotundelete" ) . '</strong>' );
+ $wgOut->addHtml( '<p>' . wfMsgHtml( "undelete-exceptions" ) . '</p>' );
+ $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 '<ul>';
+ }
+
+ function getEndBody() {
+ return '</ul>';
+ }
+}
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 "<li> $difflink $revlink ".$this->skin->revUserLink( $rev )." ".$this->skin->revComment( $rev )."$del</li>";
+ return "<li>$difflink $revlink ".$this->skin->revUserLink( $rev )." ".$this->skin->revComment( $rev )."$del</li>";
}
/**
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(

File Metadata

Mime Type
text/x-diff
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3944
Default Alt Text
deletionNew.patch (61 KB)

Event Timeline