From ca9f1dabf3719c579fd117e7b9826a3269783e7e Mon Sep 17 00:00:00 2001 From: Bill Pirkle Date: Tue, 28 Aug 2018 17:01:48 -0500 Subject: [PATCH] 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 --- autoload.php | 1 + includes/DefaultSettings.php | 7 + includes/jobqueue/jobs/DeletePageJob.php | 32 +++ includes/page/Article.php | 34 ++- includes/page/WikiPage.php | 310 ++++++++++++++++------- includes/specials/SpecialMovepage.php | 10 +- languages/i18n/en.json | 2 + languages/i18n/qqq.json | 2 + maintenance/deleteBatch.php | 2 +- 9 files changed, 297 insertions(+), 103 deletions(-) create mode 100644 includes/jobqueue/jobs/DeletePageJob.php diff --git a/autoload.php b/autoload.php index a0f505623a..2fda8be678 100644 --- a/autoload.php +++ b/autoload.php @@ -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', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 2668cd7824..a695e76936 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -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, diff --git a/includes/jobqueue/jobs/DeletePageJob.php b/includes/jobqueue/jobs/DeletePageJob.php new file mode 100644 index 0000000000..9b5cef44ea --- /dev/null +++ b/includes/jobqueue/jobs/DeletePageJob.php @@ -0,0 +1,32 @@ +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; + } +} diff --git a/includes/page/Article.php b/includes/page/Article.php index 4a689d318a..db96cf48b9 100644 --- a/includes/page/Article.php +++ b/includes/page/Article.php @@ -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 ); } /** diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php index 7c97465389..7c0450de8b 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -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; } /** diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php index 464be4faca..2f6dc03b10 100644 --- a/includes/specials/SpecialMovepage.php +++ b/includes/specials/SpecialMovepage.php @@ -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() ); diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 30a9699a19..f651e25009 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -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": "\"$1\" has been moved to \"$2\"", "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", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index eeb0a1cf59..75839a60fd 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -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}}", diff --git a/maintenance/deleteBatch.php b/maintenance/deleteBatch.php index 0f3c506734..9e35687dcf 100644 --- a/maintenance/deleteBatch.php +++ b/maintenance/deleteBatch.php @@ -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 { -- 2.20.1