Use job queue for deletion of pages with many revisions

Pages with many revisions experience transaction size exceptions,
due to archiving revisions.  Use the job queue to split the work
into batches and avoid exceptions.

Bug: T198176
Change-Id: Ie800fb5a46be837ac91b24b9402ee90b0355d6cd
This commit is contained in:
Bill Pirkle 2018-08-28 17:01:48 -05:00
parent 306fe0ec7c
commit ca9f1dabf3
9 changed files with 298 additions and 104 deletions

View file

@ -384,6 +384,7 @@ $wgAutoloadLocalClasses = [
'DeleteLogFormatter' => __DIR__ . '/includes/logging/DeleteLogFormatter.php',
'DeleteOldRevisions' => __DIR__ . '/maintenance/deleteOldRevisions.php',
'DeleteOrphanedRevisions' => __DIR__ . '/maintenance/deleteOrphanedRevisions.php',
'DeletePageJob' => __DIR__ . '/includes/jobqueue/jobs/DeletePageJob.php',
'DeleteSelfExternals' => __DIR__ . '/maintenance/deleteSelfExternals.php',
'DeletedContribsPager' => __DIR__ . '/includes/specials/pagers/DeletedContribsPager.php',
'DeletedContributionsPage' => __DIR__ . '/includes/specials/SpecialDeletedContributions.php',

View file

@ -5534,6 +5534,12 @@ $wgAvailableRights = [];
*/
$wgDeleteRevisionsLimit = 0;
/**
* Page deletions with > this number of revisions will use the job queue.
* Revisions will be archived in batches of (at most) this size, one batch per job.
*/
$wgDeleteRevisionsBatchSize = 1000;
/**
* The maximum number of edits a user can have and
* can still be hidden by users with the hideuser permission.
@ -7518,6 +7524,7 @@ $wgServiceWiringFiles = [
* or (since 1.30) a callback to use for creating the job object.
*/
$wgJobClasses = [
'deletePage' => DeletePageJob::class,
'refreshLinks' => RefreshLinksJob::class,
'deleteLinks' => DeleteLinksJob::class,
'htmlCacheUpdate' => HTMLCacheUpdateJob::class,

View file

@ -0,0 +1,32 @@
<?php
/**
* Class DeletePageJob
*/
class DeletePageJob extends Job {
public function __construct( $title, $params ) {
parent::__construct( 'deletePage', $title, $params );
}
/**
* Execute the job
*
* @return bool
*/
public function run() {
// Failure to load the page is not job failure.
// A parallel deletion operation may have already completed the page deletion.
$wikiPage = WikiPage::newFromID( $this->params['wikiPageId'] );
if ( $wikiPage ) {
$wikiPage->doDeleteArticleBatched(
$this->params['reason'],
$this->params['suppress'],
User::newFromId( $this->params['userId'] ),
json_decode( $this->params['tags'] ),
$this->params['logsubtype'],
false,
$this->getRequestId() );
}
return true;
}
}

View file

@ -2053,25 +2053,31 @@ class Article implements Page {
* Perform a deletion and output success or failure messages
* @param string $reason
* @param bool $suppress
* @param bool $immediate false allows deleting over time via the job queue
* @throws FatalError
* @throws MWException
*/
public function doDelete( $reason, $suppress = false ) {
public function doDelete( $reason, $suppress = false, $immediate = false ) {
$error = '';
$context = $this->getContext();
$outputPage = $context->getOutput();
$user = $context->getUser();
$status = $this->mPage->doDeleteArticleReal( $reason, $suppress, 0, true, $error, $user );
$status = $this->mPage->doDeleteArticleReal( $reason, $suppress, 0, true, $error, $user,
[], 'delete', $immediate );
if ( $status->isGood() ) {
if ( $status->isOK() ) {
$deleted = $this->getTitle()->getPrefixedText();
$outputPage->setPageTitle( wfMessage( 'actioncomplete' ) );
$outputPage->setRobotPolicy( 'noindex,nofollow' );
$loglink = '[[Special:Log/delete|' . wfMessage( 'deletionlog' )->text() . ']]';
$outputPage->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink );
Hooks::run( 'ArticleDeleteAfterSuccess', [ $this->getTitle(), $outputPage ] );
if ( $status->isGood() ) {
$loglink = '[[Special:Log/delete|' . wfMessage( 'deletionlog' )->text() . ']]';
$outputPage->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink );
Hooks::run( 'ArticleDeleteAfterSuccess', [ $this->getTitle(), $outputPage ] );
} else {
$outputPage->addWikiMsg( 'delete-scheduled', wfEscapeWikiText( $deleted ) );
}
$outputPage->returnToMain( false );
} else {
@ -2297,10 +2303,10 @@ class Article implements Page {
*/
public function doDeleteArticleReal(
$reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
$tags = []
$tags = [], $immediate = false
) {
return $this->mPage->doDeleteArticleReal(
$reason, $suppress, $u1, $u2, $error, $user, $tags
$reason, $suppress, $u1, $u2, $error, $user, $tags, 'delete', $immediate
);
}
@ -2826,12 +2832,16 @@ class Article implements Page {
* @param int|null $u1 Unused
* @param bool|null $u2 Unused
* @param string &$error
* @param bool $immediate false allows deleting over time via the job queue
* @return bool
* @throws FatalError
* @throws MWException
*/
public function doDeleteArticle(
$reason, $suppress = false, $u1 = null, $u2 = null, &$error = ''
$reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', $immediate = false
) {
return $this->mPage->doDeleteArticle( $reason, $suppress, $u1, $u2, $error );
return $this->mPage->doDeleteArticle( $reason, $suppress, $u1, $u2, $error,
null, $immediate );
}
/**

View file

@ -2512,6 +2512,28 @@ class WikiPage implements Page, IDBAccessObject {
return implode( ':', $bits );
}
/**
* Determines if deletion of this page would be batched (executed over time by the job queue)
* or not (completed in the same request as the delete call).
*
* It is unlikely but possible that an edit from another request could push the page over the
* batching threshold after this function is called, but before the caller acts upon the
* return value. Callers must decide for themselves how to deal with this. $safetyMargin
* is provided as an unreliable but situationally useful help for some common cases.
*
* @param int $safetyMargin Added to the revision count when checking for batching
* @return bool True if deletion would be batched, false otherwise
*/
public function isBatchedDelete( $safetyMargin = 0 ) {
global $wgDeleteRevisionsBatchSize;
$dbr = wfGetDB( DB_REPLICA );
$revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
$revCount += $safetyMargin;
return $revCount >= $wgDeleteRevisionsBatchSize;
}
/**
* Same as doDeleteArticleReal(), but returns a simple boolean. This is kept around for
* backwards compatibility, if you care about error reporting you should use
@ -2526,13 +2548,20 @@ class WikiPage implements Page, IDBAccessObject {
* @param bool|null $u2 Unused
* @param array|string &$error Array of errors to append to
* @param User|null $user The deleting user
* @param bool $immediate false allows deleting over time via the job queue
* @return bool True if successful
* @throws FatalError
* @throws MWException
*/
public function doDeleteArticle(
$reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
$reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
$immediate = false
) {
$status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user );
return $status->isGood();
$status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user,
[], 'delete', $immediate );
// Returns true if the page was actually deleted, or is scheduled for deletion
return $status->isOK();
}
/**
@ -2550,27 +2579,23 @@ class WikiPage implements Page, IDBAccessObject {
* @param User|null $deleter The deleting user
* @param array $tags Tags to apply to the deletion action
* @param string $logsubtype
* @param bool $immediate false allows deleting over time via the job queue
* @return Status Status object; if successful, $status->value is the log_id of the
* deletion log entry. If the page couldn't be deleted because it wasn't
* found, $status is a non-fatal 'cannotdelete' error
* @throws FatalError
* @throws MWException
*/
public function doDeleteArticleReal(
$reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $deleter = null,
$tags = [], $logsubtype = 'delete'
$tags = [], $logsubtype = 'delete', $immediate = false
) {
global $wgUser, $wgContentHandlerUseDB, $wgCommentTableSchemaMigrationStage,
$wgActorTableSchemaMigrationStage, $wgMultiContentRevisionSchemaMigrationStage;
global $wgUser;
wfDebug( __METHOD__ . "\n" );
$status = Status::newGood();
if ( $this->mTitle->getDBkey() === '' ) {
$status->error( 'cannotdelete',
wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
return $status;
}
// Avoid PHP 7.1 warning of passing $this by reference
$wikiPage = $this;
@ -2585,6 +2610,26 @@ class WikiPage implements Page, IDBAccessObject {
return $status;
}
return $this->doDeleteArticleBatched( $reason, $suppress, $deleter, $tags,
$logsubtype, $immediate );
}
/**
* Back-end article deletion
*
* Only invokes batching via the job queue if necessary per $wgDeleteRevisionsBatchSize.
* Deletions can often be completed inline without involving the job queue.
*
* Potentially called many times per deletion operation for pages with many revisions.
*/
public function doDeleteArticleBatched(
$reason, $suppress, User $deleter, $tags,
$logsubtype, $immediate = false, $webRequestId = null
) {
wfDebug( __METHOD__ . "\n" );
$status = Status::newGood();
$dbw = wfGetDB( DB_MASTER );
$dbw->startAtomic( __METHOD__ );
@ -2603,11 +2648,7 @@ class WikiPage implements Page, IDBAccessObject {
return $status;
}
// Given the lock above, we can be confident in the title and page ID values
$namespace = $this->getTitle()->getNamespace();
$dbKey = $this->getTitle()->getDBkey();
// At this point we are now comitted to returning an OK
// At this point we are now committed to returning an OK
// status unless some DB query error or other exception comes up.
// This way callers don't have to call rollback() if $status is bad
// unless they actually try to catch exceptions (which is rare).
@ -2623,6 +2664,133 @@ class WikiPage implements Page, IDBAccessObject {
$content = null;
}
// Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
// one batch of revisions and defer archival of any others to the job queue.
$explictTrxLogged = false;
while ( true ) {
$done = $this->archiveRevisions( $dbw, $id, $suppress );
if ( $done || !$immediate ) {
break;
}
$dbw->endAtomic( __METHOD__ );
if ( $dbw->explicitTrxActive() ) {
// Explict transactions may never happen here in practice. Log to be sure.
if ( !$explictTrxLogged ) {
$explictTrxLogged = true;
LoggerFactory::getInstance( 'wfDebug' )->debug(
'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
'title' => $this->getTitle()->getText(),
] );
}
continue;
}
if ( $dbw->trxLevel() ) {
$dbw->commit();
}
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$lbFactory->waitForReplication();
$dbw->startAtomic( __METHOD__ );
}
// If done archiving, also delete the article.
if ( !$done ) {
$dbw->endAtomic( __METHOD__ );
$jobParams = [
'wikiPageId' => $id,
'requestId' => $webRequestId ?? WebRequest::getRequestId(),
'reason' => $reason,
'suppress' => $suppress,
'userId' => $deleter->getId(),
'tags' => json_encode( $tags ),
'logsubtype' => $logsubtype,
];
$job = new DeletePageJob( $this->getTitle(), $jobParams );
JobQueueGroup::singleton()->push( $job );
$status->warning( 'delete-scheduled',
wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
} else {
// Get archivedRevisionCount by db query, because there's no better alternative.
// Jobs cannot pass a count of archived revisions to the next job, because additional
// deletion operations can be started while the first is running. Jobs from each
// gracefully interleave, but would not know about each other's count. Deduplication
// in the job queue to avoid simultaneous deletion operations would add overhead.
// Number of archived revisions cannot be known beforehand, because edits can be made
// while deletion operations are being processed, changing the number of archivals.
$archivedRevisionCount = $dbw->selectRowCount(
'archive', '1', [ 'ar_page_id' => $id ], __METHOD__
);
// Clone the title and wikiPage, so we have the information we need when
// we log and run the ArticleDeleteComplete hook.
$logTitle = clone $this->mTitle;
$wikiPageBeforeDelete = clone $this;
// Now that it's safely backed up, delete it
$dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
// Log the deletion, if the page was suppressed, put it in the suppression log instead
$logtype = $suppress ? 'suppress' : 'delete';
$logEntry = new ManualLogEntry( $logtype, $logsubtype );
$logEntry->setPerformer( $deleter );
$logEntry->setTarget( $logTitle );
$logEntry->setComment( $reason );
$logEntry->setTags( $tags );
$logid = $logEntry->insert();
$dbw->onTransactionPreCommitOrIdle(
function () use ( $logEntry, $logid ) {
// T58776: avoid deadlocks (especially from FileDeleteForm)
$logEntry->publish( $logid );
},
__METHOD__
);
$dbw->endAtomic( __METHOD__ );
$this->doDeleteUpdates( $id, $content, $revision, $deleter );
Hooks::run( 'ArticleDeleteComplete', [
&$wikiPageBeforeDelete,
&$deleter,
$reason,
$id,
$content,
$logEntry,
$archivedRevisionCount
] );
$status->value = $logid;
// Show log excerpt on 404 pages rather than just a link
$cache = MediaWikiServices::getInstance()->getMainObjectStash();
$key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
$cache->set( $key, 1, $cache::TTL_DAY );
}
return $status;
}
/**
* Archives revisions as part of page deletion.
*
* @param IDatabase $dbw
* @param int $id
* @param bool $suppress Suppress all revisions and log the deletion in
* the suppression log instead of the deletion log
* @return bool
*/
protected function archiveRevisions( $dbw, $id, $suppress ) {
global $wgContentHandlerUseDB, $wgMultiContentRevisionSchemaMigrationStage,
$wgCommentTableSchemaMigrationStage, $wgActorTableSchemaMigrationStage,
$wgDeleteRevisionsBatchSize;
// Given the lock above, we can be confident in the title and page ID values
$namespace = $this->getTitle()->getNamespace();
$dbKey = $this->getTitle()->getDBkey();
$commentStore = CommentStore::getStore();
$actorMigration = ActorMigration::newMigration();
@ -2669,13 +2837,14 @@ class WikiPage implements Page, IDBAccessObject {
}
}
// Get all of the page revisions
// Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
// unusual case where there were exactly $wgDeleteRevisionBatchSize revisions remaining.
$res = $dbw->select(
$revQuery['tables'],
$revQuery['fields'],
[ 'rev_page' => $id ],
__METHOD__,
[],
[ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => $wgDeleteRevisionsBatchSize + 1 ],
$revQuery['joins']
);
@ -2686,16 +2855,22 @@ class WikiPage implements Page, IDBAccessObject {
/** @var int[] Revision IDs of edits that were made by IPs */
$ipRevIds = [];
$done = true;
foreach ( $res as $row ) {
if ( count( $revids ) >= $wgDeleteRevisionsBatchSize ) {
$done = false;
break;
}
$comment = $commentStore->getComment( 'rev_comment', $row );
$user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
$rowInsert = [
'ar_namespace' => $namespace,
'ar_title' => $dbKey,
'ar_timestamp' => $row->rev_timestamp,
'ar_minor_edit' => $row->rev_minor_edit,
'ar_rev_id' => $row->rev_id,
'ar_parent_id' => $row->rev_parent_id,
'ar_namespace' => $namespace,
'ar_title' => $dbKey,
'ar_timestamp' => $row->rev_timestamp,
'ar_minor_edit' => $row->rev_minor_edit,
'ar_rev_id' => $row->rev_id,
'ar_parent_id' => $row->rev_parent_id,
/**
* ar_text_id should probably not be written to when the multi content schema has
* been migrated to (wgMultiContentRevisionSchemaMigrationStage) however there is no
@ -2704,11 +2879,11 @@ class WikiPage implements Page, IDBAccessObject {
* Task: https://phabricator.wikimedia.org/T190148
* Copying the value from the revision table should not lead to any issues for now.
*/
'ar_len' => $row->rev_len,
'ar_page_id' => $id,
'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
'ar_sha1' => $row->rev_sha1,
] + $commentStore->insert( $dbw, 'ar_comment', $comment )
'ar_len' => $row->rev_len,
'ar_page_id' => $id,
'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
'ar_sha1' => $row->rev_sha1,
] + $commentStore->insert( $dbw, 'ar_comment', $comment )
+ $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
if ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
@ -2729,70 +2904,27 @@ class WikiPage implements Page, IDBAccessObject {
$ipRevIds[] = $row->rev_id;
}
}
// Copy them into the archive table
$dbw->insert( 'archive', $rowsInsert, __METHOD__ );
// Save this so we can pass it to the ArticleDeleteComplete hook.
$archivedRevisionCount = $dbw->affectedRows();
// Clone the title and wikiPage, so we have the information we need when
// we log and run the ArticleDeleteComplete hook.
$logTitle = clone $this->mTitle;
$wikiPageBeforeDelete = clone $this;
// This conditional is just a sanity check
if ( count( $revids ) > 0 ) {
// Copy them into the archive table
$dbw->insert( 'archive', $rowsInsert, __METHOD__ );
// Now that it's safely backed up, delete it
$dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
$dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ );
if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
$dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
}
if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
$dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
$dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
$dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
}
if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
$dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
}
// Also delete records from ip_changes as applicable.
if ( count( $ipRevIds ) > 0 ) {
$dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
}
}
// Also delete records from ip_changes as applicable.
if ( count( $ipRevIds ) > 0 ) {
$dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
}
// Log the deletion, if the page was suppressed, put it in the suppression log instead
$logtype = $suppress ? 'suppress' : 'delete';
$logEntry = new ManualLogEntry( $logtype, $logsubtype );
$logEntry->setPerformer( $deleter );
$logEntry->setTarget( $logTitle );
$logEntry->setComment( $reason );
$logEntry->setTags( $tags );
$logid = $logEntry->insert();
$dbw->onTransactionPreCommitOrIdle(
function () use ( $logEntry, $logid ) {
// T58776: avoid deadlocks (especially from FileDeleteForm)
$logEntry->publish( $logid );
},
__METHOD__
);
$dbw->endAtomic( __METHOD__ );
$this->doDeleteUpdates( $id, $content, $revision, $deleter );
Hooks::run( 'ArticleDeleteComplete', [
&$wikiPageBeforeDelete,
&$deleter,
$reason,
$id,
$content,
$logEntry,
$archivedRevisionCount
] );
$status->value = $logid;
// Show log excerpt on 404 pages rather than just a link
$cache = MediaWikiServices::getInstance()->getMainObjectStash();
$key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
$cache->set( $key, 1, $cache::TTL_DAY );
return $status;
return $done;
}
/**

View file

@ -547,6 +547,15 @@ class MovePageForm extends UnlistedSpecialPage {
return;
}
$page = WikiPage::factory( $nt );
// Small safety margin to guard against concurrent edits
if ( $page->isBatchedDelete( 5 ) ) {
$this->showForm( [ [ 'movepage-delete-first' ] ] );
return;
}
$reason = $this->msg( 'delete_and_move_reason', $ot )->inContentLanguage()->text();
// Delete an associated image if there is
@ -559,7 +568,6 @@ class MovePageForm extends UnlistedSpecialPage {
}
$error = ''; // passed by ref
$page = WikiPage::factory( $nt );
$deleteStatus = $page->doDeleteArticleReal( $reason, false, 0, true, $error, $user );
if ( !$deleteStatus->isGood() ) {
$this->showForm( $deleteStatus->getErrorsArray() );

View file

@ -334,6 +334,7 @@
"badarticleerror": "This action cannot be performed on this page.",
"cannotdelete": "The page or file \"$1\" could not be deleted.\nIt may have already been deleted by someone else.",
"cannotdelete-title": "Cannot delete page \"$1\"",
"delete-scheduled": "The page \"$1\" is scheduled for deletion.\nPlease be patient.",
"delete-hook-aborted": "Deletion aborted by hook.\nIt gave no explanation.",
"no-null-revision": "Could not create new null revision for page \"$1\"",
"badtitle": "Bad title",
@ -2729,6 +2730,7 @@
"movepage-moved": "<strong>\"$1\" has been moved to \"$2\"</strong>",
"movepage-moved-redirect": "A redirect has been created.",
"movepage-moved-noredirect": "The creation of a redirect has been suppressed.",
"movepage-delete-first": "The target page has too many revisions to delete as part of a page move. Please first delete the page manually, then try again.",
"articleexists": "A page of that name already exists, or the name you have chosen is not valid.\nPlease choose another name.",
"cantmove-titleprotected": "You cannot move a page to this location because the new title has been protected from creation.",
"movetalk": "Move associated talk page",

View file

@ -536,6 +536,7 @@
"badarticleerror": "Used as error message in moving page.\n\nSee also:\n* {{msg-mw|Articleexists}}\n* {{msg-mw|Bad-target-model}}",
"cannotdelete": "Error message in deleting. Parameters:\n* $1 - page name or file name",
"cannotdelete-title": "Title of error page when the user cannot delete a page. Parameters:\n* $1 - the page name",
"delete-scheduled": "Warning message shown when page deletion is deferred to the job queue, and therefore is not immediate.",
"delete-hook-aborted": "Error message shown when an extension hook prevents a page deletion, but does not provide an error message.",
"no-null-revision": "Error message shown when no null revision could be created to reflect a protection level change.\n\nAbout \"null revision\":\n* Create a new null-revision for insertion into a page's history. This will not re-save the text, but simply refer to the text from the previous version.\n* Such revisions can for instance identify page rename operations and other such meta-modifications.\n\nParameters:\n* $1 - page title",
"badtitle": "The page title when a user requested a page with invalid page name. The content will be {{msg-mw|badtitletext}}.",
@ -2931,6 +2932,7 @@
"movepage-moved": "Message displayed after successfully moving a page from source to target name.\n\nParameters:\n* $1 - the source page as a link with display name\n* $2 - the target page as a link with display name\n* $3 - (optional) the source page name without a link\n* $4 - (optional) the target page name without a link\nSee also:\n* {{msg-mw|Movepage-moved-redirect}}\n* {{msg-mw|Movepage-moved-noredirect}}",
"movepage-moved-redirect": "See also:\n* {{msg-mw|Movepage-moved}}\n* {{msg-mw|Movepage-moved-noredirect}}",
"movepage-moved-noredirect": "The message is shown after pagemove if checkbox \"{{int:move-leave-redirect}}\" was unselected before moving.\n\nSee also:\n* {{msg-mw|Movepage-moved}}\n* {{msg-mw|Movepage-moved-redirect}}",
"movepage-delete-first": "Error message shown when trying to move a page and delete the existing page by that name, but the existing page has too many revisions.",
"articleexists": "Used as error message when moving a page.\n\nSee also:\n* {{msg-mw|Badarticleerror}}\n* {{msg-mw|Bad-target-model}}",
"cantmove-titleprotected": "Used as error message when moving a page.",
"movetalk": "The text of the checkbox to watch the associated talk page to the page you are moving. This only appears when the talk page is not empty. Used in [[Special:MovePage]].\n\nSee also:\n* {{msg-mw|Move-page-legend|legend for the form}}\n* {{msg-mw|newtitle|label for new title}}\n* {{msg-mw|Movereason|label for textarea}}\n* {{msg-mw|Move-leave-redirect|label for checkbox}}\n* {{msg-mw|Fix-double-redirects|label for checkbox}}\n* {{msg-mw|Move-subpages|label for checkbox}}\n* {{msg-mw|Move-talk-subpages|label for checkbox}}\n* {{msg-mw|Move-watch|label for checkbox}}",

View file

@ -108,7 +108,7 @@ class DeleteBatch extends Maintenance {
}
$page = WikiPage::factory( $title );
$error = '';
$success = $page->doDeleteArticle( $reason, false, 0, true, $error, $user );
$success = $page->doDeleteArticle( $reason, false, null, null, $error, $user, true );
if ( $success ) {
$this->output( " Deleted!\n" );
} else {